Skip to content

fix(plugin-webhooks,plugin-audit,plugin-security): re-translate the object leaves that contradict their current en source - #20684

Merged
objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-20653-plugin-bundle-leaves
Sep 29, 2026
Merged

objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-20653-plugin-bundle-leaves

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #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, i18n(platform-objects): authored ja-JP / es-ES / zh-CN metadata-forms leaves contradict their moved en source (the metadata-forms half of #20653) #20666, which remains open and is not touched here.

The measurement (base 6bff748bb, before any edit)

Instrument

PR #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 fix(platform-objects): re-translate the object leaves that contradict their current en source #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 #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 #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 #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. i18n(platform-objects): authored ja-JP / es-ES / zh-CN metadata-forms leaves contradict their moved en source (the metadata-forms half of #20653) #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

…dict their current en source

sys_webhook.fields.definition_json.help told a ja-JP / es-ES / zh-CN admin
that the JSON carries the full headers / auth / retry / payload config;
en says credentials are NOT stored there (signing_secret and headers_secret
are the encrypted fields). sys_webhook.description named an HTTP connector
plugin as the executor; en names the webhook auto-enqueuer and the shared
HTTP outbox. Values only.

Claude-Session: https://claude.ai/code/session_01XY5uCwTjZj7884yYtyur4H
Co-authored-by: Claude <noreply@anthropic.com>
…r current en source

sys_activity.fields.environment_id.label / .help still named the retired
v5.0 term project (ja-JP / es-ES / zh-CN), where en says Environment; the
v5.0 rename ships no alias. sys_audit_log.fields.user_id.label still called
the sys_user lookup the actor, where en relabelled it User when the object
gained its own `actor` principal field (ADR-0014 D2): es-ES showed two
fields labelled "Actor", ja-JP / zh-CN two synonyms. Values only.

Claude-Session: https://claude.ai/code/session_01XY5uCwTjZj7884yYtyur4H
Co-authored-by: Claude <noreply@anthropic.com>
…its current en source

The ja-JP / es-ES / zh-CN leaves still described positions as definitions
"for RBAC access control": the ADR-0090 P1 rename replaced the word role
and kept the old framing. en says positions distribute capability
(ADR-0090: capability = permission_set, distribution = position). The
es-ES leaf was edited after en moved, so only the penultimate-edit check
sees it. Values only.

Claude-Session: https://claude.ai/code/session_01XY5uCwTjZj7884yYtyur4H
Co-authored-by: Claude <noreply@anthropic.com>
…rity for the re-translated leaves

Claude-Session: https://claude.ai/code/session_01XY5uCwTjZj7884yYtyur4H
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 3 package(s): @objectstack/plugin-audit, @objectstack/plugin-security, @objectstack/plugin-webhooks, touching 4 documentable anchor(s).

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

  • content/docs/automation/webhooks.mdx (via sys_webhook (symbol, a field of const object esESObjects; a field of const object jaJPObjects; a field of const object zhCNObjects))
  • content/docs/deployment/environment-variables.mdx (via sys_position (symbol, a field of const object esESObjects; a field of const object jaJPObjects; a field of const object zhCNObjects))
  • content/docs/deployment/production-readiness.mdx (via sys_audit_log (symbol, a field of const object esESObjects; a field of const object jaJPObjects; a field of const object zhCNObjects))
  • content/docs/kernel/runtime-services/audit-service.mdx (via sys_activity (symbol, a field of const object esESObjects; a field of const object jaJPObjects; a field of const object zhCNObjects), sys_audit_log (symbol, a field of const object esESObjects; a field of const object jaJPObjects; a field of const object zhCNObjects))
  • content/docs/permissions/authorization.mdx (via sys_position (symbol, a field of const object esESObjects; a field of const object jaJPObjects; a field of const object zhCNObjects))
  • content/docs/permissions/delegated-administration.mdx (via sys_position (symbol, a field of const object esESObjects; a field of const object jaJPObjects; a field of const object zhCNObjects))
  • content/docs/permissions/positions.mdx (via sys_position (symbol, a field of const object esESObjects; a field of const object jaJPObjects; a field of const object zhCNObjects))
  • content/docs/permissions/record-view-auditing.mdx (via sys_audit_log (symbol, a field of const object esESObjects; a field of const object jaJPObjects; a field of const object zhCNObjects))
  • content/docs/permissions/system-context.mdx (via sys_position (symbol, a field of const object esESObjects; a field of const object jaJPObjects; a field of const object zhCNObjects))
  • content/docs/plugins/packages.mdx (via sys_activity (symbol, a field of const object esESObjects; a field of const object jaJPObjects; a field of const object zhCNObjects), sys_audit_log (symbol, a field of const object esESObjects; a field of const object jaJPObjects; a field of const object zhCNObjects))
  • content/docs/protocol/backward-compatibility.mdx (via sys_position (symbol, a field of const object esESObjects; a field of const object jaJPObjects; a field of const object zhCNObjects))
  • content/docs/protocol/kernel/config-resolution.mdx (via sys_audit_log (symbol, a field of const object esESObjects; a field of const object jaJPObjects; a field of const object zhCNObjects))
  • content/docs/ui/setup-app.mdx (via sys_activity (symbol, a field of const object esESObjects; a field of const object jaJPObjects; a field of const object zhCNObjects), sys_audit_log (symbol, a field of const object esESObjects; a field of const object jaJPObjects; a field of const object zhCNObjects))

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

  • content/docs/releases/index.mdx (via sys_audit_log (symbol, a field of const object esESObjects; a field of const object jaJPObjects; a field of const object zhCNObjects), sys_position (symbol, a field of const object esESObjects; a field of const object jaJPObjects; a field of const object zhCNObjects))
  • content/docs/releases/v13.mdx (via sys_position (symbol, a field of const object esESObjects; a field of const object jaJPObjects; a field of const object zhCNObjects))
  • content/docs/releases/v14.mdx (via sys_activity (symbol, a field of const object esESObjects; a field of const object jaJPObjects; a field of const object zhCNObjects), sys_audit_log (symbol, a field of const object esESObjects; a field of const object jaJPObjects; a field of const object zhCNObjects), sys_position (symbol, a field of const object esESObjects; a field of const object jaJPObjects; a field of const object zhCNObjects))
  • content/docs/releases/v15.mdx (via sys_position (symbol, a field of const object esESObjects; a field of const object jaJPObjects; a field of const object zhCNObjects))
  • content/docs/releases/v16.mdx (via sys_activity (symbol, a field of const object esESObjects; a field of const object jaJPObjects; a field of const object zhCNObjects), sys_webhook (symbol, a field of const object esESObjects; a field of const object jaJPObjects; a field of const object zhCNObjects))
  • content/docs/releases/v17/17-0.mdx (via sys_audit_log (symbol, a field of const object esESObjects; a field of const object jaJPObjects; a field of const object zhCNObjects))
  • content/docs/releases/v17/17-1.mdx (via sys_audit_log (symbol, a field of const object esESObjects; a field of const object jaJPObjects; a field of const object zhCNObjects), sys_position (symbol, a field of const object esESObjects; a field of const object jaJPObjects; a field of const object zhCNObjects))
  • content/docs/releases/v17/17-2.mdx (via sys_position (symbol, a field of const object esESObjects; a field of const object jaJPObjects; a field of const object zhCNObjects))
  • content/docs/releases/v17/17-5.mdx (via sys_audit_log (symbol, a field of const object esESObjects; a field of const object jaJPObjects; a field of const object zhCNObjects), sys_webhook (symbol, a field of const object esESObjects; a field of const object jaJPObjects; a field of const object zhCNObjects))

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

What this run could not see
  • 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 — 22 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 139bef809cda0084d56544bce1e6c58783da1a8e → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 139bef809cda0084d56544bce1e6c58783da1a8e

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

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review September 29, 2026 17:13
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 29, 2026
Merged via the queue into main with commit 7184436 Sep 29, 2026
36 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-20653-plugin-bundle-leaves branch September 29, 2026 17:33
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
… commits that decided them (objectstack-ai#20693)

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

## What changed

This is the fifth stage of the `domain:services` lane of the
dead-citation sweep. It covers
`packages/services/service-datasource/src/**` and nothing else. By the
seat's fresh census at the claim (`5894843429`), it is the largest
package in the lane that no in-flight work holds. Later stages cover the
other packages, so this PR says `Part of` and the card stays open.

Every comment or docblock site in scope that cited a tracker number
answering 404 has been rewritten in ruling C+D's form C (comment
5749154545 on objectstack-ai#19123), by the method of stages 1 to 4 (PR objectstack-ai#20609 as
`422db788a`, PR objectstack-ai#20626 as `b80ab579d`, PR objectstack-ai#20634 as `4d04b6be3`, PR
objectstack-ai#20658 as `9a4b2bb38`). That is **75 sites on 74 lines in 23 files,
covering 16 numbers**:

- 44 census sites (every census site this package has);
- 31 sites in test comments, which the census defers.

Each rewritten line now cites the commit in `origin/main` history that
decided what the line describes, and says in its own words what was
decided: **17 distinct shas**. `objectstack-ai#8696` was one card fixed in two halves,
so its lines cite the half they describe: the mysql DSN branch
(`72050cc47`) or the mongodb DSN branch (`90a12fb18`). `PR objectstack-ai#8588` was
itself a pull request, and it now cites its squash commit `3dede582b`.
No number in this package has an ADR or ruling record of its own in the
repository (a grep of `docs/adr/` for all 16 finds none), so every
anchor is a commit, per ruling C's order. No number was dropped.

Only comments changed. Every touched source file keeps its line count
(78 lines out, 78 in, over 23 files), so no line citation into these
files moves. 4 of those 78 lines hold no dead citation; they are reflow,
listed under Wordings below. No code token moves (see the guard below).

**No citation number is added.** Every tracker number on an added line
was already on the line it replaces: the only one is `objectstack-ai#12482`, which
resolves and stood on `datasource-connection-service.ts:101` before.
Over the whole diff, added minus removed is 0 or negative for every
number, and no number is new to the diff. No PR number stands on an
added line.

Thirteen dead sites are left on purpose, all of them test titles (see
the list below).

One more file: a `patch` changeset for
`@objectstack/service-datasource`, because the rewritten docblocks ship
(see Changeset below).

## Census: `service-datasource`, before and after

**Instrument (A1).** The gate's own `node
scripts/check-issue-citations.mjs --census --json`, read-only and
unchanged. The count below is its `allocated-but-absent` findings under
`packages/services/service-datasource/`. Each run counts as a reading
only because its board frontier equals the newest issue number, read by
a separate request just before and just after the run.

| reading | tree | board | whole-repo `allocated-but-absent` |
service-datasource sites | lines | files | numbers |
|---|---|---|---|---|---|---|---|
| before | base `6981abfd2`, run 2026-09-29T17:02:34Z to 17:06:27Z |
enumerated, 186 pages, frontier objectstack-ai#20684 (newest objectstack-ai#20684 before and after),
18,511 numbers | 1,318 | **44** | 43 | 9 | 14 |
| after | head `f5ec6bacd`, run 17:18:37Z to 17:22:33Z | enumerated, 186
pages, frontier objectstack-ai#20686 (newest objectstack-ai#20686 before and after), 18,513 numbers
| 1,274 | **0** | 0 | 0 | 0 |

The before count matches the seat's census at the claim (44 sites in 9
files, at `6bff748b`). The whole-repo drop is 44, exactly this diff's
census sites. The `resolves` tally is 32,909 in both runs, and
`resolves-as-pull-request` (1,984) and `cross-repo-unjudged` (994) did
not move either. The after run was taken on `f5ec6bacd`; the head
`265dc6861` adds only the changeset. No run was truncated or discarded:
all four enumerations in this stage (two census runs and the two
supplementary boards below) read 186 pages at the newest frontier.

**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` file under `service-datasource/src` (61 files). It uses one
board for both trees, enumerated by the gate's own `enumerateBoard` at
17:22:42Z (186 pages, frontier objectstack-ai#20686, equal to the newest).

| reading | citations | dead | src comment | test comment | src string |
test string |
|---|---|---|---|---|---|---|
| before, `6981abfd2` | 791 | **88** | 44 | 31 | 0 | 13 |
| after, `f5ec6bacd` | 716 | **13** | 0 | 0 | 0 | 13 |

Its src-comment column equals the census's 44, which is the control on
the second instrument. The 646 resolving and 57 pull-request citations
are the same in both readings, and the drop of 75 citations is exactly
the rewritten sites. An earlier board (17:07:08Z, frontier objectstack-ai#20685) gave
the same base reading. A third, raw reading (every `#` followed by
digits, judged against the same board, whatever surrounds it) finds 88
dead occurrences before and 13 after, and its residue equals the gate's
residue site for site.

## Per-number table

Sites and files count every dead occurrence in scope at the base
(comments and strings, tests included). `rewritten / left` counts the
sites rewritten and the sites left. Each anchor was read in its message
and diff, not only its subject.

| number | sites / files | rewritten / left | anchor: what it decided |
|---|---|---|---|
| `objectstack-ai#6268` | 6/4 | 6/0 | `68f5eccb1`: the libSQL/Turso host loader gets
one owner (`@objectstack/runtime`), and `MissingDriverPackageError`
becomes one class across both hosts, because `serve.ts` decides fatality
with `instanceof`. The cli and runtime stages' anchor |
| `objectstack-ai#6345` | 9/5 | 9/0 | `e2798fab7`: one driver vocabulary; `mongo`
renamed to `mongodb`, `turso` made a full builtin with a contract, and
this factory's dispatch made exhaustive. The spec stages' anchor |
| `objectstack-ai#8588` | 1/1 | 1/0 | `3dede582b`: `external.credentialsRef` (and only
it) allowed on `schemaMode: 'managed'`; `objectstack-ai#8588` was that pull request,
and this is its squash commit |
| `objectstack-ai#8696` | 13/4 | 10/3 | two halves: `72050cc47`, a bound
`credentialsRef` reaches the mysql client on the DSN branch as `{ uri,
password }` (5 sites); `90a12fb18`, the mongodb DSN branch carries it in
`options.auth` beside an unmodified url (5 sites). The spec stage's
anchor for the mongo half |
| `objectstack-ai#8873` | 8/3 | 7/1 | `096106522`: a bound `credentialsRef` reaches
the postgres SERVER on the DSN branch; `connectionString` is dropped and
`pg` gets its own parse of the url with the credential attached. The
spec stage's anchor |
| `objectstack-ai#8874` | 9/2 | 7/2 | `d70428ae7`: a declared `ssl` reaches the mysql
client on both branches, in the spelling `mysql2` accepts (`{}`, never
`true`). The spec stage's anchor |
| `objectstack-ai#8876` | 1/1 | 1/0 | `d634e665b`: `urlUserinfoUsername` exported, the
username half of the shared URL userinfo grammar. The spec stage's
anchor |
| `objectstack-ai#9040` | 4/3 | 2/2 | `24206416a`: a credential in the mongo options
passthrough (`config.options.auth.password`) is refused at publish. The
spec stage's anchor |
| `objectstack-ai#9041` | 2/2 | 2/0 | `d491625c1`: a bound `credentialsRef` with a
user-less mongo `config.url` is refused at the one door that sees both
halves. The spec stage's anchor |
| `objectstack-ai#10537` | 3/2 | 3/0 | `e634ecf6a`: `POST /external/validate` scoped
to the URL's datasource; it adds `validateDatasource`. The rest and
runtime stages' anchor |
| `objectstack-ai#10962` | 5/2 | 4/1 | `29d067646`: one live introspection per
datasource per validation sweep, memoised per call and never per
instance (its message names `objectstack-ai#10962`) |
| `objectstack-ai#11166` | 5/1 | 4/1 | `735f5c709`: an unreachable remote is the new
`unreachable` diff kind, not `missing_table`. The runtime stage's anchor
|
| `objectstack-ai#12010` | 9/5 | 8/1 | `77b91bdb4`: `ConnectionEngineLike` derived
from the engine contract, and `registerDriver` stops promising it
accepts any value. The runtime stage's anchor |
| `objectstack-ai#12248` | 1/1 | 1/0 | `8425c17cc`: the ruled engine members adopted
onto `IDataEngine`, the datasource-lifecycle trio among them. The spec
stage's anchor |
| `objectstack-ai#12943` | 3/2 | 3/0 | `090f2302e`: the guarded optional-driver loads
declared as optional peers of this package. The cli and runtime stages'
anchor |
| `objectstack-ai#13279` | 9/4 | 7/2 | `6a180e42d`: a failed permission-store read
raises `AuthzStoreUnavailableError` (503) instead of reading as zero
grants; its message carries the 2026-08-30 ruling, and it moved
`driver-error-classification.ts` into `@objectstack/types`. The anchor
of stage 2 and of the rest, runtime and types stages |

Every cited sha matches exactly one commit (`git rev-parse
--disambiguate`, count 1 for each of the 17), and every one is an
ancestor of the base (`merge-base --is-ancestor`, exit 0 for all 17; the
history is complete, `--is-shallow-repository` false, 15,110 commits).
Where an earlier stage already anchored a number, this stage reuses that
anchor after checking it against this package's lines. New to the sweep
here: `72050cc47` (the mysql half of `objectstack-ai#8696`), `3dede582b` and
`29d067646`.

## Wordings to check

- **`objectstack-ai#8696`'s two halves.** The mysql arm's lines
(`default-datasource-driver-factory.ts:718`, `:824`, and
`bound-secret-dsn-branches.test.ts:4`, `mysql-dsn-ssl.test.ts:189`,
`:263`) cite `72050cc47`; the mongo arm's lines
(`default-datasource-driver-factory.ts:926`, `:954`, `:1243`,
`datasource-credential-migration.ts:182`, and the heading
`bound-secret-dsn-branches.test.ts:72`, 「the mongodb half, added
second」) cite `90a12fb18`.
- **A referent, `datasource-connection-service.ts:95-96`.** 「the
inventory that filed that card」 lost its referent with the number, so it
now says 「the inventory that filed its card」, the card behind
`77b91bdb4` (1 reflow line).
- **`datasource-connection-service.ts:100-101`.** 「objectstack-ai#12248 adjudicated
all three onto IDataEngine」 became 「Commit 8425c17 adopted all three
onto IDataEngine per the ruling」: the ruling decided and the commit
carried it out, as its changeset says (1 reflow line).
- **What the card described, `default-datasource-driver-factory.ts:412`
and `mysql-dsn-ssl.test.ts:40`.** 「the one objectstack-ai#8874 describes as honouring」
became 「the one commit d70428a's card describes as honouring」, since
the words quote the card, not the commit.
- **A cross-reference, `default-datasource-driver-factory.ts:736`.**
「the falsy-value note under objectstack-ai#8874 below」 points at the heading at
`:767`, which now carries `commit d70428a`, so the pointer names the
same anchor.
- **A future tense made past, `postgres-dsn-bound-secret.test.ts:223`.**
「the authoring door (objectstack-ai#9041), which this card lands before」 became
「(commit d491625), which landed after this pin」. `096106522` (this
file's commit) is an ancestor of `d491625c1`, both on 2026-08-16.
- **`external-datasource-service.test.ts:444`.** 「The card's measured
defect」 became 「Its card's measured defect」, the card behind
`735f5c709`; `:588` 「the pre-objectstack-ai#10537 route」 became 「the route … before
commit e634ecf」.
- **`datasource-admin-service.test.ts:674`.** 「Before PR objectstack-ai#8588」 became
「Before commit 3dede58」, the squash commit of that pull request, which
answers 404.
- **Reflow, 4 lines with no dead site** (every file keeps its line
count): `datasource-connection-service.ts:96`, `:101`,
`default-datasource-driver-factory.ts:825`, `:826`.

## The 13 sites left

- **Test titles, 13 sites.** `describe` / `it` titles, which are string
tokens, left as stages 1 to 4 left theirs:
`admin-routes-authz-outage-envelope.test.ts:158` (`objectstack-ai#13279`);
`admin-routes-tenancy-posture-admission.test.ts:557` (`objectstack-ai#13279`);
`bound-secret-dsn-branches.test.ts:136`, `:244` (`objectstack-ai#8696`);
`connection-engine-like-contract.test.ts:21` (`objectstack-ai#12010`);
`datasource-config-redaction.test.ts:406` (`objectstack-ai#9040`);
`datasource-credential-migration.test.ts:226` (`objectstack-ai#9040`);
`external-datasource-service.test.ts:453` (`objectstack-ai#11166`), `:690` (`objectstack-ai#10962`);
`mysql-dsn-ssl.test.ts:165`, `:324` (`objectstack-ai#8874`), `:260` (`objectstack-ai#8696`);
`postgres-dsn-bound-secret.test.ts:160` (`objectstack-ai#8873`).
- There is no operator string, assertion message, generated header or
quoted ruling carrying a dead number in this package. The verbatim
maintainer quotations in scope (「同意」 and 「同意所有」, on 8 lines) carry no
dead number and are untouched.

## Mechanical guard: no code token moves

The guard compares the TypeScript parser's leaf nodes, with comments as
trivia and JSDoc nodes never visited, base `6981abfd2` against head.
Template literals are therefore read in context. It ran over all 23
touched `.ts` files.

- Real run: 24,055 base leaf tokens, **0 files with a token change**
(exit 0).
- Comment control in `default-datasource-driver-factory.ts` (`Lazy +
caught exactly like` to `Lazy and caught exactly like`): 0 files
changed, as expected (exit 0).
- Positive control, a code token added in
`default-datasource-driver-factory.ts` (`const url =
resolveTursoUrl(spec);` given a trailing `?? undefined`): DIFFER (exit
1).
- Positive control, one digit changed inside a kept test title
(`mysql-dsn-ssl.test.ts:165`): DIFFER (exit 1).

Every mutation went through `scripts/ablation-replace.mjs`, and each
landed (anchor 1 to 0, blob changed). Each restore was proven
byte-identical to the HEAD blob (`8c164aa6178f`, `2feeaeb0411d`), with
`git diff HEAD` empty and a clean tree afterwards. A first draft of the
guard used the bare scanner, which loses template context and reported
token changes inside comments; it was replaced by the parser walk before
any reading was taken from it.

## Changeset

This change ships bytes, so a `patch` changeset for
`@objectstack/service-datasource`
(`.changeset/20596-service-datasource-provenance-anchors.md`) is
included. It says only that the provenance comments were re-anchored, in
stages 3 and 4's words.

Measured on the built package (A3): `files[]` is `dist`, `README.md` and
`CHANGELOG.md`. After the build, the rewritten comments reach `dist`:
`6a180e42d`, `e2798fab7` and `e634ecf6a` once, and `29d067646` three
times, in each of `dist/index.d.ts`, `index.d.cts`, `index.js` and
`index.cjs`; `68f5eccb1` 4 times, `090f2302e` twice, and `77b91bdb4` and
`8425c17cc` once each, in both declaration files. Positive control: the
unchanged line 「`registerDatasourceDef`, `markDatasourceUnavailable`,」
beside the shipped rewrite at `datasource-connection-service.ts:95` is
found once in `index.d.ts`. A never-written negative phrase appears
nowhere in `dist`. No dead number of the 16 is left anywhere in `dist`.

## Gates (head `265dc6861`)

- **Citation judging, as CI runs it:** `pnpm check:issue-citations`
(self-test) exits 0. `node scripts/check-issue-citations.mjs` exits 0:
the diff-scoped run judged 1 citation (`objectstack-ai#12482`), and it resolves.
- **Doc authoring:** `pnpm check:doc-authoring` exits 0, with the
sibling-package prose ids at their baseline and no growth.
- **Derived gates:** `node scripts/pm/dispatch-gates.mjs --commands
--repo objectstack-ai/objectstack` at `265dc6861` derived 63 commands:
all 54 derived at dispatch, plus `check:duration-unit-keys`,
`check:dispatcher-error-vocabulary`, `check:engine-double-contract`,
`check:logger-receiver-detach`, `check:objectql-double-limit`,
`check:query-options-erasure`, `check:type-check-coverage`,
`check:type-check-debt` and `check:where-matcher`. Each ran with its
exit code captured before any pipe, and all 63 exit 0. `--ran`, fed each
command with its exit code, reports 63 run, 0 NOT MEASURED (a derived
zero), 0 unrun, and exits 0. A full `turbo run build` of `./packages/*`
and `./packages/*/*` ran first under the shared verify lock (71 of 71
tasks, exit 0), so no gate hit an unbuilt workspace.
- **Roster families the derivation lists outside its commands** (their
rosters sit in directories this diff touches): `node
scripts/check-changeset-fixed.mjs`, `pnpm check:authz-resolver`, `pnpm
check:error-code-casing` and `pnpm check:filter-alias-parity`, each exit
0.
- **Tests and typecheck, under the verify lock:**
- `pnpm --filter @objectstack/service-datasource test`: 34 files pass
and 693 tests pass. That is every test file in the package, the 14
touched ones included.
- `pnpm --filter @objectstack/service-datasource typecheck` exits 0. Its
`tsconfig.json` includes all of `src`, and `--listFiles` shows all 61
files under `src/`, the 34 test files included, and all 23 touched files
in the program.
- **Lint, as a proven narrowing:** `eslint --no-inline-config --format
json` over the 23 touched `.ts` files gives 23 files, 0 errors and 0
warnings. All 23 are in eslint's own population (`isPathIgnored` is
false for each). `eslint.config.mjs` never enables type-aware linting
(no `parserOptions.project`, as its own lines 327-328 state), so a
comment edit here cannot move the verdict on any untouched file. The
repo-wide `pnpm lint` is CI's run.
- **Control bytes:** `pnpm check:nul-bytes` exits 0, and a raw scan of
the 24 changed files for control bytes finds none.

## Acceptance notes

- **The gate-invisible spellings, grepped as the claim asked.**
`CITATION_RE` refuses a hyphen after the digits and a `/` before the `#`
(objectstack-ai#20636). In this package there is no `#N-word` spelling at all. There
are 7 `#A/#B` lines (`admin-routes.ts:28`,
`datasource-route-ledger.ts:159`, `turso-driver-config.ts:132`,
`external-introspection-seam.test.ts:14`, `:102`, `:163`,
`turso-bound-secret-authoring.test.ts:8`), and every second number on
them is live: `objectstack-ai#10998`, `objectstack-ai#4251` and `objectstack-ai#4249` are issues, and `objectstack-ai#8078`,
`objectstack-ai#4176` and `objectstack-ai#4202` are pull requests. So nothing there needed
rewriting. The raw scan above, which sees both spellings, agrees.
- **The census instrument did not truncate in this stage.** Four
enumerations read 186 pages each at the newest frontier.
- **Anchors the next stages can reuse**, each checked here: `objectstack-ai#13279` →
`6a180e42d`; `objectstack-ai#12010` → `77b91bdb4`; `objectstack-ai#6345` → `e2798fab7`; `objectstack-ai#6268` →
`68f5eccb1`; `objectstack-ai#12943` → `090f2302e`; `objectstack-ai#8696` → `72050cc47` (mysql) or
`90a12fb18` (mongodb); `objectstack-ai#8873` → `096106522`; `objectstack-ai#8874` → `d70428ae7`;
`objectstack-ai#10962` → `29d067646`.
- **Base.** The branch is 5 commits behind `origin/main` (`14f80e239`,
read at 17:27Z). None touches `service-datasource`, `scripts/` or
`.changeset/config.json`, so there was no merge.

---
_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