Uh oh!
There was an error while loading. Please reload this page.
fix(i18n): read the provenance companion at serving time in all nine bundle sets - #12724
Conversation
…dle sets Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LZbWd2jNV1FErXTPSS4Dry
…ever served Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LZbWd2jNV1FErXTPSS4Dry
…hen it moves Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LZbWd2jNV1FErXTPSS4Dry
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LZbWd2jNV1FErXTPSS4Dry
…s that newly import it Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LZbWd2jNV1FErXTPSS4Dry
📓 Docs Drift Check9 anchor(s) derived from 8 changed package(s); no hand-written page names any of them. What this run could not see
Coarse fallback — 29 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 3418ef9aa65f14cf37317b97417663be6f0d24a0 && git checkout 3418ef9aa65f14cf37317b97417663be6f0d24a0
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin d29e42f8b1b44b92ea58c1d05e619f27c027c3a9 ae7a467d1497d193df69619457f9fc688c38260d && git checkout -B drift-repro d29e42f8b1b44b92ea58c1d05e619f27c027c3a9 && git merge --no-ff ae7a467d1497d193df69619457f9fc688c38260d
node scripts/docs-audit/affected-docs.mjs --json d29e42f8b1b44b92ea58c1d05e619f27c027c3a9 |
…e ledger Declares @objectstack/platform-objects in plugin-webhooks — already in its install closure via service-messaging, so this declares a resolution that already resolved rather than adding a package to the graph. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LZbWd2jNV1FErXTPSS4Dry
…ests; changeset covers nine sets Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LZbWd2jNV1FErXTPSS4Dry
os-zhuang
commented
Aug 27, 2026
PM review — ACCEPT, ready + enqueuedReviewer of record: The addendum's load-bearing claim, checked independentlyI asked for two things before this could land: that the constdeclared=Object.keys(UNSERVED_PROVENANCE).sort();const{added: undeclared,removed: repaired}=ratchet([...unservedByOut.keys()].sort(),// ← derived from the TREEdeclared,// ← the ledger);
Arithmetic closed independentlyThe report's 9 of 9 sets, 27 of 27 companions. The count the gate prints is the count the tree supports. Fences
On the two flagged deviations
Title change — accepted, for the same reason: "seven more" had stopped being true. The five
Re-deriving the gate union after the manifest change — and finding three families the first derivation could not have named — is the habit that makes the union a measurement instead of a guess. Enqueued. Every check green, not the required subset. Generated by Claude Code |
Fixes#12642.
Provenance is now recorded in 9 of 9 bundle sets and served in 9 of 9. It was recorded in 9 and served in 1.
The measurement, re-taken today (not inherited from the card)
Re-run on
origin/mainat15bf9e85:Every call site was in
@objectstack/platform-objects—src/apps/translations/setup.translation.tsandsrc/metadata-translations/index.ts. Counted from the tree rather than from the card: 27 committed provenance companions across 9 bundle sets, 3 of them read at serving time. The card's "recorded 9 of 9, served 1 of 9" held, and PR #12491 (merged the same day, same directory family) had not moved it.plugin-webhookswas not a latent case: it carries 20 records across its three locales, so the instrument was armed there.The failure, reproduced before anything was changed
The tree is 0-stale by construction — a record is only ever written for a leaf that IS a byte copy of the CURRENT source — so nothing is observable until a source moves. Revising the source string behind
objects.sys_share_link.fields.token.labelinplugin-sharing(recorded in es-ES alone, which is why no gate can testify):findStaleFillson the es-ES companion"Token"— the superseded draft"Share token"— the current source"令牌""令牌", untouchedPositive control, proving the probe can observe a substitution at all: the same mutation on
@objectstack/platform-objects, where the seam was already wired, movedobjects.sys_account._actions.link_social.params.provider.options.applefrom"Apple"(raw es-ES module) to"Apple ID"(served bundle). The probe sees substitutions where substitutions happen.Both mutation legs confirmed on disk by grep counts before and after; both restore legs verified byte-identical to the
HEADblob viagit hash-objectplus an emptygit diff HEAD.⭐ The route triage preferred does not exist — measured, not assumed
This is the most reusable result in the PR, so it is stated plainly: a future reader will otherwise propose the same route again.
Triage's preferred route was move the seam into the i18n loader, so a set opts in by committing a companion rather than by wiring a call. That seam does not exist. Measured:
II18nService.loadTranslations(locale, translations), declared inpackages/spec/src/contracts/i18n-service.ts. It receives one locale at a time and has no channel for either the source bundle or the recorded table. Applying provenance there means widening a published contract.kernel:readyhook.AppPlugin.loadTranslationsin@objectstack/runtimedoes see a wholeTranslationBundle, but the nine platform sets never go through it — it servesdefineStack({ translations })app bundles — and it has no companion channel either.The only place a bundle AND its provenance companion are both in scope is the bundle set's own serving barrel. So the thing that can be centralised is the CODE — already public at
@objectstack/platform-objects/apps— and the GUARD, not the call site.Two relocation routes were measured and rejected. Relocate to
@objectstack/core: reaches 8 of 9 free, butplatform-objectsholdscoreonly as a devDependency, so it trades one new edge for another, and drags the CLI along. Relocate to@objectstack/spec: the only package all nine already depend on, but it widens a published contract for a utility with nine internal consumers and no external pull — clause ②, and the spec seat's call, not this lane's.The ninth set, and why the dependency edge is the right move
@objectstack/plugin-webhookswas the one set that could not reach the seam: it depends on@objectstack/core,@objectstack/service-messagingand@objectstack/spec— not on@objectstack/platform-objects. That fork was stopped on and reported before being taken, and the measurement that resolved it:⇒
platform-objectsis already inplugin-webhooks' install closure throughservice-messaging. Declaring it adds no package to the graph — it declares a resolution that already resolves. Relying on that resolution without declaring it is the phantom-dependency shape under this repo's strict package manager, so declaring it is the correct move rather than a shortcut. The barrel is then wired identically to the other seven: one mechanism, no variant.Why a new gate verdict, and why structural
check:i18n-stale-fillgains a second verdict, UNSERVED PROVENANCE, kept distinct from the stale-fill verdict the waycheck:i18nkeeps its two. It extends the guard that already walks every bundle set and already knows the companions by name, rather than adding a tenth script.Wiring nine sets without it would fix today's instance while leaving "recorded implies served" an assumption — rebuilding the exact inference hazard the card was filed about, one layer further on. The card's hazard is what a reader concludes from "recorded 9 of 9"; this verdict is what makes that conclusion true and keeps it true for a tenth set that lands tomorrow.
The behavioural alternatives are vacuous today and were measured as such: served-versus-source bytes are equal whether or not the seam is wired (0-stale by construction), and object identity does not separate them either, because
withSourceFallbackdeliberately returns its input by reference when nothing is stale. A gate observable only green is indistinguishable from a gate matching nothing. So the verdict asks the structural question — is this companion's own exported identifier passed as an ARGUMENT to awithSourceFallback(...)call in this package? — and publishes the recognised spelling in its failure text, the tradecheck:cross-package-test-inputsdocuments.Naming the identifier is not enough and neither is calling the seam: the near-miss the rule must separate is a set that wires two locales and forgets the third. Both halves are required per companion, which is per locale.
UNSERVED_PROVENANCEledger is EMPTY, and that is where a guard usually diesplugin-webhooksheld the only entry while its dependency question was open; wiring it deleted that entry in the same change, because the ledger is shrink-only and fails both ways — a stale entry reds the gate. An empty registry is exactly the state where a guard degenerates into a no-op and nobody notices, so the discrimination is proven rather than assumed, at two levels:Mechanically, in
--self-test— the verdict now compares through the same two-sidedratchetthe stale-fill verdict uses, so these cases are its decision procedure rather than a model of it:End to end, on the real tree — un-wiring
plugin-webhooks, the very set whose entry was just deleted, with the ledger empty:27 provenance companion(s), 27 served at serving time, 0 bundle set(s) unserved (0 declared)UNSERVED PROVENANCE — 1 bundle set(s) record provenance and never read it, naming all three companions and the identifier each exportsAblation — both new guards were observed RED
Beyond the empty-ledger arms above, the earlier ablation on
plugin-sharing(barrel reverted to theorigin/mainshape, confirmed on disk as 3 → 0withSourceFallback(occurrences):check:i18n-stale-fill→ exit 1, UNSERVED PROVENANCE naming all three companions.packages/plugins/plugin-sharing/src/translations/serving-seam.test.ts→ exit 1,AssertionError: expected 'Token' to be 'Share token'— the measured failure itself.No rebuild was needed between legs and this is load-bearing rather than an omission: the barrel is TypeScript source read relatively by both the test and the gate, and
withSourceFallbackcomes fromplatform-objects/dist, which the ablation does not touch. Every restore verified byte-identical to theHEADblob, under atraponEXIT INT TERMwith absolute paths.Scope
withSourceFallbackwas already public at@objectstack/platform-objects/apps— the CLI has imported that barrel since the recording rollout.ento the source makes the asymmetry sharper, not smaller (needs a maintainer decision) #8765 Option B, and every key-set claim over these bundles is untouched.packages/specnot touched — read-only for this seat.content/docs/releases/**untouched.@objectstack/platform-objectsdeclared inplugin-webhooks, for the reason measured above.pathsrules resolving the seam to source inplugin-approvalsandplugin-audit(check:type-source-resolution), and matching anchored vitest aliases inplugin-auditandplugin-webhooks(check:test-source-alias). Both registries are shrink-only; neither was widened.Verification
Gate union derived from the ACTUAL changed paths with
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack— not from a hand-written diff — and re-derived after the manifest change, which pulled in three families the first derivation could not see (check:override-consistency,check-osv-exemptions,check-changeset-fixed). Every exit code captured before any pipe. All runs below are onae7a467d, the final commit of this branch, with a clean tree.Green:
check:nul-bytes·check:i18n·check:i18n-stale-fill(both verdicts, plus its own self-test: 27 assertions, 12 of them new) ·check:override-consistency·check-osv-exemptions·check-changeset-fixed·check:agent-test-spelling·check:bash32-floor·check:cli-command-ids·check:entry-guard·check:parse-guard·check:pnpm-filter-targets·check:page-declaration-shape·check:published-files·check:slot-lookup·check:test-source-alias·check:type-source-resolution·check:cross-package-test-inputs(both invocations) ·check:type-check-coverage·check:type-check-debt(--re-measure: 31 ledger entries, 1687 raw errors, none above its recorded number — unchanged by this diff; the one surplus it reports is a pre-existing@objectstack/runtimeentry) ·check:engine-double-contract·check:where-matcher·check:query-options-erasure·check:objectql-double-limit·check-comment-mask-adoption·check-plugin-teardown-shape·check-ci-filter-parity·check:changeset-gate-self-tests·check:objectui-changeset·check:pm-half-states·check-changeset-no-major·check-empty-changeset·check-adr-0087-registration·release-rehearsal-clone --self-test·bare-root-worklist --self-test·check:pm-dispatch-gates.Tests for the eight wired packages: 239 test files, 4049 tests, all passing.
typecheckgreen for the seven that declare the script.Not measured, and recorded as not measured rather than green:
@objectstack/service-storagedeclares notypecheckscript, so the filtered run matched zero scripts and exited 0 having measured nothing. A zero-match--filterrun is the same trap as a gate that refuses.scripts/pm/check-half-states.mjsrefuses withPREREQUISITE NOT MET(exit 3) — this container holds no valid GitHub credential, and the script states itself that it swept no issue. It judges the PM board, not this diff.Generated by Claude Code