fix(sync): verify the native token sync instead of pretending to perform it #223

Merged
peterswimm merged 2 commits from fix/native-sync-verifies-instead-of-pretending into main 2026-10-21 22:47:03 +00:00
Owner

Scope

The Sync Native Tokens to Rituals lane has been red on main since 2026-10-20, and
it could not have done its job even when green. sync.py has no git calls at all —
it copies generated artifacts into a sibling checkout and stops. In CI that checkout is
a container directory thrown away when the job ends, so no runner has ever delivered a
token to Rituals. The workflow's name described work that has never happened.

This makes it a verifier, which is what CI can honestly do. Delivery stays a human
just sync from a machine that has both repos.

Nothing was actually undelivered. Measured against live main on both sides:
packages/udg/lib/src/udg_colors.dart is byte-identical to dist/rituals/udg_tokens.dart,
and the macOS JSON already carries radiusNone and radiusXl.

The two mechanical faults that made it red

  • actions/setup-python@v4 ran inside container: python:3.12-slim, and this runner
    cannot reach the python-versions manifest, so it died with The version '3.12' with architecture 'x64' was not found. Redundant anyway — the container is python 3.12.
  • secrets.CI_TOKEN does not exist at org or repo scope, so the Rituals checkout failed
    with Input required and not supplied: token on every run this workflow has ever
    had
    . Same defect #215 found in PUBLIC_REPO_TOKEN. It now reads
    secret/forge/rituals-read via the bastia-secret action — the token there belongs to
    ci-rituals-reader, an account with read on toilville_rituals and nothing else.
    No new credential and no new grant: three workflows here already take that route
    (block-validation.yml, test-generator.yml, udg-copy-coverage.yml), and
    test-generator.yml's Rituals checkout succeeded on this PR, so the route is
    known good rather than assumed. (My first commit reached for
    secret/forge/forgejo/ci, the push credential from #215; the second commit corrects
    it. Verified against the live repo: toilville_rituals is private and its
    collaborators are ville, mdn, ci-rituals-reader.)
  • git was missing (only nodejs was installed), so actions/checkout fell back to a
    REST API tarball — not a repository, which defeats align_check.py's git-based sibling
    lookup.

Why fixing only those would have been worse than leaving it red

1. It would have clobbered the macOS app. The manifest pointed
dist/rituals/udg_tokens.json at apps/rituals-macos/Resources/udg_tokens.json. Those
two files share no top-level key:

top-level keys
the app's file light, dark, mode_accents, mode_containers, shadows, state_opacity, font_families
what we generate border, breakpoint, color, radius, space, typography, …

The app's is {mode: {tier_plural}} and additionally holds app concepts this repo does
not generate; the generated one is the transpose. I scanned every JSON under dist/ —
none matches the app's shape. A whole-file copy would have replaced the app's tokens
with an incompatible document. This is the exact hazard
sync-manifest.yml already documents for the Swift maps,
which were disabled for it while this entry was left enabled. Now disabled the same
way, with the measurement recorded.

2. The lane would have gone green while verifying almost nothing. Three paths reported
success having done no work:

  • the Rituals checkout was continue-on-error: true, and the sync step took its else
    branch — "Skipping sync (this is normal in fork PRs)" — and exited 0. This workflow
    only triggers on push: branches: [main], so a fork PR never reaches it. The excuse
    covered nothing.
  • cmd_apply printed ✗ source missing and exited 0, so just sync could inject zero
    files and look like a clean run.
  • load_manifest dropped the last consumer of every product followed by another
    product
    — only the finalize block appended the consumer being accumulated, which the
    last product alone reaches. rituals declares three consumers and the loader returned
    two, silently losing Federated_Code/forgejo. It was never synced, never verified, and
    never reported missing.

What the lane runs now

sync.py --check byte-compares each generated artifact against the file the consumer
holds, so every tier is covered, and it fails rather than skips when a declared
consumer is not checked out. Federated_Code/forgejo carries an explicit
verify_skip_reason (the fork 404s from this forge) so it is named on every run instead
of vanishing.

align_check.py check is wired up beside it — until now it was run by nothing at all,
existing only as just align-check-strict. It covers what a byte comparison cannot: ARGB
literals in the two hand-maintained native files, which are not generated.

Testing

Every claim has a control that fails. All controls run in the python:3.12-slim CI
container with the sibling laid out the way the lane lays it out.

control old new
sibling absent (the failed Rituals checkout) step exits 0, prints "Rituals repo not available" --check exits 1, names the consumer
just sync, every generated source absent exits 0 — "Sync complete: 0 file(s) injected" exits 1
every radius token deleted from the file Rituals ships — sync.py --check 1, align_check 0

That third row is why both gates run rather than one. On the same mutant tree,
sync.py --check is rc=1 and align_check.py check is rc=0.

Which consumers the loader sees:

  • main: Toilville_Rituals twice, never Federated_Code
  • this branch: the dart consumer + Federated_Code/forgejo, with the macOS JSON correctly absent

align_check.py's pre-existing guards still fire (regression-checked): no sibling →
exit 1; a mutated textOnAccent → exit 1; report stays exit 0.

Also found while testing the gate I was about to wire in. align_check's native audit
reads ARGB literals, and udgColorLightHex in token_registry.dart became
{...udg.udgColorLightHex, ..._appColorLightHex} — so the regex matches the map, finds
zero entries, and the Dart half compares nothing. Live measurement: 1 app value compared
against 63 DTCG keys
, all of it from the Swift file. It now prints that denominator every
run and fails if it reaches zero. Control: strip the one Swift literal and check exits 1
where it previously printed in sync ✓.

The floor is at zero, not at a coverage target, deliberately — so this does not newly
red the lane for pre-existing coverage.

One thing this PR cannot prove. The new lane triggers on push: main and
workflow_dispatch, so it does not run on this PR — the first real execution is after
merge. What is verified instead: both gates and the guard step were rehearsed in the
python:3.12-slim CI container in the lane's exact sibling layout (target and control
both), the credential route is the one a lane on this PR used successfully, and
yamllint plus 62 workflow invariants pass. If it does go red on merge, it carries
workflow_dispatch so it can be re-run directly.

Suite: 584 passed / 3 skipped / 0 failed in the CI container. yamllint .forgejo/workflows/ clean. forgeblock_validate, artifact, native, build_surfaces
and forgejo check all report no dist drift.

Impact Statement

  • No token values change. No dist/ output changes. Nothing is written to any other
    repo by this PR or by the lane it creates
    — the lane is read-only by construction, and
    that is the point of the change.
  • The lane's status context is renamed (Sync Native Tokens to Rituals / Sync native tokens to Rituals → Verify native token sync / Verify native token sync). Checked
    against branch protection on main: all eight required contexts are (pull_request)
    lanes, and this is a push: main lane, so nothing required depends on the old name.
  • The lane will now fail where it used to pass vacuously. If a consumer falls behind,
    main goes red until someone runs just sync. That is the intended behaviour and the
    reason for the change, but it is a new way for main to be red.
  • just sync now exits non-zero when a consumer cannot be written. Anything scripted
    around it that relied on exit 0 will start reporting failures — correctly.
  • The trigger gains dist/rituals/tokens.dtcg.json, udg/generators/sync.py and
    udg/generators/align_check.py, since all three are inputs to the verification.
  • Two things I deliberately did not do. The 1-of-63 coverage gap in align_check's
    native audit is reported, not fixed — closing it means either teaching the audit to
    resolve Dart spread operators or dropping that half in favour of byte comparison, and
    that is a judgement about what the hand-maintained files are for. And
    spel/apps/rituals-web declares mode: package-bump, which sync.py does not
    implement; --check now names it on every run rather than passing over it, but
    implementing it is separate work.
## Scope The `Sync Native Tokens to Rituals` lane has been red on main since 2026-10-20, and **it could not have done its job even when green.** `sync.py` has no git calls at all — it copies generated artifacts into a sibling checkout and stops. In CI that checkout is a container directory thrown away when the job ends, so no runner has ever delivered a token to Rituals. The workflow's name described work that has never happened. This makes it a **verifier**, which is what CI can honestly do. Delivery stays a human `just sync` from a machine that has both repos. **Nothing was actually undelivered.** Measured against live main on both sides: `packages/udg/lib/src/udg_colors.dart` is byte-identical to `dist/rituals/udg_tokens.dart`, and the macOS JSON already carries `radiusNone` and `radiusXl`. ### The two mechanical faults that made it red - `actions/setup-python@v4` ran **inside** `container: python:3.12-slim`, and this runner cannot reach the python-versions manifest, so it died with `The version '3.12' with architecture 'x64' was not found`. Redundant anyway — the container *is* python 3.12. - `secrets.CI_TOKEN` does not exist at org or repo scope, so the Rituals checkout failed with `Input required and not supplied: token` on **every run this workflow has ever had**. Same defect #215 found in `PUBLIC_REPO_TOKEN`. It now reads `secret/forge/rituals-read` via the `bastia-secret` action — the token there belongs to `ci-rituals-reader`, an account with read on `toilville_rituals` and nothing else. **No new credential and no new grant:** three workflows here already take that route (`block-validation.yml`, `test-generator.yml`, `udg-copy-coverage.yml`), and `test-generator.yml`'s Rituals checkout **succeeded on this PR**, so the route is known good rather than assumed. (My first commit reached for `secret/forge/forgejo/ci`, the *push* credential from #215; the second commit corrects it. Verified against the live repo: `toilville_rituals` is private and its collaborators are `ville`, `mdn`, `ci-rituals-reader`.) - `git` was missing (only `nodejs` was installed), so `actions/checkout` fell back to a REST API tarball — not a repository, which defeats `align_check.py`'s git-based sibling lookup. ### Why fixing only those would have been worse than leaving it red **1. It would have clobbered the macOS app.** The manifest pointed `dist/rituals/udg_tokens.json` at `apps/rituals-macos/Resources/udg_tokens.json`. Those two files share **no top-level key**: | | top-level keys | |---|---| | the app's file | `light`, `dark`, `mode_accents`, `mode_containers`, `shadows`, `state_opacity`, `font_families` | | what we generate | `border`, `breakpoint`, `color`, `radius`, `space`, `typography`, … | The app's is `{mode: {tier_plural}}` and additionally holds app concepts this repo does not generate; the generated one is the transpose. I scanned every JSON under `dist/` — **none** matches the app's shape. A whole-file copy would have replaced the app's tokens with an incompatible document. This is the exact hazard [sync-manifest.yml](udg/config/sync-manifest.yml) already documents for the Swift maps, which were disabled for it **while this entry was left enabled**. Now disabled the same way, with the measurement recorded. **2. The lane would have gone green while verifying almost nothing.** Three paths reported success having done no work: - the Rituals checkout was `continue-on-error: true`, and the sync step took its `else` branch — *"Skipping sync (this is normal in fork PRs)"* — and exited 0. This workflow only triggers on `push: branches: [main]`, so **a fork PR never reaches it.** The excuse covered nothing. - `cmd_apply` printed `✗ source missing` and exited 0, so `just sync` could inject zero files and look like a clean run. - `load_manifest` **dropped the last consumer of every product followed by another product** — only the finalize block appended the consumer being accumulated, which the last product alone reaches. `rituals` declares three consumers and the loader returned two, silently losing `Federated_Code/forgejo`. It was never synced, never verified, and never reported missing. ### What the lane runs now `sync.py --check` byte-compares each generated artifact against the file the consumer holds, so **every tier is covered**, and it fails rather than skips when a declared consumer is not checked out. `Federated_Code/forgejo` carries an explicit `verify_skip_reason` (the fork 404s from this forge) so it is named on every run instead of vanishing. `align_check.py check` is wired up beside it — **until now it was run by nothing at all**, existing only as `just align-check-strict`. It covers what a byte comparison cannot: ARGB literals in the two *hand-maintained* native files, which are not generated. ## Testing **Every claim has a control that fails.** All controls run in the `python:3.12-slim` CI container with the sibling laid out the way the lane lays it out. | control | old | new | |---|---|---| | sibling absent (the failed Rituals checkout) | step exits **0**, prints "Rituals repo not available" | `--check` exits **1**, names the consumer | | `just sync`, every generated source absent | exits **0** — "Sync complete: 0 file(s) injected" | exits **1** | | every radius token deleted from the file Rituals ships | — | `sync.py --check` **1**, `align_check` **0** | That third row is why **both** gates run rather than one. On the *same mutant tree*, `sync.py --check` is rc=1 and `align_check.py check` is rc=0. **Which consumers the loader sees:** - main: `Toilville_Rituals` twice, **never** `Federated_Code` - this branch: the dart consumer + `Federated_Code/forgejo`, with the macOS JSON correctly absent **`align_check.py`'s pre-existing guards still fire** (regression-checked): no sibling → exit 1; a mutated `textOnAccent` → exit 1; `report` stays exit 0. **Also found while testing the gate I was about to wire in.** `align_check`'s native audit reads ARGB *literals*, and `udgColorLightHex` in `token_registry.dart` became `{...udg.udgColorLightHex, ..._appColorLightHex}` — so the regex matches the map, finds zero entries, and the Dart half compares nothing. Live measurement: **1 app value compared against 63 DTCG keys**, all of it from the Swift file. It now prints that denominator every run and fails if it reaches zero. Control: strip the one Swift literal and `check` exits 1 where it previously printed `in sync ✓`. The floor is at **zero, not at a coverage target**, deliberately — so this does not newly red the lane for pre-existing coverage. **One thing this PR cannot prove.** The new lane triggers on `push: main` and `workflow_dispatch`, so **it does not run on this PR** — the first real execution is after merge. What is verified instead: both gates and the guard step were rehearsed in the `python:3.12-slim` CI container in the lane's exact sibling layout (target and control both), the credential route is the one a lane on this PR used successfully, and `yamllint` plus 62 workflow invariants pass. If it does go red on merge, it carries `workflow_dispatch` so it can be re-run directly. **Suite:** 584 passed / 3 skipped / **0 failed** in the CI container. `yamllint .forgejo/workflows/` clean. `forgeblock_validate`, `artifact`, `native`, `build_surfaces` and `forgejo check` all report **no dist drift**. ## Impact Statement - **No token values change. No `dist/` output changes. Nothing is written to any other repo by this PR or by the lane it creates** — the lane is read-only by construction, and that is the point of the change. - **The lane's status context is renamed** (`Sync Native Tokens to Rituals / Sync native tokens to Rituals` → `Verify native token sync / Verify native token sync`). Checked against branch protection on `main`: all eight required contexts are `(pull_request)` lanes, and this is a `push: main` lane, so **nothing required depends on the old name.** - **The lane will now fail where it used to pass vacuously.** If a consumer falls behind, main goes red until someone runs `just sync`. That is the intended behaviour and the reason for the change, but it is a new way for main to be red. - **`just sync` now exits non-zero** when a consumer cannot be written. Anything scripted around it that relied on exit 0 will start reporting failures — correctly. - The trigger gains `dist/rituals/tokens.dtcg.json`, `udg/generators/sync.py` and `udg/generators/align_check.py`, since all three are inputs to the verification. - **Two things I deliberately did not do.** The `1-of-63` coverage gap in `align_check`'s native audit is reported, not fixed — closing it means either teaching the audit to resolve Dart spread operators or dropping that half in favour of byte comparison, and that is a judgement about what the hand-maintained files are *for*. And `spel/apps/rituals-web` declares `mode: package-bump`, which `sync.py` does not implement; `--check` now names it on every run rather than passing over it, but implementing it is separate work.
fix(sync): verify the native token sync instead of pretending to perform it
All checks were successful
PR policy check / policy-gate (pull_request) Successful in 0s
Security Scans / Trivy filesystem scan (pull_request) Successful in 50s
Block Validation / validate (pull_request) Successful in 1m16s
Security Scans / Gitleaks secret scan (pull_request) Successful in 40s
Test Generator / lint-workflows (pull_request) Successful in 26s
Test Generator / test (pull_request) Successful in 1m12s
Test Generator / lint-prose (pull_request) Successful in 35s
UDG dist drift gate / dist/ matches dtcg + udg sources (pull_request) Successful in 25s
Copy coverage / Design copy reaching rendered HTML (pull_request) Successful in 54s
2aa45a23dd
The "Sync Native Tokens to Rituals" lane has been red on main since
2026-10-20 and could not have done its job even when green. sync.py has
no git calls at all: it copies generated artifacts into a sibling
checkout and stops. In CI that checkout is a container directory thrown
away when the job ends, so no runner has ever delivered a token to
Rituals. The lane is now a verifier, which is what CI can honestly do;
delivery stays a human `just sync`.

Nothing was actually undelivered. Measured against live main on both
sides: packages/udg/lib/src/udg_colors.dart is byte-identical to
dist/rituals/udg_tokens.dart, and the macOS JSON already carries
radiusNone and radiusXl.

Two mechanical faults made the job red:

  * `actions/setup-python@v4` ran INSIDE `container: python:3.12-slim`,
    and this runner cannot reach the python-versions manifest, so it
    died with "The version '3.12' ... was not found". It was redundant
    anyway -- the container IS python 3.12. Removed.
  * `secrets.CI_TOKEN` does not exist at org or repo scope, so the
    Rituals checkout failed with "Input required and not supplied:
    token" on every run this workflow has ever had. That is the same
    defect #215 found in PUBLIC_REPO_TOKEN; it now takes the same route,
    the bastia-secret action reading secret/forge/forgejo/ci. No new
    credential and no new grant -- ci-runner already reads that path.
  * git was missing from the job (only nodejs was installed), so
    actions/checkout fell back to a REST API tarball, which is not a
    repository and defeats align_check.py's git-based sibling lookup.

Fixing only those would have been worse than leaving it red, for two
reasons found while testing it.

First, it would have clobbered the macOS app. The manifest pointed
dist/rituals/udg_tokens.json at apps/rituals-macos/Resources/udg_tokens.json,
and those two files share NO top-level key: the app's is
{mode: {tier_plural}} and additionally holds mode_accents,
mode_containers, state_opacity and font_families, which this repo does
not generate; the generated one is the transpose. No file under dist/
matches the app's shape. A whole-file copy would have replaced the app's
tokens with an incompatible document. This is the hazard the manifest
already documents for the Swift maps, which were disabled for it while
this entry was left enabled. Now disabled the same way, with the
measurement recorded.

Second, the lane would have gone green while verifying almost nothing,
because three paths reported success having done no work:

  * the Rituals checkout was `continue-on-error: true` and the sync step
    took its `else` branch -- "Skipping sync (this is normal in fork
    PRs)" -- and exited 0. This workflow only triggers on push to main,
    so a fork PR never reaches it. The excuse covered nothing.
  * cmd_apply printed "✗ source missing" and exited 0, so `just sync`
    could inject zero files and look like a clean run.
  * load_manifest dropped the last consumer of every product followed by
    another product: only the finalize block appended the consumer being
    accumulated, which the last product alone reaches. `rituals` declares
    three consumers and the loader returned two, silently losing
    Federated_Code/forgejo. It was never synced, never verified and
    never reported missing.

sync.py gains --check, the gate the lane runs. It byte-compares each
generated artifact against the file the consumer holds, so every tier is
covered, and it fails rather than skips when a declared consumer is not
checked out. Federated_Code/forgejo carries an explicit
verify_skip_reason (the fork 404s from this forge) so it is named on
every run instead of vanishing.

align_check.py check, which until now was run by nothing at all -- it
existed only as `just align-check-strict` -- is wired up beside it,
because it covers what a byte comparison cannot: ARGB literals in the
two HAND-MAINTAINED native files, which are not generated. It also
stopped being able to see most of what it claims: udgColorLightHex in
token_registry.dart became `{...udg.udgColorLightHex, ..._appColorLightHex}`,
so the regex matches the map, finds zero literal entries, and the Dart
half compares nothing. Live measurement: 1 app value compared against 63
DTCG keys, all of it from the Swift file. It now prints that denominator
on every run and fails if it reaches zero. The floor is at zero, not at
a coverage target, so this does not newly red the lane for pre-existing
coverage -- the 1-of-63 gap is a follow-up, not something to fix here.

Controls, all run in the python:3.12-slim CI container with the sibling
laid out as the lane lays it out:

  * sibling absent: OLD step body exits 0 printing "Rituals repo not
    available"; sync.py --check exits 1 naming the consumer.
  * every radius token deleted from the file Rituals ships:
    sync.py --check exits 1, align_check.py check exits 0 on the SAME
    mutant tree -- which is why both gates run, not just one.
  * `just sync` with every generated source absent: main exits 0
    ("Sync complete: 0 file(s) injected"), this exits 1.
  * consumers the loader sees: main yields Toilville_Rituals twice and
    never Federated_Code; this yields the dart consumer and
    Federated_Code, with the macOS JSON correctly absent.
  * align_check's own pre-existing guards still fire: no sibling exits
    1, a mutated textOnAccent exits 1, `report` stays exit 0.
  * zero-floor: strip the one Swift literal it compares and check exits
    1 where it previously printed "in sync ✓".

584 passed / 3 skipped / 0 failed in the CI container; yamllint clean;
forgeblock_validate, artifact, native, build_surfaces and forgejo check
all report no dist drift.
fix(sync): read the Rituals token from the path that can read Rituals
All checks were successful
Security Scans / Trivy filesystem scan (pull_request) Successful in 46s
Test Generator / lint-workflows (pull_request) Successful in 37s
Block Validation / validate (pull_request) Successful in 1m16s
Security Scans / Gitleaks secret scan (pull_request) Successful in 1m7s
Test Generator / lint-prose (pull_request) Successful in 38s
UDG dist drift gate / dist/ matches dtcg + udg sources (pull_request) Successful in 29s
Copy coverage / Design copy reaching rendered HTML (pull_request) Successful in 59s
Test Generator / test (pull_request) Successful in 4m31s
PR policy check / policy-gate (pull_request) Successful in 0s
0232eed0e9
Correcting this change's own first draft. It fetched
secret/forge/forgejo/ci -- the forge-ci token, which #215 authorized for
pushing to design-kit-public. Reading the private toilville_rituals is a
different credential: secret/forge/rituals-read, whose token belongs to
ci-rituals-reader, an account with read on that repository and nothing
else. Three workflows here already take that route
(block-validation.yml, test-generator.yml, udg-copy-coverage.yml), and
test-generator's Rituals checkout succeeded on this very PR, so the
route is known good.

Verified against the live repo rather than assumed: toilville_rituals is
private, and its collaborators are ville, mdn and ci-rituals-reader.

Adopts the rest of block-validation.yml's shape, which is better than
what this had:

  * ref: main is pinned. The question is whether the branch Rituals
    ships from holds the tokens this repo generates, whatever branch the
    lane runs on.
  * the token fetch and the checkout fail SOFT, and a guard step then
    fails hard naming the repository, the Vault path and the AppRole
    secrets. A bare checkout failure names none of them -- which is how
    this lane spent its whole life reporting 'Input required and not
    supplied: token' without saying which token or where from. The two
    gates would still fail on their own; they are the backstop.

Guard rehearsed in the CI container from the workspace root, both
directions: consumer present -> pass, sibling moved away -> exit 1.
62 workflow invariants pass; 584 passed / 3 skipped; yamllint clean.
peterswimm deleted branch fix/native-sync-verifies-instead-of-pretending 2026-10-21 22:47:03 +00:00
Sign in to join this conversation.
No description provided.