Skip to content

fix(platform-objects): re-translate the object leaves that contradict their current en source - #20652

Merged
objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/issue-20539-stale-authored-leaves
Sep 29, 2026
Merged

objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/issue-20539-stale-authored-leaves

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #20539

Clause-②: no

What changed

A translated leaf in packages/platform-objects/src/apps/translations/{ja-JP,es-ES,zh-CN}.objects.generated.ts that a translator wrote by hand keeps its value when its en source changes later. Some of those leaves still said what the old source said. This PR measures that set first, then re-translates the leaves whose meaning now contradicts the current en: 19 leaves (ja-JP 4, es-ES 11, zh-CN 4). The leaves whose en only gained detail or was reworded are listed below and left as written.

  • Values only. No key is added or dropped, no en file is edited, and no provenance companion or echo-decision ledger changes (measured below; the tooling requires none).
  • .changeset/20539-stale-authored-object-leaves.md: @objectstack/platform-objects patch.
  • No new gate, per triage. No test pins a translation string.

The measurement (taken on this branch at base cd901d7a5, before any edit)

Instrument

A diff of en since each authored leaf's last edit, as the card names it, run over git history:

  1. Population. Every string leaf of each locale's objects bundle at the ref, minus two classes that are not authored: a leaf byte-equal to the current en leaf (an echo, owned by the echo ledgers), and a leaf with a row in the locale's *.source-hashes.generated.ts (a recorded byte copy, owned by check:i18n-stale-fill). What is left is the authored set.
  2. Last edit. Walk every first-parent main commit that touched the locale bundle, oldest first, parse the object literal at each, and record for each path the commit where its value last changed. Values are compared parsed, so a reformat is not an edit. For a leaf that landed through a merge commit, en is also read at the branch side.
  3. Verdict. Compare en's value for the same path at that commit with en today. If they differ, en moved after the leaf was last edited, and the leaf is a candidate. Meaning is then judged by hand for each candidate.

It reads the en bundle history rather than the object source files. Today en is the source, byte for byte (pnpm check:i18n green). Before 8af76aebf the en bundle could lag its source, and that lag only ever adds candidates; it never hides one. The bundle diff is therefore at least as sensitive as a source diff, and it survives object files moving between packages. Full history is required: the checkout was shallow (499 commits) and was unshallowed to 15092 before the run.

The same logic, condensed, reproduces the full instrument's candidate sets exactly at cd901d7a5 and 3fa342501:

// node instrument.mjs REF  (run from the repo root; needs full, unshallowed history)
import { execFileSync as x } from 'node:child_process';
const REF = process.argv[2] || 'HEAD', D = 'packages/platform-objects/src/apps/translations/';
const git = (...a) => x('git', a, { encoding: 'utf8', maxBuffer: 268435456 });
const leaves = (o, pre = '', m = new Map()) => { for (const [k, v] of Object.entries(o)) { const p = pre ? pre + '.' + k : k; if (typeof v === 'string') m.set(p, v); else if (v && typeof v === 'object') leaves(v, p, m); } return m; };
const at = (rev, f) => { const s = git('show', rev + ':' + D + f); return leaves(new Function('return (' + s.slice(s.indexOf('= {', s.indexOf('export const')) + 2, s.lastIndexOf('};') + 1) + ');')()); };
const enNow = at(REF, 'en.objects.generated.ts');
for (const L of ['ja-JP', 'es-ES', 'zh-CN']) {
  const f = L + '.objects.generated.ts', last = new Map();
  let prev = new Map();
  for (const c of git('log', '--first-parent', '--format=%H', REF, '--', D + f).trim().split('\n').reverse()) {
    const cur = at(c, f);
    for (const [p, v] of cur) if (prev.get(p) !== v) last.set(p, c);
    prev = cur;
  }
  const recorded = git('show', REF + ':' + D + L + '.source-hashes.generated.ts');
  let authored = 0; const cands = [];
  for (const [p, v] of at(REF, f)) {
    if (v === enNow.get(p) || recorded.includes('"objects.' + p + '"')) continue; // echo / recorded byte copy
    authored++;
    const c = last.get(p), enThen = at(c, 'en.objects.generated.ts').get(p);
    if (enThen !== enNow.get(p)) cands.push(p + ' @' + c.slice(0, 9));
  }
  console.log(L, 'authored=' + authored, 'candidates=' + cands.length);
  for (const k of cands) console.log('  ' + k);
}

Two widening checks ran beside it:

  • Penultimate edit. For each non-candidate, the full instrument also compared en at the leaf's previous edit. This catches a sweep that touched a leaf mechanically after en had moved. It flagged 12 / 8 / 12 leaves (ja-JP / es-ES / zh-CN). All of them were re-translated in the same commit that moved en, so none is stale.
  • Retired-term scan. The since-last-edit instrument cannot see a leaf that was already wrong at its last edit. For the two renames the candidates surfaced (ADR-0057 department to business unit, and v5.0 project to environment), every leaf that carries the locale's word for the retired term (部門 / 部门 / departamento case-insensitive; プロジェクト / 项目 / proyecto) was compared with the en leaf at the same path. A leaf is flagged when en does not carry the English term there.

Known-positive control

sys_two_factor.fields.backup_codes.help, line 1095 of all four bundles. en has read "Backup recovery codes, encrypted at rest (a single opaque ciphertext, not readable JSON)" since 266654d82. The three translations were last edited at 7bb92056e, when en read "JSON-serialized backup recovery codes". The instrument flags it in all three locales, zh-CN included. The retired-term scan's positive is sys_metadata.fields.environment_id.label, which both instruments flag.

Population and radius

  • Covered: the objects bundles of @objectstack/platform-objects for ja-JP, es-ES and zh-CN. There are 1529 leaves per locale. Echoes of the current en number 43 / 54 / 42, recorded byte copies 0 beyond those, and authored leaves 1486 / 1475 / 1487. History covered: every first-parent commit since the bundles were created at 6bacbced2 (62 / 62 / 63 commits touching each locale file).
  • Not covered: a leaf that was wrong at its last edit for any reason other than the two renames above; the platform-objects metadata-forms bundles; the other eight packages' bundle sets. Raw candidate counts for those are in the acceptance notes.

Counts per locale

ja-JP es-ES zh-CN
authored leaves 1486 1475 1487
candidates (en moved since last edit), before 17 21 17
of which contradicting, before 4 9 4
retired-term scan, wrong at authoring, before 0 2 0
re-translated here 4 11 4
candidates after (at 3fa342501) 13 12 13
contradicting after 0 0 0
retired-term scan after 0 0 0

The after-candidate sets are exactly the before sets minus the re-translated leaves: 0 added, and none of the new values equals its en leaf. The 13 / 12 / 13 that remain are the list below.

The judgment rule

A leaf contradicts the current en when a reader who acts on it would believe something about the current field or object that en now says is false. Three cases:

  • en now denies what the leaf asserts;
  • the object was re-modelled, so the leaf describes a different thing;
  • the leaf names the record's entity by a term that a rename retired from it.

A leaf does not contradict when en only added detail, narrowed or reworded without retracting what the leaf says, or changed punctuation.

Re-translated (19)

path locales why it contradicted
sys_two_factor.fields.backup_codes.help ja-JP, es-ES, zh-CN Said JSON-serialized, where en says "a single opaque ciphertext, not readable JSON" (266654d82).
sys_notification.description ja-JP, es-ES, zh-CN Said "per-user notification inbox entries". ADR-0030 re-modelled the object as one event row per emit(), carrying no recipient; the object header says it "was previously a per-user inbox".
sys_job_run.description ja-JP, es-ES, zh-CN Said "audit trail". ca19ee81f changed the source to "history" because the object is job-run history and not the audit trail, which is sys_audit_log (the binding ruling that commit cites).
sys_metadata.fields.environment_id.label ja-JP, es-ES, zh-CN Said "Project (deprecated)" for the environment_id column. The v5.0 rename project to environment has no aliases.
sys_business_unit.fields.manager_user_id.help, .effective_from.help, .effective_to.help, sys_business_unit_member.description, sys_business_unit_member.fields.is_primary.help es-ES Named the business unit a departamento, where en says business unit (ADR-0057 D2; department is now one kind of business unit). 61ed5c7c9 renamed these in en, ja-JP and zh-CN and missed es-ES.
sys_business_unit.fields.manager_user_id.label, sys_business_unit.fields.id.label es-ES Same rename: "Responsable del departamento" and "ID de departamento" against en "Business Unit Head" and "Business Unit ID". These were wrong when authored (e16f2a830), so the since-last-edit instrument cannot see them; the retired-term scan found them.

New values: ja-JP 保存時に暗号化されたバックアップ回復コード(単一の不透明な暗号文であり、読み取り可能な JSON ではありません) · 通知イベント — emit() 1 回につき 1 行(ADR-0030 レイヤー 2 の入口) · バックグラウンドジョブの実行履歴 · 環境(非推奨). zh-CN 备用恢复码,静态存储时加密(单个不透明密文,而非可读的 JSON) · 通知事件——每次 emit() 产生一行(ADR-0030 第 2 层入口) · 后台任务执行历史 · 环境(已废弃). es-ES uses unidad de negocio, the word the bundle already uses for the object and its parent_business_unit_id / business_unit_id labels; the rest is in the diff.

Stale but not contradicting (listed, left as written)

path locales what en did
sys_team.fields.organization_id.help all three Added "Null in single-tenant; auto-stamped in multi-tenant."
sys_business_unit.fields.organization_id.help all three Added the same sentence.
sys_business_unit.description all three Reworded and extended (canonical BU tree, data-partition role, kind examples). "Hierarchical org tree node" is still true.
sys_business_unit_member.fields.is_primary.help ja-JP, zh-CN Nothing to fix: the leaf already says business units. It was translated ahead of en at 61ed5c7c9.
sys_oauth_application.fields.client_secret.help all three Added the enforcement citation and "shown once". The leaf's "hashed at rest" is still true.
sys_notification.label / .pluralLabel all three Narrowed "Notification" to "Notification Event". The less specific word is not false, and nothing else in these bundles carries it: sys_inbox_message is "Inbox Message", in its own package.
sys_notification.fields.actor_id.help all three Reworded "the notification" to "the event".
sys_email.fields.headers_json.help all three Added "Never returned on the generic data path…". The leaf does not claim that it is.
sys_email.fields.error.help all three Widened from transport errors to transport or pre-delivery validation errors. The leaf is narrower but not contradicted. This is the closest call.
sys_metadata_history.description all three Reframed as a durable event log of overlay changes. "Version history" is still true, and the source never retracted the audit wording: 59261af32 calls the row the audit row.
sys_metadata_history.fields.metadata.help all three Added "(null for deletes)".
sys_metadata_history.fields.change_note.help all three Added a trailing period.

sys_notification.description, the second example the card names, was measured stale and contradicting in all three locales. It is re-translated above.

Dispatch hypotheses, measured

  • H1 held. The control is live at f1e921ab8e and at base cd901d7a5, at line 1095 of all four files, and zh-CN is in the set.
  • H2 falsified. Neither companion records an authored object leaf. *.source-hashes.generated.ts records only byte copies: 43 / 54 / 42 rows, each an echo of the current en. *.source-hashes.ts is the hand table for apps / dashboards / pages only: 130 rows each, 0 under objects. So "recorded hash differs" names nothing here, and the instrument had to be git history.
  • H3 held: values only. Measured that no companion changes. collectFilledFromHashes records a leaf only when its value equals the current source or a recorded digest. At 3fa342501, node scripts/check-i18n-bundles.mjs --write --filter=platform-objects rewrote the bundle set byte-identical (git status empty). Control: with ja-JP sys_job_run.description set to the en string (via scripts/ablation-replace.mjs), pnpm check:i18n goes red with platform-objects DRIFTED (1). Restored after: blob equals HEAD, and git diff HEAD is empty.
  • H4 falsified for this set. No echo-decision ledger row, and no test file under packages/platform-objects, names any of the 19 paths (git grep exit 1). The old values appear nowhere outside the bundles (exit 1). Both controls hit: a path pinned in the es-ES ledger, and Cc in the ja-JP ledger. No pin moved.

Verification (all at 3fa342501)

  • node scripts/pm/dispatch-gates.mjs --commands (no paths) derived 55 commands, and all 55 exit 0. --ran printed "55 derived famil(ies) accounted for — 55 run, 0 NOT-MEASURED". pnpm check:dual-build-cjs-loads first refused with exit 3 (nine unrelated packages had no dist/). Those were built and the gate re-ran: exit 0.
  • Named by the dispatch: pnpm check:i18n ("OK (9 package(s) — all bundles in sync…)"), pnpm check:i18n-stale-fill ("OK (10 bundle set(s) — no new stale fills, 0 baselined)"), pnpm check:nul-bytes, pnpm check:published-files and pnpm check:cross-package-test-inputs all exit 0.
  • pnpm --filter @objectstack/platform-objects test: 58 files, 942 tests passed. pnpm --filter @objectstack/platform-objects typecheck: exit 0.
  • Lint, narrowed to the three edited bundles: eslint --no-inline-config --format json read 3 files, 0 errors, 0 warnings. ESLint#isPathIgnored answers false for all three, read from the repo's own config. The config never enables type-aware linting (no parserOptions.project), so a change to string values in these files cannot move the verdict on any other file. The full pnpm lint run is left to CI.

Acceptance notes

  • Beyond the radius (raw instrument counts at cd901d7a5; meaning judged only where noted):

    • platform-objects metadata-forms: 9 / 9 / 6;
    • plugin-approvals: 17 / 17 / 16. All of these are en options moving from the machine value to a title (pending to Pending), with no change of meaning;
    • plugin-audit: 3 / 3 / 3;
    • plugin-security: 7 / 5 / 7;
    • plugin-webhooks: 3 / 3 / 3;
    • plugin-sharing and the three services: 0.

    Read by hand and reported to the seat as one family finding: plugin-webhooks sys_webhook.fields.definition_json.help in all three locales says the JSON carries the full headers and auth config, where en says credentials are NOT stored there. plugin-audit sys_activity.fields.environment_id.label still says Project. The metadata-forms permission section still promises custom context variables, and the agent section still promises tools; en dropped both.

  • bu-rename-consistency.test.ts spells its es-ES term as the case-sensitive /Departamento/. That is why the two lowercase es-ES labels re-translated here (field labels, which the test's header says it asserts) passed it. This PR fixes the values. The test's blind spot remains, and a case-insensitive /departamento/i would close it. That edit is outside this card's file surface and is not made here.

  • The history walk needed full history. The shared object store was unshallowed (git fetch --unshallow origin main), which every worktree on this clone now sees.


Generated by Claude Code

… their current en source

Measured first: for every authored leaf of the ja-JP, es-ES and zh-CN
objects bundles, the en value at the first-parent commit where the leaf
last changed was compared with en today. 17 / 21 / 17 candidates; the
ones whose meaning now contradicts en are re-translated, the rest
(en only added detail or reworded) are left as written.

- all three: sys_two_factor.backup_codes help, sys_notification
  description, sys_job_run description, sys_metadata.environment_id label
- es-ES: the seven sys_business_unit / sys_business_unit_member leaves
  that still named the business unit a department (five from the
  since-last-edit instrument, two labels that were wrong at authoring,
  found by a retired-term scan)

Values only; no key added or dropped.

Claude-Session: https://claude.ai/code/session_01DEvba2nBuD4tWzfq8r8NFY
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/s documentation Improvements or additions to documentation tooling labels Sep 29, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

25 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json c6b37cd08d0eb622d4f59b79effdc954f86dad9f.

⛔ 9 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 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.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 3 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 c6b37cd08d0eb622d4f59b79effdc954f86dad9f → packageMentionDocs.

Which tree this was computed on

This run read content/docs from ee85ff77d35feb3a6577fb640d7c0a64ea80eeb3 — the merge of head 3fa342501bc9fb46dfa19c738451027fbb820b61 into base c6b37cd08d0eb622d4f59b79effdc954f86dad9f, 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 ee85ff77d35feb3a6577fb640d7c0a64ea80eeb3 && git checkout ee85ff77d35feb3a6577fb640d7c0a64ea80eeb3
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin c6b37cd08d0eb622d4f59b79effdc954f86dad9f 3fa342501bc9fb46dfa19c738451027fbb820b61 && git checkout -B drift-repro c6b37cd08d0eb622d4f59b79effdc954f86dad9f && git merge --no-ff 3fa342501bc9fb46dfa19c738451027fbb820b61

node scripts/docs-audit/affected-docs.mjs --json c6b37cd08d0eb622d4f59b79effdc954f86dad9f

⚠️ 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 c6b37cd08d0eb622d4f59b79effdc954f86dad9f → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 3fa342501bc9fb46dfa19c738451027fbb820b61
Local-runs: none

Inputs: card #20539 (body and its 3 comments: triage 5882208447, Claim 5890841958, os-dev-report 5891725468); PR #20652 body and file list (4 files, +46 / -19); the net diff of the branch against origin/main (merge-base cd901d7a5, origin/main at c6b37cd08 when read); the check-runs on the head. Read-only: nothing built, run or re-run; the en values below are git show origin/main: reads.

Check-runs on the head at 2026-09-29T14:02Z: 32 runs — 18 success, 3 skipped (Console Pin Gate, Build Docs, Packed-tarball smoke), 11 in_progress (Lint and Repo Gates, Test Core 1-6 of 6, Type Check workspace, Type Check consumer gates, Temporal Conformance, the Dogfood Regression Gate aggregate), 0 failure. Recorded as read, not polled.

① Derived judgments

  1. Public surface: string VALUES only, inside the three exported translation constants (jaJPObjects, esESObjects, zhCNObjects) of @objectstack/platform-objects. No key added or dropped, no type change, no en edit, no *.source-hashes* companion or echo-decision ledger change, no test change. Accept-set: unchanged — nothing an author can write moves. Right.
  2. Each of the 19 re-translated values, judged against the CURRENT en value of the same key on origin/main:
    • sys_two_factor.fields.backup_codes.help (ja-JP, es-ES, zh-CN): en reads "Backup recovery codes, encrypted at rest (a single opaque ciphertext, not readable JSON)". All three now say exactly that — encrypted at rest, a single opaque ciphertext, not readable JSON. Right.
    • sys_notification.description (all three): en reads "Notification events — one row per emit() (ADR-0030 Layer 2 ingress)". All three say that. Right.
    • sys_job_run.description (all three): en reads "Background job execution history" (ca19ee81f replaced "audit trail" with "history", verified in that commit's diff). All three now say history, none says audit. Right.
    • sys_metadata.fields.environment_id.label (all three): en reads "Environment (deprecated)". All three now say Environment (deprecated); each locale's sibling help already said deprecated. Right.
    • es-ES sys_business_unit.fields.manager_user_id.label and .help, .effective_from.help, .effective_to.help, .id.label, sys_business_unit_member.description, sys_business_unit_member.fields.is_primary.help (7): en says business unit in each; es-ES now says "unidad de negocio", the term the bundle already uses for the object label and its business_unit_id labels. Right.
      Nothing is added beyond what en says, and nothing contradicting is left in the same objects.
  3. Retired-term sweep of the three HEAD bundles (departamento and proyecto case-insensitive; 部門, プロジェクト; 部门, 项目) against en at the same path: the only carriers are sys_business_unit.description (kind examples) and sys_business_unit.fields.kind.options.department, and en carries "department" at both. 0 mismatches — the dev's "0 after" count holds.
  4. The 13 / 12 / 13 candidates left as written: each read against en. All are cases where en added detail or reworded (the two organization_id helps, client_secret, headers_json, sys_metadata_history.*, actor_id, sys_notification.label / .pluralLabel). None asserts what en now denies. The two closest calls: sys_email.fields.error.help (the locales are narrower — transport error only — where en adds pre-delivery validation) and sys_metadata_history.description (locales still say audit trail; unlike sys_job_run, no source commit retired that word — 59261af32 keeps the audit framing). Both are stale-not-contradicting under the card's ruling. Right; a judgment call, recorded.
  5. No companion or ledger change: consistent with the tooling as the dev measured it — the source-hashes companions record byte copies of en only, and a hand translation that differs from en is not a recorded fill. The head's own check:i18n and check:i18n-stale-fill verdicts belong to Lint and Repo Gates, in_progress at read.
  6. The dev's acceptance note on bu-rename-consistency.test.ts: verified on origin/main line 22 — es-ES is spelled as the case-sensitive /Departamento/, so the two lowercase labels passed a test whose header says it asserts every field label. True.
  7. The source commits the PR body cites (ca19ee81f, 266654d82, 61ed5c7c9, e16f2a830, 7bb92056e, 59261af32) exist in history and their subjects say what the PR body says of them.

② Semver level

.changeset/20539-stale-authored-object-leaves.md declares @objectstack/platform-objects patch. The package publishes (no private, publishConfig.access: public, currently 17.5.0), and a bug fix in a released package takes patch — right; not skip-changeset, and no minor is owed because nothing an author can write changes. Clause-②: no, in the PR body and in the changeset body, with no arm — right for a values-only translation diff. Check Changeset is success on the head.

Each factual sentence of the changeset: (a) an authored leaf is kept as written when its en source moves — true (the bundle header, quoted on the card). (b) on a ja-JP, es-ES or zh-CN console the sys_two_factor record page described backup_codes as JSON-serialized where en says one opaque ciphertext, not readable JSON — true (the old values in the diff; en on origin/main). (c) nineteen leaves — 4 + 11 + 4 = 19 value changes in the diff, true. (d) the four keys re-translated in all three locales and what each now says — true. (e) "es-ES only: seven sys_business_unit / sys_business_unit_member labels and help texts" — the count and the substance are true; the kind is imprecise: 2 labels, 4 help texts and 1 description (sys_business_unit_member.description). A nit — the headline line already names descriptions — not a blocker. (f) leaves whose en only gained detail or was reworded are unchanged — true. (g) values only, no key added or removed, no provenance table changes — true on the file list. The title's (#20539) follows the package's own CHANGELOG convention.

③ Boundary flags

open_questions: none.

  1. Two es-ES labels found by a retired-term scan outside the card's named instrument — ANSWERED, in scope. The ruling is "re-translate the leaves whose meaning now contradicts en"; the since-last-edit diff was the suggested instrument, not the boundary of the set. Both labels contradict en today, sit in the same file, are values only, and are inside the Claim's file surface. Accepted.
  2. Stale-but-not-contradicting leaves left as written — ANSWERED, this is what the ruling asks; the 13 / 12 / 13 are listed in the PR body with what en did. Accepted (①.4).
  3. No companion or ledger changed — ANSWERED. The Claim allowed those files only where the regenerator requires it, and the dev's measurement says it requires nothing here. Accepted; the at-head gate family is in Lint and Repo Gates, in_progress at read.
  4. Shared clone unshallowed — ANSWERED, a local-environment effect on the dev's clone with no file in the diff. Noted; no repo effect.
  5. Out-of-scope finding a (plugin-webhooks sys_webhook.fields.definition_json.help contradicting en on a security field in all three locales; plugin-audit sys_activity.fields.environment_id.label still saying Project; the metadata-forms permission and agent section descriptions promising what en dropped) — ESCALATED to the seat: file the one family card the dev proposed. Not for this PR; outside its file surface.
  6. Out-of-scope finding b (bu-rename-consistency.test.ts case-sensitive es-ES regex, "carrier: none") — ESCALATED to the seat as a decision: the guard gap is dormant now that the values are fixed; a one-token case-insensitive spelling would close it. Fold into the family card or leave it. Not a blocker for this head.
  7. The other listed deviations (a check:dual-build-cjs-loads re-run after building nine packages, the model-free commit trailer, measurement scripts kept in the scratchpad) are process notes with nothing in the diff.

Governed surface: none touched — the file list is three translation bundles and one changeset; Governed Surface Queue Guard is success.

Implemented-by: claude/issue-20539-stale-authored-leaves
Reviewed-by: session_01DEvba2nBuD4tWzfq8r8NFY

VERDICT: PASS

Landing note for the owning seat: eleven check-run families were still in_progress when this record was read; landing waits on their green, per the Post-Task Checklist.


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review September 29, 2026 14:09
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 29, 2026
Merged via the queue into main with commit f4ce10c Sep 29, 2026
36 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-20539-stale-authored-leaves branch September 29, 2026 14:31
veigajoao pushed a commit to veigajoao/objectstack that referenced this pull request Sep 29, 2026
…o the commits that decided them, and the source-hashes header at its producer (objectstack-ai#20656)

Part of objectstack-ai#20594
Clause-②: no

## What changed

This is stage 3 of the `domain:cli` lane of the dead-citation sweep:
`packages/cli/src/**`, plus the generated-header producer the
`domain:services` pointer on the card hands this lane. Every comment or
docblock site in scope that cited a tracker number answering 404 now
cites, in ruling C+D's form C (comment 5749154545 on objectstack-ai#19123), the commit
in this repository's history that decided what the line describes, and
says in its own words what that commit decided. PR objectstack-ai#20533 is the method;
PR objectstack-ai#20624 (stage 1, `packages/runtime`) and PR objectstack-ai#20632 (stage 2,
`packages/rest`) are the precedents this follows. Later stages cover
`types` and the rest of the lane, so this PR says `Part of` and the card
stays open.

That is **313 comment sites on 304 lines in 64 files, covering 63
numbers**: the census's 170 rewritable sites (of its 174), 142 more in
test comments (which the census defers), and one site whose dead number
is the second half of a slash-joined pair the citation grammar does not
read (`serve.ts:1173`, `objectstack-ai#10943/objectstack-ai#11157`). Each rewritten line cites one
of **62 distinct commits**, except the six `objectstack-ai#15041` sites, which cite
ADR-0104's 2026-09-05 addendum: that ADR records the maintainer ruling
the lines describe, and the ruling allows the ADR to be cited instead of
a commit.

Only comments changed in `packages/cli/src`, apart from the two string
literals this stage declares (next section). Every touched file keeps
its line count (319 lines out, 319 in, over 65 files), so no line
citation into these files moves. Thirteen of the 319 lines held no dead
site; each is the other half of a sentence that had to change:
`create.ts:326`, `doctor-organizations-message-spelling.test.ts:11`,
`environments.test.ts:162`, `generate.ts:153`,
`serve-cluster-host-resolution.test.ts:816`,
`serve-host-fallback-base.test.ts:5`, `validate.ts:336`,
`validate.ts:813`, `validate.ts:815`, `hook-body-lowering.test.ts:10`,
`format.ts:1132`, `format.ts:1435` and `i18n-extract.ts:34` (mostly
「that card」 to 「that commit」 once the antecedent became a commit).

**No citation number is added.** Every tracker number on an added line
was already on the line it replaces. One PR number stands on an added
line, and it was there before:
`doctor-organizations-message-spelling.test.ts:9` read 「PR objectstack-ai#12463
(objectstack-ai#12151) single-sourced」 and now reads 「Commit 27b6902 (PR objectstack-ai#12463)
single-sourced」, keeping the live PR as a convenience link beside the
commit, as the ruling allows (objectstack-ai#12463 is that commit's own PR). No code
token moves (see the guard below).

Four dead comment sites are left on purpose, listed under "The sites
left". Two more files outside `packages/cli`: a `patch` changeset for
`@objectstack/cli`, and the shrink-only `check:doc-authoring` prose-id
ledger (see Deviations).

## The source-hashes producer and its 27 generated companions

The claim declares these as the only strings this stage moves, and the
regeneration of the files they write.

- **The producer.** `packages/cli/src/utils/i18n-extract.ts:2294` is the
string literal `renderSourceHashModule` writes as line 8 of every
`LOCALE.source-hashes.generated.ts`. It read 「bundles (objectstack-ai#11671,
maintainer ruling objectstack-ai#12069 Option A, extending objectstack-ai#8765 Option B).」 and now
reads 「bundles (commit 09b4f4e, maintainer ruling objectstack-ai#12069 Option A,
extending objectstack-ai#8765 Option B).」. `09b4f4e4e` is the commit that extended the
objectstack-ai#8765 Option B source-hash mechanism to the generated bundles per
maintainer ruling objectstack-ai#12069 Option A; its message says exactly that, and it
is the anchor stages 1 and 2 of objectstack-ai#20596 gave `objectstack-ai#11671`. objectstack-ai#12069 and objectstack-ai#8765
answer 200 (REST and web) and stay. The comment at `:249` beside
`previousSourceHashes` cites the same commit.
- **The flag's help text**
(`packages/cli/src/commands/i18n/extract.ts:227`, author-shown CLI
help), in form D: the lesson in words, no number. It read 「the
provenance companion that lets a stale fill be told from a translation
(objectstack-ai#11671).」 and now reads 「the provenance companion that records which
source revision each generated leaf is still a copy of, so a stale fill
can be told from a translation.」. The rest of the description is
unchanged.
- **The regeneration.** `node scripts/check-i18n-bundles.mjs --write`,
which runs each package's documented `os i18n extract` command from its
`i18n-extract.config.ts` (every one of the nine carries the
source-hashes flag), on a CLI built from `47241dd80e`. Before it, the
bundle check reported the nine packages DRIFTED, 3 bundles each, against
the new producer. After it:
- exactly 27 files changed, `+27 −27`: 3 locales in
`packages/platform-objects/src/apps/translations`,
`plugins/plugin-{approvals,audit,security,sharing,webhooks}` and
`services/service-{messaging,realtime,storage}`;
- every hunk is `@@ -8 +8 @@`, and the one removed and one added line
are the same in all 27 files;
- each file with line 8 deleted hashes identically at base and head (27
of 27), so every hash entry is byte-identical;
- `git status` shows nothing else in the nine packages, tracked or
untracked;
- `node scripts/check-i18n-bundles.mjs` then reads all nine packages in
sync (7 bundles each), and `check:i18n-stale-fill` serves 27 of 27
companions with 0 stale fills.
- **H3 holds.** `git grep -n "objectstack-ai#11671" -- '*.source-hashes.generated.ts'`
answers 0 hits at head; the same grep at `04b202e5cb` answers 27 (the
positive control).
- **Not published by the nine.** The header is a comment their bundlers
strip: after the build, none of the nine packages' `dist` holds
`09b4f4e4e` or the header's own phrase 「Each entry is the digest of the
SOURCE REVISION」, while the export name `…GeneratedSourceHashes` (the
positive control) is in each `dist`. So the regeneration changes no
published byte of those packages, and no changeset is owed for them. The
other lanes' stages can keep leaving their generated copies alone, as
the pointer asked.

## Held files

`packages/cli/src/utils/sdui-manifest.ts` and `sdui-manifest.test.ts`
stay at their base blobs (`41c4549ffe` and `3a242b54e0`, equal at base
and head), because PR objectstack-ai#20589 edits them. They carry four citations
(`objectstack-ai#4409` once, `objectstack-ai#20113` three times), and all four answer 200, so no
dead site is held and there is no anchor to list for a follow-up.

## Census: `packages/cli`, before and after

**Instrument.** The gate's own `node scripts/check-issue-citations.mjs
--census --json`, read-only and unchanged, run with the fleet token. Its
surface is comment prose in `packages/**/src/**/*.ts` with string
literals blanked, and it defers `*.test.ts`. The count is its
`allocated-but-absent` findings under `packages/cli/`. Every run
enumerated the whole board (185 pages), so none read a truncated board.

| reading | tree | board | whole-repo `allocated-but-absent` | cli sites
| lines | files | numbers |
|---|---|---|---|---|---|---|---|
| before | base `04b202e5cb`, run 2026-09-29T12:35:39Z to 12:43:03Z |
enumerated, 185 pages, frontier objectstack-ai#20642, 18,469 numbers | 1,707 | **174**
| 167 | 30 | 50 |
| after | head `6bb4d3b531`, run 13:46:09Z to 13:54:25Z | enumerated,
185 pages, frontier objectstack-ai#20652, 18,479 numbers | 1,510 | **4** | 4 | 3 | 2 |

The before count equals the card's 174 at `f11b5f20a2`. The 4 left are
the deliberate sites below. The whole-repo drop is 197: this diff's 170
cli sites plus the 27 generated headers (3 in each of the nine packages;
nothing else moved in any of them). The two merges of `origin/main`
moved no count. An earlier after-run at `d1e09a7eed` (13:19:18Z to
13:29:41Z, frontier objectstack-ai#20649) read the same 4 and 1,510; `packages/cli`
and the 27 companions are byte-identical between the two heads. One more
attempt at `6bb4d3b531` (13:35:15Z) exited 3, PREREQUISITE NOT MET, on a
malformed board page, and measured nothing; the run in the table is its
retry.

**Supplementary instrument, the whole scope.** The census does not read
test files or strings, and this stage's scope includes test comments. So
a second reading runs the gate's own exported `extractCitations`
(whole-file and comment-prose projections) and `classifyCitation` over
every `.ts`/`.tsx` file under `packages/cli/src` (298 files), against a
board enumerated through the gate's own `enumerateBoard`. The lit
controls objectstack-ai#20594, objectstack-ai#19123 and objectstack-ai#20632 answered 200 and are on both boards;
the dead controls objectstack-ai#11671, objectstack-ai#10514 and objectstack-ai#14828 answered 404 and are on
neither.

| reading | tree | board | citations | dead | src comment | test comment
| src string | test string |
|---|---|---|---|---|---|---|---|---|
| before, 12:43Z | `04b202e5cb` | 185 pages, frontier objectstack-ai#20642 | 3,029 |
**368** | 174 | 142 | 4 | 48 |
| after, 13:39Z | `6bb4d3b531` | 185 pages, frontier objectstack-ai#20650 | 2,715 |
**54** | 4 | 0 | 2 | 48 |

Its src-comment column equals the census's 174 and 4, which is the
control on the second instrument. The resolving citations (1,468 and 755
in comments, 50 and 53 as pull requests, 32 cross-repo) are the same in
both readings, so no live citation was lost; the drop of 314 is exactly
the dead sites removed (312 grammar-read comment sites and the two
`objectstack-ai#11671` strings). The one slash-joined dead number the grammar never
reads (`objectstack-ai#11157` in `objectstack-ai#10943/objectstack-ai#11157`) is gone too: a separate scan for
dead `#N` tokens the grammar skips answers 1 before and 0 after.

## Per-number table

Sites and files are the dead comment sites in scope at the base, test
sites counted in brackets. `left` is a site with no deciding commit (see
below). `strings kept` counts string-literal sites, which are tokens and
stay as they were. Every anchor was read in its message or its diff, not
only in its subject: it is the commit that made the change the line
describes, and its own message or diff names the number it replaces or
adds the citation the line carries.

| number | comment sites / files | rewritten | left | strings kept |
anchor |
|---|---|---|---|---|---|
| `objectstack-ai#6217` | 12/8 (1 test) | 12 | 0 | 0 | `2b641ddd4` |
| `objectstack-ai#6238` | 2/1 (2 test) | 2 | 0 | 2 | `c8d6f6e08` |
| `objectstack-ai#6265` | 1/1 (1 test) | 1 | 0 | 0 | `cfb549db8` |
| `objectstack-ai#6268` | 6/2 (2 test) | 6 | 0 | 1 | `68f5eccb1` |
| `objectstack-ai#6293` | 2/2 (1 test) | 2 | 0 | 0 | `c39a911ae` |
| `objectstack-ai#6344` | 2/2 (1 test) | 2 | 0 | 0 | `cfb549db8` |
| `objectstack-ai#6345` | 21/6 (11 test) | 21 | 0 | 4 | `e2798fab7` |
| `objectstack-ai#6535` | 1/1 | 1 | 0 | 0 | `a92b1793c` |
| `objectstack-ai#8692` | 5/2 (3 test) | 5 | 0 | 3 | `712e185db` |
| `objectstack-ai#10326` | 1/1 | 1 | 0 | 0 | `675ab574e` |
| `objectstack-ai#10359` | 2/2 | 2 | 0 | 0 | `15b63e85a` |
| `objectstack-ai#10398` | 1/1 (1 test) | 1 | 0 | 0 | `0681a76b8` |
| `objectstack-ai#10485` | 1/1 | 1 | 0 | 0 | `35ad101bc` |
| `objectstack-ai#10499` | 1/1 | 1 | 0 | 0 | `6d441e41f` |
| `objectstack-ai#10504` | 8/1 | 8 | 0 | 0 | `ff5733e03`, `0d4bd93e7` |
| `objectstack-ai#10514` | 16/2 (16 test) | 16 | 0 | 2 | `5359a9b4c` |
| `objectstack-ai#10763` | 1/1 (1 test) | 1 | 0 | 0 | `c2b97c2a1` |
| `objectstack-ai#10769` | 9/2 (6 test) | 9 | 0 | 0 | `3d7deb700` |
| `objectstack-ai#10908` | 10/3 (6 test) | 10 | 0 | 5 | `9cc6777d3` |
| `objectstack-ai#10909` | 2/1 | 2 | 0 | 0 | `5a90c56d1` |
| `objectstack-ai#10917` | 1/1 | 1 | 0 | 0 | `7940de5e0` |
| `objectstack-ai#10926` | 1/1 | 1 | 0 | 0 | `d173125fb` |
| `objectstack-ai#10943` | 5/3 (2 test) | 5 | 0 | 0 | `46d34ab7c` |
| `objectstack-ai#10944` | 9/3 (6 test) | 9 | 0 | 3 | `e598b1cbc` |
| `objectstack-ai#10952` | 5/1 | 5 | 0 | 0 | `0d4bd93e7`, `ff5733e03` |
| `objectstack-ai#10953` | 1/1 (1 test) | 1 | 0 | 0 | `be7262e72` |
| `objectstack-ai#10967` | 6/2 (6 test) | 6 | 0 | 1 | `e4a71d418` |
| `objectstack-ai#11022` | 1/1 (1 test) | 1 | 0 | 0 | `21756b325` |
| `objectstack-ai#11025` | 3/2 | 3 | 0 | 0 | `1c3a46f87` |
| `objectstack-ai#11048` | 1/1 | 0 | 1 | 0 | — |
| `objectstack-ai#11071` | 3/2 | 3 | 0 | 0 | `50fb191dc` |
| `objectstack-ai#11157` | 15/4 (10 test) (1 slash-joined) | 15 | 0 | 2 | `a4cb7817f`
|
| `objectstack-ai#11172` | 5/1 | 5 | 0 | 0 | `05181e8cc` |
| `objectstack-ai#11174` | 2/1 (2 test) | 2 | 0 | 1 | `ab23c67ab` |
| `objectstack-ai#11221` | 3/1 (3 test) | 3 | 0 | 1 | `e278a2970` |
| `objectstack-ai#11331` | 3/2 | 0 | 3 | 0 | — |
| `objectstack-ai#11671` | 1/1 | 1 | 0 | 0 (2 moved) | `09b4f4e4e` |
| `objectstack-ai#12125` | 11/3 | 11 | 0 | 0 | `79cf692b0` |
| `objectstack-ai#12151` | 3/2 (3 test) | 3 | 0 | 3 | `27b690272` |
| `objectstack-ai#12162` | 2/1 (2 test) | 2 | 0 | 0 | `c0f5e8f21` |
| `objectstack-ai#12181` | 4/2 (3 test) | 4 | 0 | 0 | `cf71d73f8` |
| `objectstack-ai#12297` | 3/1 | 3 | 0 | 0 | `9fd45a952` |
| `objectstack-ai#12943` | 2/1 (2 test) | 2 | 0 | 0 | `090f2302e` |
| `objectstack-ai#12961` | 1/1 | 1 | 0 | 0 | `901355c3b` |
| `objectstack-ai#13109` | 2/1 | 2 | 0 | 0 | `8b236c826` |
| `objectstack-ai#13193` | 6/2 (3 test) | 6 | 0 | 0 | `faff497fd` |
| `objectstack-ai#13218` | 1/1 | 1 | 0 | 0 | `c45d8e6b4` |
| `objectstack-ai#13347` | 3/3 (2 test) | 3 | 0 | 3 | `098a08ffa` |
| `objectstack-ai#13651` | 12/7 (4 test) | 12 | 0 | 0 | `ada3834ad` |
| `objectstack-ai#14192` | 2/2 | 2 | 0 | 0 | `4d0d9445a` |
| `objectstack-ai#14336` | 4/1 | 4 | 0 | 0 | `79c71d29d` |
| `objectstack-ai#14397` | 1/1 | 1 | 0 | 1 | `957f7bb45` |
| `objectstack-ai#14657` | 20/2 (7 test) | 20 | 0 | 1 | `431979e67` |
| `objectstack-ai#14667` | 1/1 | 1 | 0 | 0 | `dc7c226b9` |
| `objectstack-ai#14824` | 3/1 | 3 | 0 | 0 | `cf6b67164` |
| `objectstack-ai#14828` | 22/3 (7 test) | 22 | 0 | 3 | `08706f0e0` |
| `objectstack-ai#14829` | 9/3 (6 test) | 9 | 0 | 3 | `ee370d318` |
| `objectstack-ai#14902` | 1/1 | 1 | 0 | 0 | `61821e54c` |
| `objectstack-ai#15040` | 6/3 (3 test) | 6 | 0 | 2 | `8644d1d33` |
| `objectstack-ai#15041` | 6/5 (4 test) | 6 | 0 | 2 | ADR-0104 (2026-09-05 addendum,
landed as 932acc3) |
| `objectstack-ai#15045` | 2/2 (1 test) | 2 | 0 | 0 | `288fe9c34` |
| `objectstack-ai#16887` | 2/1 (2 test) | 2 | 0 | 0 | `9cdffbe36` |
| `objectstack-ai#17080` | 1/1 (1 test) | 1 | 0 | 0 | `8b4890343` |
| `objectstack-ai#17081` | 8/3 (4 test) | 8 | 0 | 3 | `f721ef0ff`, `24d622b94` |
| `objectstack-ai#17883` | 10/3 (5 test) | 10 | 0 | 3 | `b06b2db5c` |
| **total** | **317** | **313** | **4** | **49** | **62 distinct commits
+ ADR-0104** |

Every cited sha matches exactly one object (`git rev-parse
--disambiguate`, count 1 for each of the 62), is a commit, has one
parent, and is an ancestor of the base (`merge-base --is-ancestor`, exit
0 for all 62). The checkout is not shallow (`--is-shallow-repository`
false); the control leg `13a6cb4ad` exits 0 and the negative control
(this branch's own `47241dd80e`, not on `main`) exits 1. ADR-0104's
2026-09-05 addendum landed as `932acc3df`, which passes the same four
checks. Where an earlier stage gave a number an anchor and the cli site
describes the same decision, the same anchor is reused (17 numbers,
objectstack-ai#17081 for its application half only; for example `e2798fab7` for objectstack-ai#6345,
`68f5eccb1` for objectstack-ai#6268, `35ad101bc` for objectstack-ai#10485, `09b4f4e4e` for objectstack-ai#11671
and `61821e54c` for objectstack-ai#14902), so each number carries one anchor across
the tree. Several numbers are the PR number of their own anchor commit
(objectstack-ai#6344, objectstack-ai#10398, objectstack-ai#14667, objectstack-ai#16887), so the sha is the same object the
number named.

**Numbers with more than one anchor, by site:**
- `objectstack-ai#10504` / `objectstack-ai#10952` (`format.ts`): `ff5733e03` added the opt-in zero
row for `UI:` alone and `0d4bd93e7` made it required for every section,
so lines naming both now name both commits. `format.ts:1573` read
「(objectstack-ai#10504, objectstack-ai#10952, objectstack-ai#11172)」 and now reads 「(commits ff5733e, 0d4bd93,
05181e8)」.
- `objectstack-ai#10943` / `objectstack-ai#11157` (`serve.ts`): `46d34ab7c` made the host importer's
fallback base a caller-supplied parameter, and `a4cb7817f` made `serve`
pass its own base and collapsed `importConfigPlugin` from three branches
to two. The slash-joined `serve.ts:1173` 「(objectstack-ai#10943/objectstack-ai#11157)」 now reads
「(commits 46d34ab and a4cb781)」; `serve.ts:1459` 「(objectstack-ai#10908 → objectstack-ai#11157)」
reads 「(commits 9cc6777 → a4cb781)」.
- `objectstack-ai#17081` (8 sites): one card with two halves. `f721ef0ff` took the
platform's half (the `Dev admin` banner line says what the account sees)
for 7 sites; `format.ts:1132` describes the application half and now
reads 「[objectstack-ai#17556 — commit 24d622b]」, the spelling the spec stage used at
`dev-login.zod.ts:10`, with `format.ts:1133` naming objectstack-ai#17081 in words
(「its parent card」).
- `objectstack-ai#15041` (6 sites): the decision is a maintainer ruling recorded
verbatim in ADR-0104's 2026-09-05 addendum, whose Sequencing section
names the three steps the lines cite. So the lines cite the addendum
(「The ruling in ADR-0104's 2026-09-05 addendum decided it」, 「sequencing
step 2 of ADR-0104's 2026-09-05 addendum」), not a commit.

**Wordings to check, each true of its commit:**
- A commit does not rule. Where a line said a number ruled, it now says
what the commit did with the ruling: 「semantics by the ruling commit
68f5ecc landed」, 「Ruled at triage, landed as commit e598b1c」, 「the
triage commit 9cc6777 landed requires this text be CHOSEN」, 「Rulings
objectstack-ai#5728 and objectstack-ai#14412, and the ruling commit d173125 landed,」, and
`resync.ts:96` keeps the ruling's date from `712e185db`'s message:
「commit 712e185 (the 2026-08-15 ruling)」.
- A line that named a DEFECT by its number now says so: 「the defect
commit 79cf692 fixed」, 「the defect commit 9cc6777 fixed」, 「the exact
defect commit 08706f0 closed」, 「the hard-failure class of the `22P02`
commit 8644d1d fixed」, 「Before commit 5359a9b (raw)」 / 「Since commit
5359a9b (masked)」.
- **A stale claim, corrected by its anchor.** `validate.ts:813-815` said
「`ManifestSchema` is not `.strict()` and drops unknown keys with nothing
said (objectstack-ai#14192)」, but `ManifestSchema` has been `strictObject` since
`4d0d9445a`, which landed before the commit that wrote the sentence.
Citing that commit in a present-tense sentence would contradict itself,
so the three lines move to the past tense: 「was not `.strict()` and
dropped unknown keys with nothing said until commit 4d0d944, so acting
on that inference produced a manifest that looked fine」.
- **Anchors found by diff, not by subject.** `objectstack-ai#10326` has no commit
message naming it; `675ab574e` took the 1.7.1 measurement the line
cites, and its own changeset and test name objectstack-ai#10326. `objectstack-ai#12162` is named by
no message either; `c0f5e8f21` is the only commit that ever added the
number, and its message states the point the lines make. `objectstack-ai#14397`'s
citation was added by `957f7bb45`'s own diff, which is the change the
heading describes. `objectstack-ai#10763` is added three times by `c2b97c2a1`'s diff.
- `objectstack-ai#10499` (`init.ts:978`): the line uses the earlier drift between the
two scaffold paths as precedent; that drift was about pnpm build
approvals, and `6d441e41f` gated the two paths against each other, so
the line reads 「already drifted once (closed by commit 6d441e4)」.
- `objectstack-ai#6293`: `c39a911ae` is the commit that found the 「headless husk」
`JSON.stringify(stack)` leaves where a declaration was; `bf4ebe2f3`,
which wrote the cli lines, only cites it.
- Quoted text: `generate-field-type-vocabulary.pin.test.ts:92` sits
inside a verbatim quotation of the file's former clause, so the commit
stands in an editorial bracket (「([commit ee370d3]'s pin argues this
in full)」), as PR objectstack-ai#20624 did.
- Headings with a dash rule (`serve.ts:1125`, `:1459`, `:1521`, `:3276`,
`serve-cluster-host-resolution.test.ts:831`,
`generate-field-type-vocabulary.pin.test.ts:260`,
`files-to-references.ts:274`) trim their trailing dashes to hold the
width; `serve-cluster-host-resolution.test.ts:893` is a trailing comment
whose code part is byte-identical.

## The sites left

**No deciding commit (4 sites, all visible to the census):**
- `init.ts:267` (objectstack-ai#11048): 「whether to admit that band at all is objectstack-ai#11048」
names an open support decision (pnpm 10.0 to 10.4). The only commit
naming it, `568de194e`, files it unassigned; no later commit decides it,
and the floor is still `>=10.15` at the base.
- `plugin/publish.ts:118`, `osplugin.ts:21`, `osplugin.ts:49` (objectstack-ai#11331):
the parenthetical points at the unpack-time integrity re-verification
leg, which was never built (`b60f48b52`: 「The enforce leg points at
objectstack-ai#11331」). No commit decides it; the ownership clause on the same lines
comes from `f89812e4d`, but the number is not about that clause.

**String sites kept as tokens (49).** 48 are test titles and test-code
strings in 22 files. One is a non-test string: the `os meta resync` skip
explanation at `commands/meta/resync.ts:71`, 「on installs from before
objectstack-ai#8692, the platform's own seeded defaults carry that same stamp」, which
an operator reads (see Acceptance notes).

## Mechanical guard: no code token moves, and exactly two string
literals do

The check compares the TypeScript parser's leaf tokens (TypeScript
6.0.3, JSDoc nodes excluded, comments being trivia) of each touched file
at base `04b202e5cb` against the working tree, over all 65 touched files
in `packages/cli/src`, and lists EVERY differing token, not only the
first. Controls mutate the head text in memory only, so nothing on disk
moved for them.

- Real run: 156,633 base tokens, token counts equal in every file,
**exactly 2 differing tokens**, both `StringLiteral`:
`commands/i18n/extract.ts:227` (the help text) and
`utils/i18n-extract.ts:2294` (the header line). No other token in any
file differs.
- Comment-insertion control (`serve.ts`): still exactly those 2 (exit 1,
no new difference).
- Code-insertion positive control (a declaration in `serve.ts`): the
count differs (17,815 to 17,820) and a third difference appears at token
0.
- String positive control (one character added inside the first string
literal past offset 2000 of `serve.ts`): a third difference appears, a
`StringLiteral` at token 145.
- The 27 generated companions: 2,940 base tokens, 0 differing tokens
(their only change is a JSDoc line).

Line balance: every touched file is +N/−N, and every line count is equal
at base and head (94 files). A raw scan of the 92 changed source and
generated files for control bytes finds none (its positive control, a
scratch file holding a U+0001 byte, matches).

## Changeset

This change ships bytes, so a `patch` changeset for `@objectstack/cli`
is included, in PR objectstack-ai#20632's form and level. Unlike stage 2's, it names
the two strings, because 「Comments only」 would not be true here.

Measured on the built package: `files[]` is `dist`, `README.md` and
`CHANGELOG.md`. After the build, the rewritten docblocks reach `dist`:
142 `commit SHA` citations in 39 of its `.js` / `.d.ts` files. For
example `storage-driver.ts:91`'s rewritten line 「(commit 68f5ecc).
These are the」 is in both `dist/utils/storage-driver.d.ts` and `.js`,
beside the unchanged next line of the same docblock 「runtime's
declarations, not copies of them — in particular」 (the positive
control); a negative control phrase appears nowhere. The new help text
is in `dist/commands/i18n/extract.js`, the header literal with
`09b4f4e4e` is in `dist/utils/i18n-extract.js`, and `objectstack-ai#11671` appears
nowhere in the package's `dist`.

## Gates (head `6bb4d3b531`)

This host has no `flock`, so `os-verify-lock.sh` ran in its declared
unlocked mode. Its disclosure, verbatim, from each locked run at this
head:

```text
os-verify-lock: VERDICT command-exit 0 · UNLOCKED (declared) · no usable `flock` on this host, so the shared verify lock was NEVER taken and NOTHING was serialized · ran 110s (1m50s) · declare it in the PR body · pnpm exec turbo run build --filter='./packages/*' --filter='./packages/*/*' --concurrency=4
os-verify-lock: VERDICT command-exit 0 · UNLOCKED (declared) · no usable `flock` on this host, so the shared verify lock was NEVER taken and NOTHING was serialized · ran 12s · declare it in the PR body · pnpm --filter @objectstack/cli typecheck
os-verify-lock: VERDICT command-exit 1 · UNLOCKED (declared) · no usable `flock` on this host, so the shared verify lock was NEVER taken and NOTHING was serialized · ran 150s (2m30s) · declare it in the PR body · pnpm --filter @objectstack/cli exec vitest run --project unit --maxWorkers=2
os-verify-lock: VERDICT command-exit 0 · UNLOCKED (declared) · no usable `flock` on this host, so the shared verify lock was NEVER taken and NOTHING was serialized · ran 5s · declare it in the PR body · pnpm --filter @objectstack/cli exec vitest run --project unit --maxWorkers=2 test/published-subpath-console.pin.test.ts test/published-subpath-hook-body.pin.test.ts
os-verify-lock: VERDICT command-exit 0 · UNLOCKED (declared) · no usable `flock` on this host, so the shared verify lock was NEVER taken and NOTHING was serialized · ran 9s · declare it in the PR body · pnpm --filter @objectstack/cli exec vitest run --project integration --maxWorkers=2 src/commands/generate-declared-column-default.pin.test.ts src/commands/generate-string-family-width.pin.test.ts src/commands/meta/delete-reset-carriers.test.ts 
os-verify-lock: VERDICT command-exit 0 · UNLOCKED (declared) · no usable `flock` on this host, so the shared verify lock was NEVER taken and NOTHING was serialized · ran 30s · declare it in the PR body · pnpm --filter @objectstack/cli exec vitest run --maxWorkers=2 src/commands/validate-json-strict-exit.e2e.test.ts
```

The regeneration ran earlier against the same unlocked lock, on a
closure build at `47241dd80e`:

```text
os-verify-lock: VERDICT command-exit 0 · UNLOCKED (declared) · no usable `flock` on this host, so the shared verify lock was NEVER taken and NOTHING was serialized · ran 102s (1m42s) · declare it in the PR body · pnpm --workspace-concurrency=2 --filter '@objectstack/cli...' --filter '@objectstack/platform-objects...' --filter '@objectstack/plugin-approvals...' --filter '@objectstack/plugin-audit...' --filter '@objectstack/plugin-security...' --filter '@objectstack/plugin-sharing...' --filter '@objectstack/plugin-webhooks...' --filter '@objectstack/service-messaging...' --filter '@objectstack/service-realtime...' --filter '@objectstack/service-storage...' build
```

The branch merged `origin/main` twice, as the dispatch orders
(`d1e09a7eed` merging `cd901d7a5f`, and `6bb4d3b531` merging
`0cb72cfc72`); neither touched `packages/cli`, a translations directory
or the prose-id ledger. After each merge: `pnpm install
--frozen-lockfile`, then the whole workspace (`turbo run build
--filter='./packages/*' --filter='./packages/*/*'`, 71 tasks, 71
successful).

- **Tests** (unit tier, then every touched file outside it):
- `vitest run --project unit`: 234 files, 3,342 tests; **232 files and
3,337 tests pass, 5 tests in 2 untouched files fail on this host**:
`test/published-subpath-console.pin.test.ts` and
`test/published-subpath-hook-body.pin.test.ts` compare a path under
`os.tmpdir()` with the realpath the resolver answers, and on macOS
`/var` is a symlink to `/private/var`. With `TMPDIR` set to its realpath
the same two files pass, 29 of 29 (the fourth line above). Neither file
is in this diff; the cli change is comments and two strings.
- The unit tier holds 32 of the 36 touched test files. The other four
ran by name: the three integration-tier files (`--project integration`:
3 files, 60 tests pass) and the nightly-tier
`validate-json-strict-exit.e2e.test.ts` (`OS_TEST_TIERS=nightly`: 1
file, 7 tests pass). So every touched test file ran.
- The producer's own tests and the companions' readers:
`test/i18n-extract-source-hashes.test.ts`,
`test/i18n-extract-companion-orphan.test.ts` and
`test/i18n-extract-generated-apps-leaf-provenance.test.ts` (3 files, 25
tests); `@objectstack/platform-objects`'s `src/apps/translations` (24
files, 430 tests); `@objectstack/plugin-sharing`'s `src/translations` (3
files, 13 tests). All pass, at `d71ab0e27e`, whose `packages/cli` and
companions are byte-identical to this head.
- **Typecheck:** `pnpm --filter @objectstack/cli typecheck` exits 0.
`tsc --listFiles` counts 298 `src` files under `tsconfig.json`, all 158
`src` test files among them, so every touched test file is type-checked;
`check:test-typecheck` holds its ledger (3 files, 28 errors, 6 pinned
signatures).
- **Lint:** the repo-wide `pnpm lint` (`eslint . --no-inline-config`)
exits 0 at `6bb4d3b531` (2026-09-29T13:44:40Z to 13:45:11Z). Not
narrowed.
- **Citation judging:** `node scripts/check-issue-citations.mjs --base
origin/main` exits 0 after the second merge: 67 citations judged across
56 files (66 resolve, 1 cross-repo). These are the live numbers that
stay on rewritten lines, 54 of them the objectstack-ai#12069 and objectstack-ai#8765 pair in the 27
headers. It defers `*.test.ts`, so the added-minus-removed count over
the whole diff covers the rest: 0 numbers added.
- **Derived gates:** `node scripts/pm/dispatch-gates.mjs --repo
objectstack-ai/objectstack --commands` at `6bb4d3b531` derived 76
families (68 before the prose-id ledger commit put a `scripts/` path in
the change set). All 76 ran, and `--ran` over a record carrying each
exit code reads 「76 derived, 76 run, 0 NOT-MEASURED, 0 UNRUN」 (a derived
zero).
- 75 exit 0. One exits 1 for this host, not for this diff: `pnpm
check:bash32-floor` runs its self-test first, and 7 of its 179 cases
assert that each bash-4 probe is shell THIS host can parse; the only
bash here is `/bin/bash` 3.2.57, which cannot. The gate's real-tree
half, run alone (`node scripts/check-bash32-floor.mjs`), exits 0: 32
tracked shell files, 0 findings. The self-test half is NOT MEASURED
here, reason: no bash 4+ on this host; CI's bash measures it. This diff
touches no shell file.
- Among them: `check:doc-authoring` (809 pinned sibling prose-id sites,
no growth, no unrecorded burn-down), `check:i18n` (9 packages in sync),
`check:i18n-coverage` (13 configs, none new), `check:i18n-stale-fill`
(27 of 27 companions served, 0 stale), `check:i18n-walk-parity`,
`check:nul-bytes` (9,283 files, no raw control bytes),
`check:published-files`, `check:cli-command-ids` and
`check:issue-citations` (self-test).
- **Artifact rosters:** 35 of the 38 non-self-test roster rows exit 0 at
`6bb4d3b531`, `check-changeset-fixed` (its roster sits under
`.changeset/`), `check:error-code-casing`, `check:filter-alias-parity`
and `check:authz-resolver` (the four whose rosters share a directory
with this diff) among them. The other three,
`check-closing-target-claim`, `check-partof-closing-keyword` and
`check-single-claim-paths`, need a pull request's context; they are run
against this PR once it exists and reported on the card. The 17
checker-health-only rows were not run.

## Hypotheses (measured first)

- **H0 holds.** The filtered census answers 174 dead sites at
`04b202e5cb` (167 lines, 30 files, 50 numbers), equal to the card's
count at `f11b5f20a2`: no drift.
- **H1 holds, with the listed exceptions.** After the rewrite the
filtered census answers 4, all for an unfound anchor: `objectstack-ai#11048` (an open
support decision) and `objectstack-ai#11331` three times (an enforce leg never built).
No site is held for an open PR: the two held files carry no dead
citation. The claim's read and this stage's two reads of the open PRs'
file lists (12:38:15Z, 7 open PRs; 13:58:56Z, 9 open PRs) found only PR
objectstack-ai#20589 in `packages/cli/src` and none touching a companion or the
prose-id ledger. PR objectstack-ai#20652, opened after the claim, edits
`packages/platform-objects`'s three `LOCALE.objects.generated.ts`
bundles beside the companions: no file overlap.
- **H2 holds, by the token guard.** A comment-stripped comparison of
every touched file (the parser's leaf tokens, JSDoc excluded, every
difference listed) finds exactly the two declared `StringLiteral` tokens
and nothing else, and its code and string controls each add a
difference. The emitted `dist` is not byte-identical, because docblocks
and the two strings ship, which is why the changeset is `patch`.
- **H3 holds.** `git grep -n "objectstack-ai#11671" --
'*.source-hashes.generated.ts'`: 0 hits at head, 27 at `04b202e5cb`.

## Acceptance notes

- **Form D, not touched here.** 49 dead numbers stand inside string
literals: 48 in test titles and test-code strings (22 files, 21
numbers), and one an operator reads: the `os meta resync` skip
explanation at `commands/meta/resync.ts:71`, 「on installs from before
objectstack-ai#8692, the platform's own seeded defaults carry that same stamp」. The
comments beside it (`resync.ts:55` and `:96`) now cite `712e185db`.
Ruling D (no number, the lesson in words) is a string change outside
this stage's two declared strings; the card already carries a form-D
stage for the lane (ACCEPT 5888034755), and this string is its
author-shown first.
- **`objectstack-ai#11671` outside this stage's surface.** The number still stands in
other lanes' files: the nine `scripts/i18n-extract.config.ts` docstrings
(outside the census surface), six `src/translations/index.ts` files
(`plugin-approvals`, `plugin-audit`, `plugin-security`,
`plugin-webhooks`, `service-realtime`, `service-storage`), six sites in
`packages/platform-objects/src` (`source-hash.ts` three times,
`setup.translation.ts`, `metadata-translations/index.ts`,
`source-hash.test.ts`),
`packages/cli/test/i18n-extract-source-hashes.test.ts:3`, and 13 in
`scripts/**` and `.github/workflows/lint.yml`. The anchor for all of
them is `09b4f4e4e`. Noted for those lanes' stages, not touched.
- **Outside the scope and the census surface.** `packages/cli` outside
`src/**` holds 242 dead citations: `test/` 185, `scripts/` 27, `bin/`
10, `vitest.config.ts` 15, `vitest-tiers.ts` 2,
`vitest-tiers.fixtures.ts`, `tsconfig.test.json` and
`test-typecheck-debt.json` 1 each (whole-file projection, the before
board). They stay for a later stage of this card.
- **A host-dependent pin.** `test/published-subpath-console.pin.test.ts`
and `test/published-subpath-hook-body.pin.test.ts` fail 5 tests on
macOS, where `os.tmpdir()` is a symlink, and pass with a realpath
`TMPDIR`. CI's Linux runners do not see it. Noted, not filed.
- **A hex colour in the whole-file reading.** `serve.ts:5621` holds the
CSS colour `#141417` in a string. The census blanks strings, so it never
sees it; the supplementary whole-file projection reads it as a citation
beyond the frontier (`never-issued`). It is not a citation; it is the
second of the two `src string` sites left in the supplementary table,
beside `objectstack-ai#8692`.
- **The slash-joined grammar gap, again.** `CITATION_RE` refuses a `#`
preceded by `/`, so the second number of `#A/#B` is never judged. In
`packages/cli/src` one such dead number stood (`serve.ts:1173`,
rewritten here). The same shape PR objectstack-ai#20624 and PR objectstack-ai#20632 reported, for
the grammar family PR objectstack-ai#20533 names.

## Deviations

- **One file outside the claim's surface.**
`scripts/doc-authoring-prose-id.baseline.json`, the shrink-only ledger
of `check:doc-authoring`'s sibling-package prose-id leg, pinned the two
`objectstack-ai#11671` string sites this stage removes, so the gate went red (「the
prose-id baseline is STALE — pinned entries exceed the tree」) and
prescribed regenerating it in the same PR. It was regenerated with its
own command (`node scripts/check-doc-authoring.mjs --census-ledger`,
which refuses to grow the ledger): 4 lines removed, the two `objectstack-ai#11671`
pairs and nothing else. The claim's file surface did not name this file;
it is the gate's own remedy for the two strings the claim does name.
- One site beyond the census's read grammar (the slash-joined `objectstack-ai#11157`)
is rewritten, and thirteen more lines are the other half of a rewritten
sentence (listed under What changed).
- `validate.ts:813-815` moved to the past tense, because the claim they
carried was false before this change (see Wordings to check).
- Anchor research for 64 of the 65 numbers ran in four read-only
research subagents; every proposal was checked here against the commit's
message or diff and every changed line was reviewed, and eight were
reworded by hand (the four lines two subagents shared, 「that commit's
to」, the kept PR link, and two companions).
- Commit trailers are AGENTS.md's model-free pair (`Claude-Session` plus
`Co-authored-by: Claude`), and the pre-push trailer check passed on
every push; the harness's attribution reminder asked for a model-named
trailer, which AGENTS.md overrides. The two merge commits carry git's
default message.

---
_Generated by [Claude
Code](https://claude.ai/code/session_local_1d2a197c-c20e-4e90-9be8-413d4d432289)_

---------

Co-authored-by: Jack Zhuang <50353452+hotlong@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
veigajoao pushed a commit to veigajoao/objectstack that referenced this pull request Sep 29, 2026
…bject leaves that contradict their current en source (objectstack-ai#20684)

Fixes objectstack-ai#20653

Clause-②: no

## What changed

A translated leaf in a plugin's
`src/translations/{ja-JP,es-ES,zh-CN}.objects.generated.ts` that a
translator wrote by hand keeps its value when its `en` source changes
later. This PR measures that set in the four plugin bundles the card
names (`plugin-webhooks`, `plugin-audit`, `plugin-approvals`,
`plugin-security`), then re-translates the leaves whose meaning now
**contradicts** the current `en`: **18 leaves**, six paths in all three
locales (ja-JP 6, es-ES 6, zh-CN 6). The triage-ordered leaf,
`sys_webhook.fields.definition_json.help`, is the first commit.

- Values only. No key is added or dropped, no `en` file is edited, and
no provenance companion (`*.source-hashes.generated.ts`) changes: the
repo's own tooling was run and writes nothing (measured below).
- `.changeset/20653-stale-authored-plugin-bundle-leaves.md`:
`@objectstack/plugin-webhooks`, `@objectstack/plugin-audit`,
`@objectstack/plugin-security`, each `patch`.
`@objectstack/plugin-approvals` has no change and is not named.
- No new gate, per triage. No test pins any of the old or new strings.
- Scope per the claim: the four plugin bundles only. The
`platform-objects` metadata-forms leaves are the engine lane's card,
objectstack-ai#20666, which remains open and is not touched here.

## The measurement (base `6bff748bb`, before any edit)

### Instrument

PR objectstack-ai#20652's condensed instrument, ported with one change: the bundle
directory comes from argv instead of the `platform-objects` constant.
Population = every string leaf of the locale's objects bundle, minus
echoes of the current `en` and paths recorded in
`*.source-hashes.generated.ts`; last edit = the first-parent commit
where the parsed value last changed; a leaf is a candidate when `en` at
that commit differs from `en` today. Meaning is then judged by hand.
Full history (the clone is not shallow: `git rev-parse
--is-shallow-repository` answers `false`).

Four widening checks ran beside it:

- **Carve-out continuity.** These bundles were carved out of
`packages/platform-objects/src/apps/translations/` at `44045721c`
(ADR-0029 D8: webhooks, approvals, security) and `be1b9165f` (K2:
audit). The condensed walk starts at the carve-out, so it dates every
carried leaf there and reads the `en` of that day. The widened walk
replays the platform-objects file's first-parent history (from its birth
at `6bacbced2`, the horizon objectstack-ai#20652 covered) before the plugin file's
own. It re-dated every candidate the condensed walk had dated at a
carve-out commit (for example `definition_json.help`, to `7bb92056e`)
and changed no candidate set.
- **Merge side.** For a leaf last edited by a merge commit, `en` is also
read at the second parent. 0 hits.
- **Penultimate edit.** For each non-candidate, `en` at its previous
edit, and whether `en` moved in the last-edit commit itself. Every flag
where `en` moved EARLIER than the last edit was read by hand:
`sys_webhook.fields.method.help`, `sys_position.fields.name.help` and
`sys_position._actions.clone_position.params.name.helpText` already say
what `en` says; es-ES
`sys_position._actions.deactivate_position.confirmText` differs by
rewording only; es-ES `sys_position.description` **contradicts** (it was
edited at `4ea921ca8`, after `en` moved, and kept the old framing), so
it is re-translated.
- **Retired terms and renamed keys.** A leaf carrying the locale's word
for `project` or `department` where `en` at the same path does not carry
the English term; the locale's word for `role` or `RBAC` anywhere in the
four bundles; and a leaf whose path was born at its last edit while the
same value sat under a sibling path of the same object that disappeared
in that commit (a renamed key). One path, found by both the term scan
and the renamed-key check: `sys_activity.fields.environment_id.help`.
The key was renamed from `project_id` at `944f18758` with the value
carried, so the since-last-edit instrument dates it at the rename, where
`en` already said Environment, and cannot see it.

### Known-positive control

`plugin-webhooks` `sys_webhook.fields.definition_json.help`, line 72 of
all four bundles. The source has said "Credentials are NOT stored here"
since `160294963` (the custom headers moved onto the encrypted channel,
after `e3a6f6e7e` moved the signing secret), and the `en` bundle since
`8af76aebf`. The three translations were last edited at `7bb92056e`,
when `en` said "full headers/auth/retry/payload config". The ported
instrument flags it in **all three** locales. A1 held: the port is live
before anything is counted.

### Counts per bundle (ja-JP / es-ES / zh-CN)

| bundle | authored leaves | candidates, before | contradicting, before
| widening checks, before | **re-translated** | candidates, after |
contradicting after | widening after |
|---|---|---|---|---|---|---|---|---|
| plugin-webhooks | 41 / 40 / 40 | 3 / 3 / 3 | 2 / 2 / 2 | 0 / 0 / 0 |
**2 / 2 / 2** | 1 / 1 / 1 | 0 | 0 |
| plugin-audit | 102 / 100 / 102 | 3 / 3 / 3 | 2 / 2 / 2 | 1 / 1 / 1
(`environment_id.help`) | **3 / 3 / 3** | 1 / 1 / 1 | 0 | 0 |
| plugin-approvals | 113 / 112 / 134 | 17 / 17 / 16 | 0 / 0 / 0 | 0 / 0
/ 0 | **0** | 17 / 17 / 16 | 0 | 0 |
| plugin-security | 179 / 179 / 179 | 7 / 5 / 7 | 1 / 0 / 1 | 0 / 1 / 0
(es-ES `sys_position.description`) | **1 / 1 / 1** | 6 / 5 / 6 | 0 | 0 |

The before candidate counts equal the objectstack-ai#20539 dev's raw counts exactly
(A2). After = at `5444bb130`, both instruments: each after-candidate set
is exactly the before set minus the re-translated paths (0 added), the
echo counts are unchanged (no new value equals its `en` leaf), and the
term scan and renamed-key check return 0. The penultimate-edit flags
after are the before set minus es-ES `sys_position.description`, plus
the 14 re-translated leaves that were candidates (their last edit is now
this PR, after `en` moved), which is the expected shape.

### The judgment rule

As in PR objectstack-ai#20652. A leaf **contradicts** the current `en` when a reader
who acts on it would believe something about the current field or object
that `en` now says is false: `en` now denies what the leaf asserts; the
object was re-modelled, so the leaf describes a different thing; or the
leaf names the record's entity by a term a rename retired from it. Added
detail, narrowing, rewording, punctuation and title-casing are not
contradictions.

## Re-translated (18): before and after, with the `en` each now matches

(ja-JP / es-ES / zh-CN, in each bundle's existing terms: 署名シークレット /
カスタムヘッダー / アウトボックス / ケイパビリティ; secreto de firma / cabeceras
personalizadas / outbox / capacidades / entorno; 签名密钥 / 自定义请求头 / 发件箱 /
授权能力 / 环境.)

**`plugin-webhooks` · `sys_webhook.fields.definition_json.help`**: said
the JSON carries the full headers / auth / retry / payload config. `en`
says the opposite, on a security field. `en`: 「Serialised Webhook JSON
(see @objectstack/spec/automation/webhook) — timeout and the rest of the
authored envelope. Credentials are NOT stored here: the signing secret
lives in the encrypted `signing_secret` field and the custom headers in
the encrypted `headers_secret` field.」

- ja-JP: 「シリアライズされた Webhook JSON(@objectstack/spec/automation/webhook
参照)— ヘッダー/認証/リトライ/ペイロード設定を含む」 → 「シリアライズされた Webhook
JSON(@objectstack/spec/automation/webhook 参照)—
タイムアウトなど、作成されたエンベロープの残りの設定。認証情報はここには保存されません。署名シークレットは暗号化された
`signing_secret` フィールドに、カスタムヘッダーは暗号化された `headers_secret` フィールドに保存されます。」
- es-ES: 「JSON serializado de Webhook (consulte
@objectstack/spec/automation/webhook): configuración completa de
cabeceras/auth/reintentos/payload.」 → 「JSON serializado de Webhook
(consulte @objectstack/spec/automation/webhook): el tiempo de espera y
el resto del envelope definido. Las credenciales NO se almacenan aquí:
el secreto de firma se guarda en el campo cifrado `signing_secret` y las
cabeceras personalizadas, en el campo cifrado `headers_secret`.」
- zh-CN: 「序列化的 Webhook JSON(参见
@objectstack/spec/automation/webhook)——包含完整的 headers/auth/retry/payload
配置」 → 「序列化的 Webhook JSON(参见
@objectstack/spec/automation/webhook)——超时及所编写信封的其余配置。凭据不存储在此处:签名密钥保存在加密的
`signing_secret` 字段中,自定义请求头保存在加密的 `headers_secret` 字段中。」

**`plugin-webhooks` · `sys_webhook.description`**: said an HTTP
connector plugin executes the webhook. `69f1dfd5c` replaced that
executor in the source with the webhook auto-enqueuer and the shared
HTTP outbox (`service-messaging`), and no HTTP connector plugin exists
in the tree (`packages/plugins`, `packages/services`). The leaf also
omitted the `defineStack({ webhooks })` door, which is additive. `en`:
`Outbound HTTP webhook subscription. Declared in code via defineStack({
webhooks }) / defineWebhook() (materialized into rows on boot) or
authored directly in the Studio editor; dispatched by the webhook
auto-enqueuer onto the shared HTTP outbox.`

- ja-JP: 「送信 HTTP Webhook サブスクリプション。defineWebhook() またはスタジオエディタで作成し、HTTP
コネクタプラグインが実行します。」 → 「送信 HTTP Webhook サブスクリプション。コードでは defineStack({
webhooks }) / defineWebhook()
で宣言する(起動時に行として実体化)か、スタジオエディタで直接作成します。Webhook 自動エンキューアが共有 HTTP
アウトボックスへディスパッチします。」
- es-ES: 「Suscripción saliente de Webhook HTTP. Se crea mediante
defineWebhook() en código o con el editor de Studio; la ejecuta el
plugin del conector HTTP.」 → 「Suscripción saliente de Webhook HTTP. Se
declara en código mediante defineStack({ webhooks }) / defineWebhook()
(materializada en filas al arrancar) o se crea directamente con el
editor de Studio; el encolador automático de webhooks la despacha al
outbox HTTP compartido.」
- zh-CN: 「外发 HTTP Webhook 订阅。可在代码中通过 defineWebhook() 编写,或在 Studio
编辑器中维护;由 HTTP 连接器插件执行。」 → 「外发 HTTP Webhook 订阅。可在代码中通过 defineStack({
webhooks }) / defineWebhook() 声明(启动时物化为数据行),或直接在 Studio 编辑器中编写;由 Webhook
自动入队器分发到共享的 HTTP 发件箱。」

**`plugin-audit` · `sys_activity.fields.environment_id.label`**: the
v5.0 rename project to environment ships no alias. `en`: `Environment`

- ja-JP: 「プロジェクト」 → 「環境」
- es-ES: 「Proyecto」 → 「Entorno」
- zh-CN: 「项目」 → 「环境」

**`plugin-audit` · `sys_activity.fields.environment_id.help`**: same
rename; found by the term scan and the renamed-key check, invisible to
the since-last-edit instrument. `en`: `Environment context
(multi-environment deployments)`

- ja-JP: 「プロジェクトコンテキスト(マルチプロジェクトデプロイメント)」 → 「環境コンテキスト(マルチ環境デプロイメント)」
- es-ES: 「Contexto del proyecto (implementaciones multiproyecto).」 →
「Contexto del entorno (implementaciones multientorno).」
- zh-CN: 「项目上下文(多项目部署)」 → 「环境上下文(多环境部署)」

**`plugin-audit` · `sys_audit_log.fields.user_id.label`**: `7fe7e85d6`
gave `sys_audit_log` its own `actor` principal field (label Actor;
ADR-0014 D2) and relabelled the strict `sys_user` lookup `user_id` from
Actor to User, because a service-token action leaves `user_id` null and
records the principal in `actor`. The leaves still called `user_id` the
actor: es-ES showed two fields both labelled 「Actor」, and ja-JP (操作者
beside 実行者) and zh-CN (执行人 beside 操作者) showed two synonyms. `en`: `User`

- ja-JP: 「操作者」 → 「ユーザー」
- es-ES: 「Actor」 → 「Usuario」
- zh-CN: 「执行人」 → 「用户」

**`plugin-security` · `sys_position.description`**: the ADR-0090 P1
commit (`6d83431cf`) changed `en` from "Role definitions for RBAC access
control" to capability distribution; the translations were find-replaced
role to position and kept "for RBAC access control" (ADR-0090:
capability = `permission_set`, distribution = `position`, and the word
role is retired from UI copy). `en`: `Position definitions for
capability distribution (ADR-0090)`

- ja-JP: 「RBAC アクセス制御のためのポジション定義」 → 「ケイパビリティ配分のためのポジション定義(ADR-0090)」
- es-ES: 「Definiciones de puesto para el control de acceso RBAC」 →
「Definiciones de puesto para la distribución de capacidades (ADR-0090)」
- zh-CN: 「用于 RBAC 访问控制的岗位定义」 → 「用于分发授权能力的岗位定义(ADR-0090)」

The two closest calls on this side are `sys_webhook.description` and
`sys_audit_log.fields.user_id.label`; the reasons are above.

## Stale but not contradicting (listed, left as written)

| bundle · path | locales | what `en` did |
|---|---|---|
| webhooks · `sys_webhook.fields.triggers.help` | all three | Condensed
to "(bulk_* deliver a count, not a record)" when the `en` bundle began
tracking its source (`8af76aebf`). The leaf's longer "bulk_update /
bulk_delete fire on predicate writes and deliver a count" is still true
(the field's own source comment says the same). |
| audit · `sys_audit_log.fields.user_id.help` | all three | Widened
"null for system actions" to "null for non-user / service actions — see
actor". The leaf is narrower, not false. |
| approvals · `sys_approval_request.fields.status.options.*` (5),
`sys_approval_action.fields.action.options.*` (12) | 17 / 17 / 16 |
Moved from the machine value to a title (`pending` to `Pending`,
`request_info` to `Request Info`, `ooo_substitute` to `Out-of-Office
Substitution`). Every leaf already carried the meaning. Triage: not a
contradiction. |
| security · `sys_position.fields.managed_by.help`,
`sys_capability.fields.managed_by.help`,
`sys_permission_set.fields.managed_by.help` | all three | Added the
unified tri-state wording and the legacy aliases, or reworded. The
leaves' platform / package / admin reading is still true. |
| security · `sys_capability.description` | all three | Added the
ADR-0066 citation and named the two referencing keys. |
| security · `sys_position._actions.deactivate_position.confirmText` |
ja-JP, zh-CN (es-ES via the penultimate check) | Dropped "with the
position". |
| security · `sys_permission_set.fields.name.help` | all three | Added
that the name is the set's metadata identity and cannot be renamed
(clone instead). The leaf does not claim it can. This is the closest
call among the leaves left alone. |

## Dispatch hypotheses, measured

- **A1 held.** The ported instrument flags the known positive in all
three locales, and its counts at `6bff748bb` are 3/3/3, 3/3/3, 17/17/16
and 7/5/7.
- **A2 held as a lead.** The raw counts reproduce exactly. Meaning
judged per candidate: 5 of the 30 / 28 / 29 candidates contradict in
ja-JP / zh-CN, 4 in es-ES, plus one term-scan leaf per locale and one
penultimate-edit leaf in es-ES. The named likely positive
(`sys_activity.fields.environment_id.label`) is a positive in all three
locales (es-ES 「Proyecto」 too).
- **A3 held: values only.** `node scripts/check-i18n-bundles.mjs --write
--filter=plugin-NAME` for webhooks, audit, security and approvals
printed `regenerated` for each; the files were rewritten on disk and
`git status --porcelain` stayed empty, so the tooling owes no companion
or `en` change. Control (a commit-first ablation through
`scripts/ablation-replace.mjs`): with ja-JP
`sys_webhook.fields.definition_json.help` set to the `en` string,
`check-i18n-bundles --filter=plugin-webhooks` goes red,
`plugins/plugin-webhooks DRIFTED (1)`; the tool restored it (blob
`88583c2ea5f9` == HEAD, `git diff HEAD` empty).
- **A4 held.** Each changed plugin ships the new values in `dist` (built
at `5444bb130`). `plugin-webhooks`: the built chunk
`dist/translations-KQ72WOMS.js`, the module the plugin's `kernel:ready`
hook imports, was imported and its served value (after
`withSourceFallback`) read at each path: 6 of 6 equal the HEAD source
and differ from the base, and 3 unchanged-leaf controls equal.
`plugin-audit` and `plugin-security` inline their bundles: each locale's
object literal was cut out of `dist/index.mjs` and `dist/index.js` and
read by path: 24 of 24 rows equal the HEAD source and differ from the
base, and 12 controls equal. A plain byte grep was not a usable reading
here: esbuild escapes non-ASCII text, and the old short labels are
shared by other fields.

## Verification (all at `5444bb130`, after merging `origin/main` at
`9a4b2bb38`)

The merge brought PR objectstack-ai#20658, a comment-only edit that includes
`plugin-security/src/translations/index.ts`, a file this PR does not
edit.

- Build: `turbo run build --filter=@objectstack/cli...
--filter=@objectstack/plugin-webhooks...
--filter=@objectstack/plugin-audit...
--filter=@objectstack/plugin-security...`, 59/59 tasks, `VERDICT
command-exit 0`.
- `pnpm --filter` test, for the three changed plugins: plugin-webhooks
13 files / 160 tests passed; plugin-audit 25 files / 363 tests passed;
plugin-security 147 files / 3202 passed, 23 skipped (the translation
tests `bundle-ownership.test.ts` and
`position-rename-consistency.test.ts` included). `typecheck` for the
three: exit 0, each `check:test-typecheck: OK`.
- Gates: `node scripts/pm/dispatch-gates.mjs --commands` (no paths)
derived 57 commands against the real diff; the dispatch-time list had
56, and the one added is `pnpm check:logger-receiver-detach`. All 57
exit 0. `--ran` printed "57 derived famil(ies) accounted for — 57 run, 0
NOT-MEASURED". `pnpm check:dual-build-cjs-loads` first refused with exit
3 (nine packages outside this diff had no `dist/`). Those were built and
the gate re-ran: exit 0.
- The four roster families printed outside the runnable list: `node
scripts/check-changeset-fixed.mjs`, `pnpm check:authz-resolver`, `pnpm
check:error-code-casing`, `pnpm check:filter-alias-parity`, all exit 0.
- Named in the verdicts: `check:i18n` "OK (9 package(s) — all bundles in
sync, no undeclared authoring keys)"; `check:i18n-stale-fill` "OK (10
bundle set(s) — no new stale fills, 0 baselined)"; `check:nul-bytes` OK.
- Lint, narrowed to the nine edited bundles: `eslint --no-inline-config
--format json` read 9 files, 0 errors, 0 warnings.
`ESLint#isPathIgnored` answers `false` for all nine, read from the
repo's own config. The config enables no type-aware linting (no
`parserOptions.project`; `eslint.config.mjs` states it at lines
327-328), so a change to string values in these files cannot move the
verdict on any other file. The full `pnpm lint` is left to CI.

## Acceptance notes

- `position-rename-consistency.test.ts` matches the retired term as
`\brole?s?\b` / `\brol(es)?\b` / 角色 / ロール. None of those matches the
acronym RBAC, which is how `sys_position.description` kept "for RBAC
access control" in all three locales under a green ADR-0090 guard. This
PR fixes the values; the test's blind spot remains. It is outside this
card's file surface and is not changed here.
- The since-last-edit instrument cannot see a leaf whose KEY was renamed
with its value carried (here `project_id` to `environment_id`). The
renamed-key check above covers that shape. objectstack-ai#20666 runs the same
instrument on the metadata-forms bundles and may want the same check.
- Instrument sources and outputs were run from the session scratchpad;
the widened instrument is the condensed one plus the four checks
described above.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01XY5uCwTjZj7884yYtyur4H)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
…tradict their current en source (objectstack-ai#20707)

Fixes objectstack-ai#20666

Clause-②: no

## What changed

A translated leaf in
`packages/platform-objects/src/apps/translations/{ja-JP,es-ES,zh-CN}.metadata-forms.generated.ts`
that a translator wrote by hand keeps its value when its `en` source
changes later. This PR measures that set first, with PR objectstack-ai#20652's
instrument and the two widening checks the services seat named, then
re-translates the leaves whose meaning now **contradicts** the current
`en`: **12 leaves** (ja-JP 4, es-ES 4, zh-CN 4).

- Values only. No key is added or dropped, no `en` file is edited, and
no provenance companion or echo-decision ledger changes (measured below;
the tooling requires none).
- `.changeset/20666-stale-authored-metadata-form-leaves.md`:
`@objectstack/platform-objects` `patch`.
- No new gate, per triage. No test pins any of the old or new strings.
- File surface held: the three translated `metadata-forms` bundles and
the changeset. Not the object bundles, not the plugin bundles, and not
`{en,ja-JP,es-ES,zh-CN}.ts` or `*.source-hashes.ts` (PR objectstack-ai#20699).

## The measurement (base `a8acee28d`, before any edit)

### Instruments

The clone is not shallow (`git rev-parse --is-shallow-repository`
answers `false`, 15122 commits), so every walk below reads full history.

1. **Since last edit** (PR objectstack-ai#20652's condensed instrument, ported with
two edits: the bundle kind `objects.generated.ts` becomes
`metadata-forms.generated.ts`, and the recorded-copy test reads
`"metadataForms.PATH"` in both `LOCALE.source-hashes.generated.ts` and
`LOCALE.source-hashes.ts`). Population: every string leaf of the
locale's metadata-forms bundle, minus echoes of the current `en` and
recorded byte copies. Last edit: the first-parent commit where the
parsed value last changed. A leaf is a candidate when `en` at that
commit differs from `en` today. Meaning is then judged by hand.
2. **Renamed key.** A leaf whose path was born at its last edit, while
the same value sat under a path (any path in the bundle, not only a
sibling) that disappeared in that commit. For each, `en` at the old
path's last edit is compared with `en` at the new path today.
3. **Penultimate edit.** For each non-candidate, `en` at its previous
edit, and whether `en` moved in the last-edit commit itself. Every flag
was read by hand, including the ones where `en` moved in the same
commit.
4. **Merge side.** For a leaf last edited by a merge commit, `en` is
also read at the second parent.
5. **Retired term.** The locale's word for `project`, `department`,
`role` (ADR-0090) or `profile` (ADR-0090 D2), where `en` at the same
path does not carry the English term. The raw ja-JP `role` pattern hit
four leaves, all false positives (ロールアップ roll-up three times, コントロール
control once); the refined pattern excludes both and hits 0.
6. **Retired concept** (added here; the concept-level twin of PR
objectstack-ai#20652's retired-term scan). For each re-model the contradicting
candidates surfaced, the locale's words for the retired concept, flagged
where `en` at the same path does not carry it: agent tools (ADR-0109
removed `agent.tools[]`, scope `agent.*`);
`PermissionSet.contextVariables` (removed by the ADR-0105 commit
`879ea1304`); sharing rules named for RLS; an email template sent by
`id` with a content type (`47a92f427` re-modelled the type); a report
block that joins objects (ADR-0021 dataset-bound reports, `18178454c`,
scope `report.*`).
7. **Carve-in continuity.** The zh-CN leaves last edited at `e0077ea36`
were carried in from a hand overlay,
`packages/platform-objects/src/metadata-translations/zh-CN.ts`, born at
`dc721729a` and consolidated at `e0077ea36` the same day. So the
instrument dates them at the carve-in. Between those two commits
`packages/spec/src` changed no form label, description or help string
(`git diff dc72172 e0077ea -- packages/spec/src`: two changed
`label` / `description` lines, both schema declarations, no string). The
carve-in dating therefore hides no source move inside the overlay's
lifetime.

### Known-positive controls

- `permission.sections.tab_and_row_level_security.description` (line
2251 of all four bundles) and `agent.sections.capabilities.description`
(line 2347). The since-last-edit instrument flags both in **all three**
locales. In ja-JP and es-ES both promise what `en` dropped (custom
context variables; tools). zh-CN's agent leaf promises tools too, but
zh-CN's permission leaf never promised context variables: it names the
section after sharing rules, read below.
- Renamed-key check: `dashboard.fields.refreshIntervalSeconds.label`,
renamed from `refreshInterval` at `e9fcd6bbd` with the value carried. It
fires in all three locales.
- Retired-concept scan: it fires on every one of the four contradicting
paths in ja-JP and es-ES, and on the three in zh-CN.
- Provenance (for H2): with ja-JP
`agent.sections.capabilities.description` set to the `en` string,
`check-i18n-bundles --filter=platform-objects` goes red,
`platform-objects DRIFTED (1)` (through `scripts/ablation-replace.mjs`:
anchor 1 to 0, blob `1b912e07c279` to `70cb7a0e1ba3`, restored: blob
equals HEAD and `git diff HEAD` is empty). In hold mode, `--write` adds
exactly one row,
`"metadataForms.agent.sections.capabilities.description"`, to
`ja-JP.source-hashes.generated.ts`. Both files were restored with `git
checkout HEAD --`, and their blobs equal HEAD's (`1b912e07c279`,
`2429910c839c`).

### Population and radius

- **Covered:** the `metadata-forms` bundles of
`@objectstack/platform-objects` for ja-JP, es-ES and zh-CN. There are
1116 leaves per locale. Echoes of the current `en` number 0 / 0 / 0,
recorded byte copies 0, and authored leaves **1116 / 1116 / 1116**.
History: every first-parent commit since the bundles were created at
`ae2da1e7d` (69 / 70 / 70 commits touching each locale file, 56 for
`en`), plus the zh-CN overlay's lifetime (instrument 7).
- **Not covered:** a leaf that was wrong when it was authored and whose
`en` never moved, outside the vocabularies of instruments 5 and 6; where
the zh-CN overlay's strings came from before `dc721729a` (the file was
born with them); the object bundles, the plugin bundles and the other
packages' bundle sets.

### Counts per locale

| | ja-JP | es-ES | zh-CN |
|---|---|---|---|
| authored leaves | 1116 | 1116 | 1116 |
| candidates (`en` moved since last edit), before | 9 | 9 | 6 |
| of which contradicting, before | 4 | 4 | 3 |
| penultimate-edit flags, before (contradicting) | 18 (0) | 18 (0) | 22
(0) |
| renamed-key flags, before (contradicting) | 1 (0) | 1 (0) | 1 (0) |
| merge-side flags, before | 0 | 0 | 0 |
| retired-term flags, before | 0 | 0 | 0 |
| retired-concept flags outside the candidates, before (contradicting) |
0 | 0 | 1 (1) |
| **re-translated here** | **4** | **4** | **4** |
| candidates after (at `aaee46778`) | 5 | 5 | 3 |
| contradicting after | 0 | 0 | 0 |
| retired-concept flags after | 0 | 0 | 0 |

The raw candidate counts equal the objectstack-ai#20653 dev's 9 / 9 / 6 exactly.
After, both walks re-run at `aaee46778`: each after-candidate set is
exactly the before set minus the re-translated paths (0 added), the echo
count is still 0 (no new value equals its `en` leaf), and the
renamed-key flag is the same one. The penultimate-edit flags after are
the before set plus the 11 re-translated candidates (their last edit is
now this PR, after `en` moved), which is the expected shape.

### The judgment rule

As in PR objectstack-ai#20652 and PR objectstack-ai#20684. A leaf **contradicts** the current `en`
when a reader who acts on it would believe something about the current
form that `en` now says is false: `en` now denies what the leaf asserts;
the type was re-modelled, so the leaf describes a different thing; or
the leaf names a mechanism the section does not hold. Added detail,
narrowing, rewording, punctuation and title-casing are not
contradictions.

## Re-translated (12): before and after

**`agent.sections.capabilities.description`**, all three. ADR-0109
removed `agent.tools[]` (`e2616e0cf`); an agent's tools come from its
skills. `en`: `Skills and knowledge sources the agent can use.`

- ja-JP: 「エージェントが使用できるスキル、ツール、ナレッジソース。」 → 「エージェントが使用できるスキルとナレッジソース。」
- es-ES: 「Skills, herramientas y fuentes de conocimiento que puede usar
el agente.」 → 「Skills y fuentes de conocimiento que puede usar el
agente.」
- zh-CN: 「代理可使用的技能、工具与知识来源」 → 「代理可使用的技能与知识来源」

**`permission.sections.tab_and_row_level_security.description`**, all
three. `879ea1304` removed `PermissionSet.contextVariables`
(enforce-or-remove, zero consumers) and its form row. `en`: `Tab
visibility and RLS policies.`

- ja-JP: 「タブ表示、RLS ポリシー、述語評価用カスタムコンテキスト変数。」 → 「タブ表示と RLS ポリシー。」
- es-ES: 「Visibilidad de pestañas, políticas RLS y variables de contexto
personalizadas para evaluar predicados.」 → 「Visibilidad de pestañas y
políticas RLS.」
- zh-CN: 「导航可见性与共享规则」 → 「标签页可见性与行级安全策略」. The old value named the section
after **sharing rules**. In this repository 共享规则 is the zh-CN name of a
different mechanism, the `sys_sharing_rule` object (`plugin-sharing`)
and its own Setup entry `nav_sharing_rules`. The section holds
`tabPermissions` and `rowLevelSecurity`. The new value uses the bundle's
own words: 标签页 (this section's label), 行级安全策略 (the bundle's rendering of
an RLS policy, as in `object.fields["access.default"].helpText`).

**`report.fields.blocks.helpText`**, all three. The 9.0 single-form
cutover (ADR-0021, `18178454c`) made every report block a dataset-bound
sub-report; no block joins objects. `en`: `Dataset-bound sub-reports
(joined report only)`

- ja-JP: 「複数オブジェクトを結合(joined レポートのみ)」 → 「データセットにバインドされたサブレポート(joined
レポートのみ)」
- es-ES: 「Une varios objetos (solo informe joined)」 → 「Subinformes
vinculados a un conjunto de datos (solo informe joined)」
- zh-CN: 「joined 报表的联合查询块」 → 「绑定数据集的子报表(仅 joined 报表)」

**`email_template.sections.identity.description`**, ja-JP and es-ES.
`47a92f427` promoted `email_template` to a first-class metadata type and
rewrote its form. The Identity section now holds `name` / `label` /
`category` / `locale` / `description`, with no `id` and no content type,
and `IEmailService.sendTemplate` resolves the template by `name`. A
reader who copied the old sample would pass `template: id`. zh-CN
already said what `en` says. `en`: `Template identifier resolved by
IEmailService.sendTemplate({ template: name, locale, ... }).`

- ja-JP: 「識別子とコンテンツ型。id は sendTemplate({ template: id, ... }) で参照される。」 →
「IEmailService.sendTemplate({ template: name, locale, ... })
が解決するテンプレート識別子。」
- es-ES: 「Identificador y tipo de contenido. El id se referencia con
sendTemplate({ template: id, ... }).」 → 「Identificador de plantilla que
resuelve IEmailService.sendTemplate({ template: name, locale, ... }).」

**`report.sections.joined_blocks.label`**, zh-CN only. It was found by
the retired-concept scan, not by the since-last-edit instrument, because
`en` (`Joined blocks`) never moved. The zh-CN label said 关联对象 ("related
objects"), the pre-9.0 model in which blocks joined objects. The section
holds the `blocks` repeater of dataset-bound sub-reports. ja-JP 結合ブロック
and es-ES Bloques unidos render `en` literally and are unchanged.

- zh-CN: 「关联对象」 → 「joined 报表分块」 (分块 is `report.fields.blocks.label`, and
`joined` stays the report-type token, as in 仅 joined 报表).

## Stale but not contradicting (listed, left as written)

| path | locales | what `en` did |
|---|---|---|
| `object.sections.fields.description` | all three | "each row becomes a
column" became "each entry becomes a column" (`9f8ec35c8`). Rewording. |
| `hook.fields.condition.helpText` | all three | "Optional formula —
skip the hook when this evaluates to false" became "CEL predicate — the
hook runs only when TRUE" (`48efe915b`, which changed the row's editor
language from `javascript` to `expression`). The runtime gate is `if
(!conditionFn(ctx))` in `packages/objectql/src/hook-wrappers.ts`, whose
own docblock says "skip when the formula evaluates FALSE", and the leaf
makes no JavaScript claim. This is the closest call among the leaves
left alone. |
| `app.fields.defaultAgent.helpText` | ja-JP, es-ES | "AI agent" became
"Platform agent", with the `ask` / `build` defaults added. Narrowing and
detail. zh-CN already says what `en` says. |
| `report.sections.joined_blocks.description` | ja-JP, es-ES | "blocks
joined into a single report" became "dataset-bound blocks stacked into a
single report". Combined into one report is still true, and the `en`
label still says Joined blocks. The second closest call. zh-CN already
says what `en` says. |
| `report.sections.filter_and_chart.description` | all three |
"Report-level filters" became "Render-time scope filter". The filter is
still the report's. |
| `dashboard.fields.refreshIntervalSeconds.label` (renamed key) | all
three | The rename added the unit to the label. Each locale's `helpText`
on the same field already says seconds (自動更新(秒), Actualización
automática (segundos), 自动刷新间隔(秒)). |

Penultimate-edit flags, all read by hand. Every one already says what
`en` says:

- `en` moved EARLIER than the last edit:
`object.fields.isSystem.helpText`,
`object.fields.fields.reference.helpText`,
`object.fields.fields.trackHistory.helpText`,
`view.fields.filter.helpText`, `page.fields.type.helpText`,
`dashboard.fields.gap.helpText`, `action.sections.advanced.description`,
`action.fields.body.helpText`, `report.sections.basics.description`,
`report.fields.columns.helpText` and `dataset.fields.measures.helpText`
(all three locales); plus, zh-CN only,
`app.fields.defaultAgent.helpText`,
`report.sections.joined_blocks.description`,
`email_template.sections.identity.description` and
`email_template.fields.variables.helpText`.
- `en` moved IN the last-edit commit:
`object.fields.indexes.fields.helpText`,
`field.fields.precision.helpText`, `field.fields.maskingRule.helpText`,
`page.fields.variables.source.label`,
`app.sections.content.description`,
`app.sections.access_and_sharing.description` and
`permission.sections.identity.description` (all three locales).

## Dispatch hypotheses, measured

- **H1 held, and widened.** The targets are exactly the three translated
bundles against `en.metadata-forms.generated.ts`. Both known leaves are
candidates in all three locales, and the raw counts reproduce as 9 / 9 /
6 at `a8acee28d`. Judged, 4 / 4 / 3 contradict. Two more paths besides
the known two contradict: `report.fields.blocks.helpText` (all three)
and `email_template.sections.identity.description` (ja-JP, es-ES).
zh-CN's permission leaf contradicts for a different reason than the
known one (sharing rules). The retired-concept scan added one zh-CN
label.
- **H2 held: values only.** The companions work as they do for the
object bundles. `collectFilledFromHashes` records a `metadataForms` leaf
only while it is a byte copy of the current `en` or of a recorded
digest. Today that is 0 rows in all three locales, and a hand
translation records nothing. At `aaee46778`, `node
scripts/check-i18n-bundles.mjs --write --filter=platform-objects`
printed `regenerated` and rewrote all eleven bundle and companion files
on disk (every mtime moved), and `git status --porcelain` stayed empty.
The control that shows the tool really reads these leaves is in
"Known-positive controls" above. No echo-decision ledger row and no test
names any of the 12 paths (`git grep` over the ledgers).
`metadata-forms-vocabulary.test.ts` asserts group keys only.
- **H3 held: both checks ran, and their hits are reported separately.**
Renamed key: 1 / 1 / 1 hit
(`dashboard.fields.refreshIntervalSeconds.label`), stale but not
contradicting. Penultimate edit: 18 / 18 / 22 flags, 0 contradicting.

## Verification (all at `aaee46778`)

- Build: `turbo run build --filter=@objectstack/cli...
--filter=@objectstack/platform-objects... --concurrency=2`: 59/59 tasks,
`VERDICT command-exit 0`.
- `pnpm --filter @objectstack/platform-objects test`: 58 files, 942
tests passed. `pnpm --filter @objectstack/platform-objects typecheck`:
exit 0, `check:test-typecheck: OK`.
- The new values ship: the built `dist/metadata-translations/index.mjs`
and `index.js` were imported, and `MetadataFormsTranslations` was read
by path after `withSourceFallback`. In both, 12 of 12 re-translated rows
equal the HEAD source and differ from the base, and 9 of 9
unchanged-leaf controls equal both.
- Gates: `node scripts/pm/dispatch-gates.mjs --commands` (no paths)
derived 55 commands from the real diff, and all 55 exit 0. `--ran`
printed "55 derived famil(ies) accounted for — 55 run, 0 NOT-MEASURED (a
DERIVED zero — all 55 recorded an exit code and none of them is 3)".
`pnpm check:dual-build-cjs-loads` first refused with exit 3
(`PREREQUISITE NOT MET`: nine packages outside this diff had no
`dist/`). Those were built and the gate re-ran: exit 0.
- Named in the verdicts: `check:i18n` "OK (9 package(s) — all bundles in
sync, no undeclared authoring keys)"; `check:i18n-stale-fill` "OK (10
bundle set(s) — no new stale fills, 0 baselined)"; `check:nul-bytes`
exit 0.
- Lint, narrowed to the three edited bundles: `eslint --no-inline-config
--format json` read 3 files, 0 errors, 0 warnings.
`ESLint#isPathIgnored` answers `false` for all three, read from the
repo's own config. The config enables no type-aware linting (no
`parserOptions.project` or `projectService`, `eslint.config.mjs` states
it at line 328), so a change to string values in these files cannot move
the verdict on any other file. The full `pnpm lint` is left to CI.
- `origin/main` moved two commits past the base (`defc7f7b5`). Neither
touches a form declaration or a translation bundle, so the measurement
stands on it.

## Acceptance notes

- `dataset-panel-echo-decisions.test.ts`, the reason on the `include`
label row, cites "report.sections.joined_blocks.label is 关联对象" as a
precedent for the word 关联. After this PR that label reads joined 报表分块,
so the citation is out of date. It is prose: no assertion reads it, and
the `include` decision stands on its own, since 关联 keeps authored
precedents such as `sys_email.fields.related_object.label` 关联对象 in the
objects bundle. The ledger is outside this card's file surface and is
not edited here.
- zh-CN `page.sections.data_context.description` reads 关联对象与变量 ("related
objects and variables") against `en` "Record binding and page-local
state." It is loose but does not contradict: the bound record's object
is the related object, and the variables are the page-local state. No
instrument flags it, and it is left as written.
- The since-last-edit instrument cannot see a leaf that was wrong when
it was authored while `en` never moved. Instruments 5 and 6 cover only
the vocabularies they name. Instruments were run from the session
scratchpad.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01DEvba2nBuD4tWzfq8r8NFY)_

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/s tooling

Projects

None yet

2 participants