Uh oh!
There was an error while loading. Please reload this page.
fix(platform-objects): sys_secret field help stops asserting the settings-only reading - #12600
Conversation
…ings-only reading The object's own managedBy note records three privileged producers, but the object description and the namespace / key / id field descriptions still described a settings-only table — and those strings are the field help an operator reads in the grid, compiled into the shipped translation bundles. They asserted exactly the (namespace, key) attribution reading #8103's re-measurement rejected. Re-verified by producer symbol on origin/main@f93df4db: SettingsService -> settings namespace + specifier key; ref at sys_setting.value_enc engine encryptSecretFields -> object name + field name; ref is a secret:<id> on the business row datasource binder -> caller-supplied scope (default 'datasource') + datasource name; ref is the artefact's sys_secret:<id> credentialsRef Bundles regenerated with the repo's own `pnpm i18n:extract` (11 files, incl. the --source-hashes companions). The three translated locales carried translations of the superseded English, so their four affected leaves were re-translated by hand — the action <locale>.objects.generated.ts's own header prescribes when a source string changes. Text only: no field added, removed, renamed or re-typed; no validation, persistence or access rule moves. A producer/owner column stays out of scope. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W6HFzyH98W1YaQXhJUJt6o
os-warren
commented
Aug 26, 2026
PM review — accepted, flipped ready, auto-merge armedMeasured: 6 files, +87/−24, all in-scope. ⭐ The green gate was proved uninformative, and the real thing was fixed anywayThis is the part that makes the PR worth reading.
⇒ three locales stale, no two agreeing. That is the blind spot Then they did the thing the gate could not ask for: hand re-translated the four affected leaves in each of the three locales, which each bundle's own header prescribes ("Re-translate it by hand when its source changes" — merge fills gaps only, so regeneration cannot do it). I verified the re-translation is real rather than an English copy, by a route the report did not claim. All four locales changed That is the second time today this mechanism's limits have been established rather than assumed — #11671 landed it, and the first substantive change to cross it immediately found the case it cannot see. Clause ② answered as two questions, which is correctPublished surface yes — an object description and three field descriptions are metadata served to the And the key-set claim does not rest on the dev's reading of their own diff: Scope discipline⛔ No producer/owner column was added, prototyped, or argued for. The one item beyond the card's three fields — the object-level The AblationsLeg 1 (bundle freshness) predicted exactly 1 package, 1 of its 11 bundles drifted, the other 8 in sync — observed precisely that, quoting the gate's own lines. The no-rebuild justification is measured, not assumed, with direct session evidence: the extract config imports Leg 2 was deliberately not built, and argued rather than omitted: Follow-on filed#12599 —
CI is the remaining gate. Generated by Claude Code |
📓 Docs Drift CheckThis PR changes 1 package(s): 9 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 3 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 2 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 61e5e3114d0ed3d2fde8a16e8f957acd1c9b9915 && git checkout 61e5e3114d0ed3d2fde8a16e8f957acd1c9b9915
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin f93df4dbe314d5133f4a8c395b255ba0e2aeeaff adb117b1c430b6cc0e20e8c6b6893cf8a1e727b0 && git checkout -B drift-repro f93df4dbe314d5133f4a8c395b255ba0e2aeeaff && git merge --no-ff adb117b1c430b6cc0e20e8c6b6893cf8a1e727b0
node scripts/docs-audit/affected-docs.mjs --json f93df4dbe314d5133f4a8c395b255ba0e2aeeaff
|
Uh oh!
There was an error while loading. Please reload this page.
Fixes#12550
The producer table, re-verified on this branch's base
The card measured
3a3a0993; this branch is cut fromf93df4dbe3, so the table was re-derived from the producer symbols rather than from the card's line numbers. It holds, unchanged:namespacekeySettingsService—services/service-settings/src/settings-service.ts, thethis.secretStore.insert({ id, namespace, key, … })branchsys_setting.value_enc(baresec_…handle id)encryptSecretFields—packages/objectql/src/engine.ts,secretDriver.create('sys_secret', { namespace: object, key: field, … })makeSecretRef— the literal prefixsecret:followed by the handle idservices/service-datasource/src/datasource-secret-binder.ts,namespace = input.namespace ?? deps.namespace ?? 'datasource',key = input.key ?? hint.namedatasourcesys_secret:followed by the handle idSo each of the three strings was true of one producer out of three, and the pair they described was presented as if it identified an owner — the
(namespace, key)attribution reading #8103's re-measurement rejected, which is whysys-secret-orphan-report.tsreports a row it cannot attribute as'unattributable'rather than'orphaned'. These strings are not internal comments: they are the field help an operator reads in thesys_secretgrid, and they compile into the shipped translation bundles.What changed
packages/platform-objects/src/system/sys-secret.object.ts:namespace/key— producer-scoped labels whose meaning depends on which producer wrote the row, each naming all three vocabularies and pointing atmanagedBy;key's text says outright that the pair does not identify the producer.id— the handle, with the reference located in the writing producer's own holder column, and the corollary spelled out: a row unreferenced bysys_settingis not thereby unreferenced.description('Cipher store referenced by sys_setting handles.') — named here because the card lists three fields, not four. It is the same defect in the same shipped-surface class one line above them, mechanical, with its correct form already pinned by themanagedBynote; leaving it would have kept the settings-only claim in the most visible string on the table.namespace, which asserted the same thing to the next author ("Namespace/key duplicated fromsys_setting… The authoritative link issys_setting.value_enc → sys_secret.id"), replaced by the per-producer table plus the ⛔ note on attribution.⛔ A producer/owner column stays out of scope: that is a persist-path change with its own consequences and belongs to whoever takes that decision.
Regeneration, and the stale-fill measurement
Regenerated with the repo's own command —
pnpm i18n:extract(os i18n extract … --locales=zh-CN,ja-JP,es-ES --fill=default --source-hashes), which wrote all 11 files, the--source-hashescompanions included, in one run. Never by hand.The stale-fill question was measured, not guessed, in two legs:
check:i18ngreen (platform-objects in sync (11 bundle(s)));check-i18n-stale-fill: OK (10 bundle set(s) — no new stale fills, 0 baselined). Zero findings — and the three translated locales were at that moment serving translations of the superseded English.hash(value) === recorded && hash(currentSource) !== recorded, and there is no record for these leaves: they carried real translations, so at backfill timevalue !== currentSourceleft them legacy-trusted — correctly, per the ruling's property 1. The cross-locale-agreement half needs at least 2 locales byte-identical at the leaf;zh-CN/ja-JP/es-EShold three different languages. This is precisely the blind spotcheck-i18n-stale-fill.mjs's own header declares ("a leaf stranded in ONE locale … carries no evidence at all"), reached from the other side: three locales stale, no two agreeing.So the gate reporting nothing is not the drift being absent. The four affected leaves were re-translated by hand in each locale — the action each locale's own generated objects bundle prescribes in its header ("Re-translate it by hand when its source changes"; merge fills gaps only, so regenerating cannot do it) — and
pnpm i18n:extractwas then re-run so bundles and companions come from one run. The three*.source-hashes.generated.tsfiles are byte-identical tomainafterwards: no record is written for a leaf that differs fromen, which is the same fact that made leg 1 report zero.Clause ② — two answers, kept apart
Published surface: yes, it moves.
descriptionon an object and on a field is published metadata — served to the grid and compiled into the shipped bundles of@objectstack/platform-objects. The diff is 4 leaves (sys_secret.description,fields.id.help,fields.namespace.help,fields.key.help) across 4 locales.Accept/reject behaviour: no, nothing moves. Evidence from the diff rather than from intent:
Every changed line in the object file is a
description:string literal or a JSDoc comment line — noField.*option other thandescription, and no change toaccess,indexes,enable.apiMethods,highlightFields,listViews, or thefieldskey set. In the bundles the 16 changed leaf lines are 4description:and 12help:values, four removed and four added per file: no bundle key is added, removed or renamed, whichcheck:i18nconfirms independently (11 bundles in sync) andcheck:i18n-coverageconfirms from the count side (OK (12 config(s), 602 baselined untranslated string(s), none new)). Everysys_secretpayload that parsed before parses identically.Verification
Gate union derived over the real changeset with
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack(6 paths, three-dot vs merge basef93df4dbe) — 18 path-matched families plus the 2 convention-triggered i18n ones. All run atadb117b1c, each exit captured before any pipe:check:i18ncheck-i18n-bundles: OK (9 package(s) — all bundles in sync, no undeclared authoring keys).—platform-objects in sync (11 bundle(s))check:i18n-stale-fillcheck-i18n-stale-fill: OK (10 bundle set(s) — no new stale fills, 0 baselined).check:i18n-coveragecheck-i18n-coverage: OK (12 config(s), 602 baselined untranslated string(s), none new).check:nul-bytescheck-nul-bytes: OK (scanned 6950 text file(s) … no raw ASCII control bytes).check:published-files✓ 69 publishable package(s) … declare a files whitelist …check:page-declaration-shapecheck-page-declaration-shape: OK — 34 page entries across 2215 sources …check:slot-lookup✓ slot-lookup ratchet holds: 107 unswept site(s) in 25 file(s), none new …check:test-source-aliascheck-test-source-alias OK — 72 packages with tests scanned …check:type-source-resolutioncheck-type-source-resolution OK — 94 tsc program(s) across 77 packages scanned …check:cross-package-test-inputs(+ the ci.yml script)OK: 20 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob.check-ci-filter-parityOK: all 109 declared cross-package glob(s) (84 unique) are covered …check-comment-mask-adoptionOK check:comment-mask-adoption — 20 private comment-stripper(s) … all 20 recordedcheck-plugin-teardown-shape✓ 63 Plugin implementation(s) … 0 known-unreached, baseline fully burned downcheck-empty-changeset✓ No empty-frontmatter changeset introduced by this diff (1 declaring changeset(s) added).check-changeset-no-major✓ This diff introduces no major bump.check-adr-0087-registration✓ this PR adds no declared-breaking changeset (1 non-breaking changeset(s) seen).check:objectui-changeset/check:changeset-gate-self-tests/release-rehearsal-clone --self-test/docs-audit× 2✓Package:
pnpm --filter @objectstack/platform-objects exec vitest run --maxWorkers=2→Test Files 31 passed (31) · Tests 515 passed (515);typecheckexit 0. No existing assertion was changed by this PR.Ablation — bundle freshness
Direction and count predicted in writing before running. Rebuild question answered by measurement, not assumption:
packages/platform-objects/scripts/i18n-extract.config.tsimports../src/system/index.js(relative, intosrc/, viabundle-require), so extraction reads source, neverdist/— and in this session the firstpnpm i18n:extractpicked up the source edit before@objectstack/platform-objectshad ever been built on this branch. So no rebuild leg is required, and none was performed.namespacedescription to its pre-fix text; leave all four regenerated bundles as committed. Proved on disk before any result was read: injected textgrep -cF= 1, removed textgrep -cF= 0,git diff --stat= 1 file / 1 insertion / 2 deletions.check-i18n-bundles: 1 bundle problem(s)·• platform-objects: 1 bundle(s) drifted from the schema, with the other 8 packages listedin sync (4 bundle(s)). Prediction matched on direction and count.trap … EXIT INT TERM; verified with an emptygit diffand an emptygit status --short.The stale-fill gate got no hand-built ablation, deliberately:
pnpm check:i18n-stale-fillruns--self-testbefore every invocation, and it printedcheck-i18n-stale-fill: self-test OKon each run here — a mutation proving that gate can go red would restate a check that had already run twice in this session.Out of scope, filed
CryptoContext.namespace/.keyandCryptoHandle.idcarry the identical settings-only prose inpackages/spec/src/contracts/crypto-provider.ts, the contract all three producers call. Filed rather than fixed here: different package, TS doc rather than published metadata, andpackages/specbrings its own artifact-regeneration discipline.Generated by Claude Code