fix(sync): verify the native token sync instead of pretending to perform it #223
No reviewers
Labels
No labels
build-dist
priority
P1
priority
P2
priority
P3
priority
P4
bug
duplicate
enhancement
help wanted
invalid
question
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
Toilville/toilville-design!223
Loading…
Reference in a new issue
No description provided.
Delete branch "fix/native-sync-verifies-instead-of-pretending"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Scope
The
Sync Native Tokens to Ritualslane has been red on main since 2026-10-20, andit could not have done its job even when green.
sync.pyhas 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 syncfrom a machine that has both repos.Nothing was actually undelivered. Measured against live main on both sides:
packages/udg/lib/src/udg_colors.dartis byte-identical todist/rituals/udg_tokens.dart,and the macOS JSON already carries
radiusNoneandradiusXl.The two mechanical faults that made it red
actions/setup-python@v4ran insidecontainer: python:3.12-slim, and this runnercannot 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_TOKENdoes not exist at org or repo scope, so the Rituals checkout failedwith
Input required and not supplied: tokenon every run this workflow has everhad. Same defect #215 found in
PUBLIC_REPO_TOKEN. It now readssecret/forge/rituals-readvia thebastia-secretaction — the token there belongs toci-rituals-reader, an account with read ontoilville_ritualsand 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), andtest-generator.yml's Rituals checkout succeeded on this PR, so the route isknown good rather than assumed. (My first commit reached for
secret/forge/forgejo/ci, the push credential from #215; the second commit correctsit. Verified against the live repo:
toilville_ritualsis private and itscollaborators are
ville,mdn,ci-rituals-reader.)gitwas missing (onlynodejswas installed), soactions/checkoutfell back to aREST API tarball — not a repository, which defeats
align_check.py's git-based siblinglookup.
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.jsonatapps/rituals-macos/Resources/udg_tokens.json. Thosetwo files share no top-level key:
light,dark,mode_accents,mode_containers,shadows,state_opacity,font_familiesborder,breakpoint,color,radius,space,typography, …The app's is
{mode: {tier_plural}}and additionally holds app concepts this repo doesnot 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:
continue-on-error: true, and the sync step took itselsebranch — "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 excusecovered nothing.
cmd_applyprinted✗ source missingand exited 0, sojust synccould inject zerofiles and look like a clean run.
load_manifestdropped the last consumer of every product followed by anotherproduct — only the finalize block appended the consumer being accumulated, which the
last product alone reaches.
ritualsdeclares three consumers and the loader returnedtwo, silently losing
Federated_Code/forgejo. It was never synced, never verified, andnever reported missing.
What the lane runs now
sync.py --checkbyte-compares each generated artifact against the file the consumerholds, so every tier is covered, and it fails rather than skips when a declared
consumer is not checked out.
Federated_Code/forgejocarries an explicitverify_skip_reason(the fork 404s from this forge) so it is named on every run insteadof vanishing.
align_check.py checkis 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: ARGBliterals 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-slimCIcontainer with the sibling laid out the way the lane lays it out.
--checkexits 1, names the consumerjust sync, every generated source absentsync.py --check1,align_check0That third row is why both gates run rather than one. On the same mutant tree,
sync.py --checkis rc=1 andalign_check.py checkis rc=0.Which consumers the loader sees:
Toilville_Ritualstwice, neverFederated_CodeFederated_Code/forgejo, with the macOS JSON correctly absentalign_check.py's pre-existing guards still fire (regression-checked): no sibling →exit 1; a mutated
textOnAccent→ exit 1;reportstays exit 0.Also found while testing the gate I was about to wire in.
align_check's native auditreads ARGB literals, and
udgColorLightHexintoken_registry.dartbecame{...udg.udgColorLightHex, ..._appColorLightHex}— so the regex matches the map, findszero 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
checkexits 1where 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: mainandworkflow_dispatch, so it does not run on this PR — the first real execution is aftermerge. What is verified instead: both gates and the guard step were rehearsed in the
python:3.12-slimCI container in the lane's exact sibling layout (target and controlboth), the credential route is the one a lane on this PR used successfully, and
yamllintplus 62 workflow invariants pass. If it does go red on merge, it carriesworkflow_dispatchso 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_surfacesand
forgejo checkall report no dist drift.Impact Statement
dist/output changes. Nothing is written to any otherrepo by this PR or by the lane it creates — the lane is read-only by construction, and
that is the point of the change.
Sync Native Tokens to Rituals / Sync native tokens to Rituals→Verify native token sync / Verify native token sync). Checkedagainst branch protection on
main: all eight required contexts are(pull_request)lanes, and this is a
push: mainlane, so nothing required depends on the old name.main goes red until someone runs
just sync. That is the intended behaviour and thereason for the change, but it is a new way for main to be red.
just syncnow exits non-zero when a consumer cannot be written. Anything scriptedaround it that relied on exit 0 will start reporting failures — correctly.
dist/rituals/tokens.dtcg.json,udg/generators/sync.pyandudg/generators/align_check.py, since all three are inputs to the verification.1-of-63coverage gap inalign_check'snative 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-webdeclaresmode: package-bump, whichsync.pydoes notimplement;
--checknow names it on every run rather than passing over it, butimplementing it is separate work.
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.