feat(ci): frontmatter schema validator — hard-block from day one#196
Conversation
Closes the implementation gap in klappy://canon/constraints/frontmatter-validation-before-merge. Canon mandates automated frontmatter validation on every writings/ PR "No Exceptions". The existing canon-quality.yml workflow only ran oddkit_audit (which audits klappy:// reference integrity), so frontmatter violations passed CI silently and the renderer silently dropped them from the homepage. That gap caused the May 10 incident (PR #194). This change adds a parallel CI job that runs scripts/validate-frontmatter.py against writings/ on every PR and push. The validator mirrors the enum and required-field rules in canon/meta/frontmatter-schema.md. Five rule_ids, each mapped to a documented Known Crash Pattern: - frontmatter-missing-block — no --- delimiters - frontmatter-parse-error — malformed YAML - frontmatter-missing-required — universal field absent, OR essay discovery field (type/slug/hook/ description) absent on public writings - frontmatter-invalid-enum — exposure/voice/tier/audience value outside the canon-allowed set - frontmatter-type-mismatch — quoted boolean or quoted integer - frontmatter-contradictory — public:false + exposure:public Hard-block from day one. No soft observation cycle. Rationale: the schema is unambiguous (enums, not subjective judgment), the renderer's failure mode is silent (no operator signal), canon mandates it 'No Exceptions', and dry-run against all 40 essays in main returns zero findings — no existing PRs would be blocked at flip time. Why not extend oddkit_audit? The oddkit_audit spec (docs/oddkit/specs/oddkit-audit.md, DRAFT v2.2) is explicitly KISS — 'one check, two rule_ids' — and mandates new checks become 'their own thin action — not bolted into this one.' Frontmatter validation is also klappy.dev-specific: the enum values belong to one knowledge base, not to oddkit's generic codebase. Per Vodka Architecture, klappy.dev-specific rules belong here. The oddkit_audit job runs in parallel and is unaffected. Tests: - scripts/tests/test_validator.py exercises all five rule classes against fixtures + the live writings/ directory (must remain clean for the test suite to pass). - Fixtures isolated under scripts/tests/fixtures/, outside the workflow's writings/ scan path. Verification: $ python3 scripts/validate-frontmatter.py ✅ Frontmatter OK — 40 file(s) scanned, 0 findings. $ python3 scripts/tests/test_validator.py All validator smoke tests passed. Canon: - klappy://canon/constraints/frontmatter-validation-before-merge (amended Automation section to describe the implementation) - klappy://canon/meta/frontmatter-schema (unchanged — source of truth)
Canon Quality — Frontmatter Schema ✅All 40 file(s) in Validator: |
Canon Quality —
|
… contradiction - Align ENUMS[audience] with canon/meta/frontmatter-schema.md (add operators, apocrypha; remove non-schema internal) - Detect contradictory frontmatter even when public is the quoted string false, so both the type-mismatch and contradiction surface in a single CI pass
Python's bool is a subclass of int, so True == 1 and True in {1,2,3,4}
silently returns True. This let YAML 'tier: true' bypass both the enum
check and the integer type-mismatch check. Add an explicit bool-vs-int
guard in the enum validator so 'tier: true' is reported as an invalid
enum value.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Reviewed by Cursor Bugbot for commit f1c23b0. Configure here.
| f"{field}: {v!r}", | ||
| f'Field "{field}" has value {v!r}, which is not in the ' | ||
| f"allowed set: [{allowed_repr}]. Canon: {CANON_REF}", | ||
| )) |
There was a problem hiding this comment.
Enum check emits misleading finding for quoted integers
Low Severity
When a user writes tier: "3", the enum check fires because the string "3" is not in {1, 2, 3, 4} (integers), emitting a frontmatter-invalid-enum finding saying the value is "not in the allowed set: [1, 2, 3, 4]." This is misleading — the value 3 IS valid, only the quoting is wrong. The frontmatter-type-mismatch check later correctly identifies the root cause. The author already added a bool_mismatch guard for True/False vs integer confusion in the same block, but the analogous string-digit case for INTEGER_FIELDS is unguarded, producing a confusing duplicate finding.
Reviewed by Cursor Bugbot for commit f1c23b0. Configure here.


What
Closes the implementation gap in
klappy://canon/constraints/frontmatter-validation-before-merge.Canon already mandates automated frontmatter validation on every
writings/PR — "No Exceptions". The existingcanon-quality.ymlworkflow only ranoddkit_audit(reference-integrity), so frontmatter violations passed CI silently and the renderer silently dropped them from the homepage. That gap caused the May 10 incident (PR #194).This adds a parallel CI job —
frontmatter— that runsscripts/validate-frontmatter.pyagainstwritings/on every PR and push. The validator mirrors the enum and required-field rules incanon/meta/frontmatter-schema.md.Six rule_ids, each tied to a Known Crash Pattern
frontmatter-missing-block---delimiters at allfrontmatter-parse-errorfrontmatter-missing-requiredtype/slug/hook/description) absent on public writingsfrontmatter-invalid-enumexposure/voice/tier/audiencevalue outside the canon-allowed setfrontmatter-type-mismatchpublic: "true") or quoted integer (tier: "3")frontmatter-contradictorypublic: false+exposure: publicHard-block from day one
No soft observation cycle. Rationale:
mainreturns zero findings. No existing PRs are blocked at flip time.Why not extend
oddkit_audit?The
oddkit_auditspec (docs/oddkit/specs/oddkit-audit.md, DRAFT v2.2) is explicitly KISS — "one check, two rule_ids" — and mandates new checks become "their own thin action — not bolted into this one." Frontmatter validation is also klappy.dev-specific: the enum values belong to one knowledge base, not to oddkit's generic codebase. Per Vodka Architecture, klappy.dev-specific rules belong here.oddkit_auditruns in parallel and is unaffected.Tests
scripts/tests/test_validator.pyexercises all five rule classes against fixtures plus the livewritings/directory.scripts/tests/fixtures/, outside the workflow'swritings/scan path.Local verification:
Canon
klappy://canon/constraints/frontmatter-validation-before-merge—Automationsection amended to describe the implementation.klappy://canon/meta/frontmatter-schema— unchanged. Single source of truth for what the validator mirrors.Note
Medium Risk
Introduces a new hard-blocking CI gate on PRs touching
writings/**, which can immediately prevent merges if the validator is wrong or drifts fromcanon/meta/frontmatter-schema.md. The changes are isolated to CI + a new Python validator, but impact the main development workflow.Overview
Adds a new hard-blocking
frontmatterCI job in.github/workflows/canon-quality.ymlthat runsscripts/validate-frontmatter.pyonwritings/, uploads a JSON findings artifact, posts a sticky PR comment/step summary, and fails the workflow if any violations are found.Introduces
scripts/validate-frontmatter.py, a schema-mirroring validator that checks for missing/invalid frontmatter, required fields (including extra discovery fields for public writings), enum violations, type mismatches (quoted booleans/integers), and contradictorypublic/exposureflags, plus adds smoke tests + fixtures underscripts/tests/.Updates
canon/constraints/frontmatter-validation-before-merge.mdto document the new CI implementation, rule IDs, and the “no exceptions” hard-block enforcement.Reviewed by Cursor Bugbot for commit f1c23b0. Bugbot is set up for automated code reviews on this repo. Configure here.