Skip to content

fix(platform-objects): sys_secret field help stops asserting the settings-only reading - #12600

Merged
os-warren merged 1 commit into
mainfrom
claude/issue-12550-sys-secret-tri-producer-descriptions
Aug 26, 2026
Merged

fix(platform-objects): sys_secret field help stops asserting the settings-only reading#12600
os-warren merged 1 commit into
mainfrom
claude/issue-12550-sys-secret-tri-producer-descriptions

Conversation

@os-warren

@os-warrenos-warren commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator

Fixes#12550

Editing note: three angle-bracket placeholders in the first version of this body were eaten by GitHub's body sanitizer (it strips short less-than fragments, backticks included). They are spelled out in words below rather than literally — please do not "repair" them back.

The producer table, re-verified on this branch's base

The card measured 3a3a0993; this branch is cut from f93df4dbe3, so the table was re-derived from the producer symbols rather than from the card's line numbers. It holds, unchanged:

producernamespacekeythe reference lives at
SettingsServiceservices/service-settings/src/settings-service.ts, the this.secretStore.insert({ id, namespace, key, … }) branchsettings namespacespecifier keysys_setting.value_enc (bare sec_… handle id)
engine encryptSecretFieldspackages/objectql/src/engine.ts, secretDriver.create('sys_secret', { namespace: object, key: field, … })object namefield namea ref written back onto the business row's own column by makeSecretRef — the literal prefix secret: followed by the handle id
datasource credential binder — services/service-datasource/src/datasource-secret-binder.ts, namespace = input.namespace ?? deps.namespace ?? 'datasource', key = input.key ?? hint.namecaller-supplied, default datasourcedatasource namethe datasource artefact's credentialsRef — the literal prefix sys_secret: followed by the handle id

So 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 why sys-secret-orphan-report.ts reports 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 the sys_secret grid, 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 at managedBy; 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 by sys_setting is not thereby unreferenced.
  • The object-level 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 the managedBy note; leaving it would have kept the settings-only claim in the most visible string on the table.
  • The JSDoc block above namespace, which asserted the same thing to the next author ("Namespace/key duplicated from sys_setting … The authoritative link is sys_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-hashes companions included, in one run. Never by hand.

The stale-fill question was measured, not guessed, in two legs:

  1. Source revised, bundles regenerated, translations untouched.check:i18n green (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.
  2. Why zero, from the mechanism's own two predicates. The generated-half predicate is hash(value) === recorded && hash(currentSource) !== recorded, and there is no record for these leaves: they carried real translations, so at backfill time value !== currentSource left 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-ES hold three different languages. This is precisely the blind spot check-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:extract was then re-run so bundles and companions come from one run. The three *.source-hashes.generated.ts files are byte-identical to main afterwards: no record is written for a leaf that differs from en, which is the same fact that made leg 1 report zero.

Clause ② — two answers, kept apart

Published surface: yes, it moves.description on 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:

 .changeset/sys-secret-tri-producer-field-help.md | 42 ++++++++++++++++++++++
.../src/apps/translations/en.objects.generated.ts | 8 ++---
.../apps/translations/es-ES.objects.generated.ts | 8 ++---
.../apps/translations/ja-JP.objects.generated.ts | 8 ++---
.../apps/translations/zh-CN.objects.generated.ts | 8 ++---
.../src/system/sys-secret.object.ts | 37 ++++++++++++++-----
6 files changed, 87 insertions(+), 24 deletions(-)

Every changed line in the object file is a description: string literal or a JSDoc comment line — no Field.* option other than description, and no change to access, indexes, enable.apiMethods, highlightFields, listViews, or the fields key set. In the bundles the 16 changed leaf lines are 4 description: and 12 help: values, four removed and four added per file: no bundle key is added, removed or renamed, which check:i18n confirms independently (11 bundles in sync) and check:i18n-coverage confirms from the count side (OK (12 config(s), 602 baselined untranslated string(s), none new)). Every sys_secret payload 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 base f93df4dbe) — 18 path-matched families plus the 2 convention-triggered i18n ones. All run at adb117b1c, each exit captured before any pipe:

gateits own verdict line
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 recorded
check-plugin-teardown-shape✓ 63 Plugin implementation(s) … 0 known-unreached, baseline fully burned down
check-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 × 2all

Package: pnpm --filter @objectstack/platform-objects exec vitest run --maxWorkers=2Test Files 31 passed (31) · Tests 515 passed (515); typecheck exit 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.ts imports ../src/system/index.js (relative, into src/, via bundle-require), so extraction reads source, never dist/ — and in this session the first pnpm i18n:extract picked up the source edit before @objectstack/platform-objects had ever been built on this branch. So no rebuild leg is required, and none was performed.

  • Mutation — revert only the namespace description to its pre-fix text; leave all four regenerated bundles as committed. Proved on disk before any result was read: injected text grep -cF = 1, removed text grep -cF = 0, git diff --stat = 1 file / 1 insertion / 2 deletions.
  • Predicted — red; exactly 1 package (platform-objects) with exactly 1 of its 11 bundles drifted; the other 8 packages in sync; translated locales and the 3 source-hash companions unaffected; exit non-zero.
  • Observed — exit 1. check-i18n-bundles: 1 bundle problem(s) · • platform-objects: 1 bundle(s) drifted from the schema, with the other 8 packages listed in sync (4 bundle(s)). Prediction matched on direction and count.
  • Restore — under trap … EXIT INT TERM; verified with an empty git diff and an empty git status --short.

The stale-fill gate got no hand-built ablation, deliberately: pnpm check:i18n-stale-fill runs --self-test before every invocation, and it printed check-i18n-stale-fill: self-test OK on 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


Generated by Claude Code

…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
os-warren marked this pull request as ready for review August 26, 2026 16:19
@os-warren
os-warren enabled auto-merge August 26, 2026 16:19
@os-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

PM review — accepted, flipped ready, auto-merge armed

Measured: 6 files, +87/−24, all in-scope.

⭐ The green gate was proved uninformative, and the real thing was fixed anyway

This is the part that makes the PR worth reading. check:i18n-stale-fill reported OK, 0 findings — while zh-CN / ja-JP / es-ES were, at that moment, serving translations of superseded English. The dev did not take the green as an answer; they derived from the gate's own two predicates why it structurally cannot see this case:

  • the generated-half predicate needs a record, and these leaves carry real translations, so at backfill time value !== currentSource left them legacy-trusted — correctly, per the ruling's own property 1;
  • the cross-locale half needs two locales byte-identical at the leaf, and the three locales hold three different languages.

three locales stale, no two agreeing. That is the blind spot check-i18n-stale-fill.mjs's own header declares — "a leaf stranded in ONE locale carries no evidence at all" — reached from the opposite side: not one stranded, but all three moved together.

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 +4/−4, and no source-hashes.generated.ts companion is touched. That is the tell: a record is written only for a leaf that is a byte copy of the current source, so if the three locales had been filled with English, the companions would have gained records. They did not. The translations are genuinely different from en.

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 correct

Published surface yes — an object description and three field descriptions are metadata served to the sys_secret grid as field help and compiled into shipped bundles. Accept/reject no — no field added, removed, renamed or re-typed; no schema, validation, access rule, index, apiMethods or persist path touched.

And the key-set claim does not rest on the dev's reading of their own diff: check:i18n ("platform-objects in sync (11 bundle(s))") and check:i18n-coverage ("602 baselined untranslated string(s), none new") confirm it independently. Note that 11 — the count #11671 raised from 8 this morning, doing its job on the first change to pass through.

Scope discipline

⛔ No producer/owner column was added, prototyped, or argued for. The one item beyond the card's three fields — the object-leveldescription, same defect one line above them — is declared in the PR body rather than slipped in, with its correct form already pinned by the file's own managedBy note. That is the right way to extend a card by one line.

The namespace JSDoc block was replaced too, and the reason matters: it told the next author the same untruth the field help told the operator. Fixing the help and leaving the docblock would have left the defect's source in place.

Ablations

Leg 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 ../src/system/index.js and the first run rewrote en.objects.generated.tsbefore the package had ever been built on that branch.

Leg 2 was deliberately not built, and argued rather than omitted: check:i18n-stale-fill runs its own --self-test before every invocation and printed self-test OK on each run, so a hand-built mutation would restate a check that had already run twice — and would say nothing about this change, whose relationship to the gate is the zero-finding measurement above. Declining an ablation with a reason is a legitimate answer; declining one silently is not.

Follow-on filed

#12599CryptoContext.namespace / .key and CryptoHandle.id in packages/spec carry the identical settings-only prose, one layer below the three producers that all construct a CryptoContext. Correctly not fixed here: different package, TS doc comments rather than published metadata, and packages/spec brings its own artifact-regeneration gate family — so the bounded in-place exemption's "same gate family, no new verification surface" condition fails. That is the exemption being applied as a test rather than as a licence.

⚠️ Wrapper caveat observed again on this card: os-verify-lock.sh printed VERDICT command-exit 0 while an inner command exited 1. Fourth today (#12365). Every verdict here comes from the per-command exit line.

CI is the remaining gate.


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/platform-objects, touching 2 documentable anchor(s).

9 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/automation/webhooks.mdx(via sys_secret (symbol))
  • content/docs/data-modeling/drivers.mdx(via sys_secret (symbol))
  • content/docs/data-modeling/external-datasources.mdx(via sys_secret (symbol))
  • content/docs/data-modeling/objects.mdx(via sys_secret (symbol))
  • content/docs/data-modeling/validation-rules.mdx(via sys_secret (symbol))
  • content/docs/deployment/backup-restore.mdx(via sys_secret (symbol))
  • content/docs/deployment/environment-variables.mdx(via sys_secret (symbol))
  • content/docs/permissions/authorization.mdx(via sys_secret (symbol))
  • content/docs/protocol/kernel/config-resolution.mdx(via sys_secret (symbol))

3 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/implementation-status.mdx(via sys_secret (symbol))
  • content/docs/releases/v16.mdx(via sys_secret (symbol))
  • content/docs/releases/v17.mdx(via sys_secret (symbol))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 2 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json f93df4dbe314d5133f4a8c395b255ba0e2aeeaffpackageMentionDocs.

Which tree this was computed on

This run read content/docs from 61e5e3114d0ed3d2fde8a16e8f957acd1c9b9915 — the merge of head adb117b1c430b6cc0e20e8c6b6893cf8a1e727b0 into base f93df4dbe314d5133f4a8c395b255ba0e2aeeaff, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs f93df4dbe314d5133f4a8c395b255ba0e2aeeaff → pass the list as
args.docs, on the commit named under Which tree this was computed on.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mtooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

sys_secret.namespace / .key still describe themselves as settings-only, on a table with three producers

2 participants

@os-warren@claude