Skip to content

feat(templates): enforce required metadata - #2

Merged
forkwright merged 1 commit into
mainfrom
feat/required-metadata
May 22, 2026
Merged

feat(templates): enforce required metadata#2
forkwright merged 1 commit into
mainfrom
feat/required-metadata

Conversation

@forkwright

Copy link
Copy Markdown
Owner

Summary

  • add template-level required metadata assertions so custom render paths fail closed
  • require audience and numeric-claim source metadata in FAQ, journal, product, and sizing schemas
  • add a sizing-guide fixture and update existing fixtures/docs for the stricter contracts

Closes #1.

Validation

  • bin/typikon-validate examples/sample-blog
  • bin/typikon-validate examples/sample-shop
  • negative schema probe: product missing price_source fails validation
  • python3 -m json.tool over schemas/*.schema.json
  • kanon lint --summary docs/SCHEMAS.md
  • git diff --check

Not rerun locally

  • Zola render/build gates could not be rerun from this parent shell because zola is not on PATH here. The worker reported Zola 0.22.1 check and build for both fixtures, CSP checks against isolated outputs, and negative render assertion checks before handoff.

Breaking change

This is a breaking schema change for consumers with FAQ, journal, product, or sizing-guide content. No automatic migration is included because meaningful audience and source metadata cannot be inferred safely.

Source note

The Ergon source citation path src/default-templates/deliverable-pdf/README.md:13 is absent on current origin/main; this implementation follows the repo's live schema/template patterns.

@forkwright
forkwright merged commit ae46d94 into main May 22, 2026
@forkwright
forkwright deleted the feat/required-metadata branch May 22, 2026 02:18
forkwright added a commit that referenced this pull request Aug 16, 2026
#153)

Closes #64

Not closing #136 — see "Notes for the reviewer" below for why.

## Session update (rebase + genuine CI failure + comment-tag gap)

This branch was cut before four PRs landed on `main` (#141 favicon
config, #150
asset-provenance label fix, #152 home-page heading, #154 consumer schema
registry). Rebased onto `origin/main` (3d9432a). One real conflict, in
`ci/run-fixtures.sh` — main added four new check invocations
(`check-favicon-path.py`, `check-init-favicon-path.py`,
`check-home-heading.py`,
`check-fixture-corpus-exemption.sh`) in the same region this branch adds
two
(`check-interactive-contrast.py`,
`check-interactive-contrast-selftest.py`).
Both sides were right; composed rather than picked one — every check
from both
branches now runs (`ci/run-fixtures.sh:8-24`).

`gate / full-gate-build` was failing on this branch (run 31921507701)
with a
genuine defect, not a stale base:
`interactive-state-contrast-selftest.spec.ts`
imported `assertContrast`/`contrastRatio`/`textContrastFloor` directly
from
`interactive-state-contrast.spec.ts`. Playwright refuses to load a test
file
that another test file imports and fails the entire collection, not just
the
importing file:

```
Error: test file "smoke/interactive-state-contrast-selftest.spec.ts" should not import test file "smoke/interactive-state-contrast.spec.ts"
Listing tests:
Total: 0 tests in 0 files
```

That's what
`{"stage":"playwright-smoke","status":"fail","duration_ms":1050,...}`
in the CI log was — a collection-time failure before any test ran, which
is
why the log carried no further detail. (The `cargo` exit-101 also
visible in
that run's log is unrelated and pre-existing: `Swatinem/rust-cache`'s
`cargo metadata` probe fails on every typikon run because this repo has
no
`Cargo.toml` — documented and treated as non-fatal in
`.github/workflows/gate-attestation.yml`'s own header comment. It logs
an
error but the job continues past it; the actual failure is the
playwright
stage above.)

Fixed by extracting the pure contrast/threshold math into a new non-spec
module, `ci/smoke/interactive-contrast.ts`, and pointing both spec files
at
it instead of one importing the other
(`ci/smoke/interactive-contrast.ts`
new; `ci/smoke/interactive-state-contrast.spec.ts:26-33` +
`ci/smoke/interactive-state-contrast-selftest.spec.ts:24` updated).
`readEffectiveStyle` stays in the browser-driven spec — it needs a
Playwright
`Locator` and only that file uses it.

**Negative fixture:
`ci/smoke/interactive-state-contrast-selftest.spec.ts` —
watched failing by `npx playwright test --config ci/playwright.config.ts
--list`, which produced exactly the "should not import test file" /
"Total: 0 tests in 0 files" error above before the fix, and now lists 7
tests across 3 files (verified again against the real
`examples/sample-blog`
build: `npx playwright test` — 16/16 passed, including the
browser-driven
interactive-state assertions on every real route).** This isn't a check
I
added — it's Playwright's own test-collection step, which already runs
on
every `bin/typikon-check`/CI invocation with no additional wiring; the
defect was that this branch's own new file tripped it.

While rebasing I re-audited `check-interactive-contrast.py` against its
own
established comment-tag convention (the earlier review round tagged two
blocks — FAQ deep-link anchor, home-triad-mark — that had rationale
prose
with no `WHY:`/`NOTE:`/etc. tag). Two more blocks in the same file had
the
identical gap and were missed by that fix: the `.nav-links
a:hover::after`
"why no separate entry" note
(`ci/check-interactive-contrast.py:216-219`) and
the home-nav-secondary-nav fallthrough note (`:221-224`), plus the
`NOT_TEXT_CONTRAST` dict's own rationale (`:337-343`). Tagged all three
`WHY:`,
matching the file's own convention. Also tagged
`ci/smoke/interactive-state-
contrast.spec.ts`'s `TARGETS` comment (`:56-63`) — same defect class,
same
file inconsistency (the file's other rationale comment,
`readEffectiveStyle`'s, already carried `WHY:`).

## Addressed in the prior independent adversarial review

An independent adversarial review returned `must_fix` on three findings
against an earlier version of this PR. All three were fixed at that time
and
verified intact after this session's rebase:

1. **Correctness — the property regex had no left-boundary anchor.**
   `_prop_var_re()` (`ci/check-interactive-contrast.py:118-126`) built
`(?:{prop_alt})\s*:\s*...` with nothing anchoring the start, unlike the
sibling `COLOR_AFFECTING_RE` two lines above it, which uses
`(?:^|;)\s*(...)`.
   `prop_alt="color"` matched the substring `color:` inside
`background-color:`/`text-decoration-color:`/etc., so a selector with
both
properties on the same rule could silently resolve to the WRONG
property's
value with no error. Fixed by mirroring `COLOR_AFFECTING_RE`'s `(?:^|;)`
boundary in both `_prop_var_re()` and its sibling `_prop_inherit_re()`
(`ci/check-interactive-contrast.py:118-136`). Reproduce the pre-fix bug:
   ```
   python3 -c "
   import sys; sys.path.insert(0, 'ci')
   import importlib.util
spec = importlib.util.spec_from_file_location('m',
'ci/check-interactive-contrast.py')
   m = importlib.util.module_from_spec(spec); spec.loader.exec_module(m)
print(m.find_declared_var('.widget { background-color: var(--danger);
}', '.widget', 'color'))
   "
   ```
   prints `None` now; printed `danger` before the fix. Re-verified this
session: `python3 ci/check-interactive-contrast.py` still passes clean
   post-rebase.

2. **Missing negative-case fixture for the checker's own logic.** Fixed
by
   committing `ci/check-interactive-contrast-selftest.py`, wired into
`ci/run-fixtures.sh` immediately after the checker it tests —
function-level
synthetic-CSS assertions (including the reviewer's own two repro cases)
plus
an end-to-end mutation of the real `static/css/style.css` (reverts the
#64
fix, asserts the exact WCAG-floor failure, restores byte-identical,
confirms
a second uncovered-rule mutation fails closed too). The Playwright side
got
the same treatment via
`ci/smoke/interactive-state-contrast-selftest.spec.ts`.
   Re-run this session: both pass clean (`python3
   ci/check-interactive-contrast-selftest.py`; `npx playwright test
   ci/smoke/interactive-state-contrast-selftest.spec.ts` — 5/5).

3. **Two comment blocks broke the closed comment-tag set.**
`ci/check-interactive-contrast.py:295-299` (FAQ deep-link anchor) and
the
home-triad-mark block were tagged `WHY:`. (This session found and fixed
two
   more instances of the same gap — see above.)

## Enumerated acceptance criteria

Issue #64's `Done when:` (restated identically in the reopening
comment):
**"a matrix covering every token/background/usage combination,
assertions
across the interactive states, and a gate stage that goes red when any
of
them regresses."**

| Bullet | Satisfied by | Evidence |
|---|---|---|
| a matrix covering every token/background/usage combination | `MATRIX`
in `ci/check-interactive-contrast.py:200-335` — 41 entries, each
resolved from the live CSS source (not hand-typed), covering every
selector this theme renders in a non-default interactive state that
touches `color`, a text-bearing `background`, or `opacity` |
`ci/check-interactive-contrast.py:426-461` walks each entry via
`resolve_chain()`; run output lists all 41 with their resolved
token/ratio |
| assertions across the interactive states | Static: `resolve_chain()` +
WCAG floor check per `MATRIX` entry
(`ci/check-interactive-contrast.py:426-461`). Browser:
`ci/smoke/interactive-state-contrast.spec.ts:104-146` drives real
Chromium through default → focus → hover → active per element via
`getComputedStyle`. `:visited` covered by source-absence check (browser
cannot observe it — history-sniffing protection, documented
`ci/smoke/interactive-state-contrast.spec.ts:14-20`) | Real run against
`examples/sample-blog`: 16/16 Playwright tests pass across every route
in the build's sitemap |
| a gate stage that goes red when any of them regresses | Coverage scan
`ci/check-interactive-contrast.py:488-512` fails closed on any
new/unreviewed state rule; `ci/run-fixtures.sh:8-24` runs both the
static and selftest checks on every `bin/typikon-check`/CI invocation;
`ci/smoke/` specs run in the same `npx playwright test` CI already
invokes | Negative fixtures (this PR's own selftest files) prove the red
path — see "Addressed" #2 above and the negative-fixture line above for
this session's fix |

Issue #136's `Done when:` — **not this PR's job**: its specific defect
(`--rule` used as a form-control border) already merged in #140
(`ci/check-control-contrast.py` verifies `.purchase-box select` /
`.buttondown-form input[type="email"]` both clear 3:1 — confirmed still
passing post-rebase: `python3 ci/check-control-contrast.py`). The one
thing
that kept #136 open — the home page's missing `<h1>` — was split into
#142
and landed on `main` as #152 before this branch rebased past it
(`ci/check-home-heading.py`, now composed into `ci/run-fixtures.sh:16`
alongside this PR's own additions). See "Notes for the reviewer" below.

## Evidence (from the original submission, still accurate)

- `ci/check-interactive-contrast.py:200-335` (`MATRIX`) — every selector
this
theme renders in a non-default interactive state that touches `color`, a
text-bearing `background`, or `opacity`, each entry resolved from the
live
  CSS source rather than hand-typed.
- `ci/check-interactive-contrast.py:139-160` (`find_declared_var`,
left-boundary-anchored) + `:163-177` (`resolve_chain`) — walks an
explicit
  selector chain exactly mirroring real CSS cascade/inheritance.
- `ci/check-interactive-contrast.py:208-209` —
  `.nav-links a:nth-child(3):hover`, resolved from source, must equal
  `--aporia-interactive` (4.91:1 against `--bg`) — the exact rule #64
  originally fixed.
- `ci/check-interactive-contrast.py:488-512` (coverage scan) — parses
every
`:hover`/`:focus`/`:focus-visible`/`:active`/`:visited`/`:disabled` rule
touching `color`/`background`/`opacity` and fails if it isn't in
`MATRIX`,
  `NOT_TEXT_CONTRAST`, or the opacity special-case.
- `ci/contrast.py` — the WCAG luminance/contrast formula and `:root`
parser,
extracted from `ci/check-control-contrast.py` (#136) so both checks
share
  one formula.
- `ci/smoke/interactive-state-contrast.spec.ts:128-148`
(`readEffectiveStyle`)
  drives real Chromium and reads `getComputedStyle`'s actual composited
  output, catching a cascade bug a static parse structurally cannot see.
- `:visited` is explicitly out of the Playwright spec's reach
(`ci/smoke/interactive-state-contrast.spec.ts:14-20`): every major
browser
  engine reports `:visited` computed style as if unvisited, to block
history-sniffing. `ci/check-interactive-contrast.py` covers it instead
by
parsing the CSS source directly and confirming no `:visited` rule
exists.
- `ci/run-fixtures.sh:8-24` + `.kanon-ci.toml` — both checks and their
  negative-case fixtures wired into the gate.

## What a reviewer would have to disprove

- That any `MATRIX` entry's resolved token is wrong — reproducible by
running `python3 ci/check-interactive-contrast.py` and reading the
printed
  line for that entry against `static/css/style.css` directly.
- That the coverage scan misses a real state-changing rule —
reproducible by
adding any `:hover`/`:focus`/`:active`/`:visited`/`:disabled` color rule
anywhere in `style.css` and confirming the script fails before making it
  pass by adding a matching entry.
- That `find_declared_var()`'s property regex can still cross-match a
wrong
  property — reproducible by running
`python3 ci/check-interactive-contrast-selftest.py`, which fails closed
on
  that exact class today.
- That the Playwright cross-import defect from this session is still
  present — reproducible by running `npx playwright test --config
ci/playwright.config.ts --list` and confirming it lists tests rather
than
  erroring.

## Notes for the reviewer

- Issue #136 is not closed by this PR and doesn't need to be — its
form-control-border-contrast defect already merged in #140, and the one
  thing keeping #136 open (the home page's missing `<h1>`) was correctly
  split into #142, landed as #152, and is out of this PR's scope.
- No CSS colors changed. `git diff origin/main...HEAD --
static/css/style.css`
  is empty by design.
- `gate / full-gate-build` and `gate / gate` are green on this branch's
current tip (run 31923043711, post-rebase + post-fix) — mergeable,
clean.
- `verda-build` (the fleet's local-gate box) is currently unusable for
this
repo's `fixtures-gate` stage for two reasons unrelated to this branch:
its
disk is at 100% (286M free on 2.4T — tracked in
`metis-ops/collab/OPERATOR.md`),
  and separately it has no `pip`/`jsonschema` installed at all (CI's own
  `check_cmd` installs `jsonschema`/`fonttools`/`Brotli` via pip before
  running `ci/run-fixtures.sh`; verda's gate script does not). Both are
  environment gaps on that box, not code defects — confirmed by running
every `ci/run-fixtures.sh` script individually on this branch, plus the
  real Playwright suite against a real `zola build`, all green. GitHub
Actions' `gate / full-gate-build` is the authoritative signal here and
is
  green.

---------

Co-authored-by: forkwright <cody@forkwright.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(templates): enforce required metadata fields

1 participant