Skip to content

fix(mcp, email, knowledge, queue, sms, storage, trigger, i18n): runtime strings state each decision in words instead of a tracker number (stage 2) - #21311

Merged
objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/issue-20751-services-strings-stage2
Oct 2, 2026
Merged

objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/issue-20751-services-strings-stage2

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Part of #20751
Clause-②: no

Stage 2 of the domain:services lane under the maintainer's A / A ruling (5902360492): the connector-mcp, plugin-email, service-storage, service-knowledge, service-queue, service-sms, trigger-record-change and service-i18n strings. The card stays open for the later stages, so this PR carries no closing keyword. Text only: no status, error code, field, route, export or control flow moves (AST-skeleton proof below, 14 of 14 files SAME).

What this does

These eight packages' refusals, warnings, log lines, route-ledger notes and the built-in change-email notice template description (in all four locales) sent the reader to a tracker number for the reason behind them. In form D, as stage 1 applied it, the number goes. Where the sentence already said what was decided, only the citation goes. Where it leaned on the number, it now says the decision in words.

All 22 ledgered occurrences in the eight packages (claim 5944824425), re-derived from the ledger on origin/main at 383a00c7, not from the card's table: plugin-email 6, service-storage 4, service-knowledge 3, connector-mcp 2, service-queue 2, service-sms 2, service-i18n 2, trigger-record-change 1. The card's table reads the same 22. They sit in 21 string sites (one storage note carried two ids).

Rewritten in words

Author- and administrator-visible text first, log lines last. Every cited card was read through REST (body and every comment) before its string was rewritten.

Where (head line) Cited The text now says Decision read from
connector-mcp mcp-provider.ts:142, allowlist-miss refusal (fatal at boot, skipped and logged on a runtime publish) 3055 "a stdio transport launches a local process, so stack metadata may only name a command the host's own code allows" card body: declarative stdio is default-deny and allowlisted by host code, because metadata (a runtime Studio publish included) could otherwise launch commands on the server
connector-mcp mcp-provider.ts:149, default-deny refusal 3055 "or use an http transport, which this policy does not gate (ADR-0024 §4)" card body: http transports stay unaffected
plugin-email templates/auth-templates.ts:573, :601, :630, :660, the auth.email_change_notice description seeded into sys_email_template (en-US, zh-CN, ja-JP, es-ES) 8019 "so a hijacked session cannot move the account identity unannounced", and the same decision in each locale maintainer ruling 5271034556: notify the old address, do not gate
plugin-email internal-header-readback.ts:128, thrown when the engine cannot dereference headers_json 8149 "a missing header does not announce itself, so the send would succeed while silently deviating from what was authored" the card adopts its blocker's landed remedy; unlock comment 5277987462, point 4 (fail closed, because a missing header is not self-announcing)
service-queue db-queue-adapter.ts:93, thrown when sys_job_queue loses its retention declaration 5179 "the one platform reaper sweeps completed rows by that declaration, and a sweeper here would be a second copy of the window, free to drift from the first" ACCEPT 5176594402: declarative retention swept by the one platform reaper (ADR-0057 §3.3), read from the declaration rather than copied
trigger-record-change record-change-trigger.ts:240, array-trigger warning to the flow author 3457 "multi-event arrays are deferred until two independent projects need a combination other than created-or-updated" closing comment 5076318442 (not planned; its restart clause is two independent real projects)
service-knowledge knowledge-service.ts:321, no-identity retrieval warning 2981 "a missing identity is not a grant of authority, so retrieval fails closed rather than searching the whole corpus unscoped" card body: fail closed on a missing identity; pass-through only for an explicit system context (ADR-0096)
service-knowledge knowledge-service-plugin.ts:221, predicate-write warning 4672 "the lifecycle reap guard de-indexes those rows before they are deleted" maintainer ruling 5194621834 (plan C)
service-queue queue-service-plugin.ts:160, rejected-floor error 5195 "that floor is what makes it refuse an override below the idempotency window" ACCEPT 5177937633: an override below a consumer's registered floor is refused
service-sms sms-daily-quota.ts:327, unreadable-counter warning 2814 "a quota the platform cannot count must not refuse the one-time codes users sign in with" card body, ask 4: a counter-store failure fails open with a warning, and the quota gate must not take sign-in down
service-storage attachment-lifecycle.ts:529, reclamation-gate line 4797 "deleting bytes cannot be undone, so it waits for a verified migration with no deviation on record, while reversible work carries on" maintainer ruling 5202265919 / 5203575604 (conditional B: withdraw the irreversible authority, keep the reversible)
plugin-email email-service.ts:925, over-limit attachments line 5172 "queueable through the storage capability, which holds it outside the row while the row keeps a reference and the attachment's audit metadata" maintainer ruling in the card body (content through storage; the row keeps the reference and permanent audit metadata)
service-storage storage-route-ledger.ts:113, download-URL row note (guard data, not shipped) 3584 "The dispatcher ledger points here because this protocol, not a dispatcher route, is the canonical storage surface the SDK speaks" landing commit 0bab8bb45 (the service-storage protocol stays canonical), and the later retirement of the dispatcher bridge recorded in packages/runtime/src/route-ledger.ts

Citation only (the sentence already stated the decision)

  • service-knowledge knowledge-service-plugin.ts:220 (4639, "A bulk event carries a count, not records, ... cannot be repaired from the event stream": the honest aggregate event).
  • service-sms sms-plugin.ts:170 (2814): the counter store's subject is now "daily SMS send quota". It is prose only, interpolated into the store's bind and fallback log lines, never a key.
  • service-i18n i18n-route-ledger.ts:98, :100 (3636, "The client now sends the path form the spec declares"; "it now sends both the object and the locale in the path", checked against i18n.getTranslations / i18n.getFieldLabels in packages/client/src/index.ts).
  • service-storage storage-route-ledger.ts:113 (second half) and :119 (3689, "moved into the declared envelope"; "retired when every storage route moved to the declared envelope, ok being a private second word for success").

Translations

The change-email notice description is the only string here with locale variants. Each of the zh-CN, ja-JP and es-ES rows carries the same decision as the en-US row ("使被劫持的会话无法在原邮箱不知情的情况下转移账号身份", "乗っ取られたセッションが元のアドレスに知られないままアカウントの識別情報を移せないようにします", "para que una sesión secuestrada no pueda trasladar la identidad de la cuenta sin aviso") and no number. They are template rows, not i18n bundle keys, so no bundle or digest moves. check:i18n reads "all bundles in sync". The built-in seeder upserts by name and locale on every boot, so a deployment's existing rows take the new description.

Ledger (scripts/doc-authoring-prose-id.baseline.json)

Recomputed with node scripts/check-doc-authoring.mjs --census-ledger (exit 0, no growth refusal) into a scratch file, then copied into place. The diff deletes 44 lines and adds none: exactly the 14 file blocks of the eight packages. A scripted comparison of every other key: 0 moved, 0 added.

before (383a00c7) after
plugin-email 6 occurrences, 3 pairs, 3 files 0
service-storage 4 occurrences, 3 pairs, 2 files 0
service-knowledge 3 occurrences, 3 pairs, 2 files 0
connector-mcp 2 occurrences, 1 pair, 1 file 0
service-queue 2 occurrences, 2 pairs, 2 files 0
service-sms 2 occurrences, 2 pairs, 2 files 0
service-i18n 2 occurrences, 1 pair, 1 file 0
trigger-record-change 1 occurrence, 1 pair, 1 file 0
whole ledger 283 occurrences, 205 pairs, 86 files 261 occurrences, 189 pairs, 72 files

pnpm check:doc-authoring at the head: "236 pinned site(s) across 72 file(s), 86149 string(s) read in 1248 parsed source(s), no growth, no burn-down unrecorded" (the census before the change read 257 sites). No gate is added or loosened; scripts/check-doc-authoring.mjs is untouched.

Changeset

.changeset/20751-services-strings-stage2-state-the-decision.md: patch for @objectstack/connector-mcp, @objectstack/plugin-email, @objectstack/service-knowledge, @objectstack/service-queue, @objectstack/service-sms, @objectstack/service-storage and @objectstack/trigger-record-change.

Measured after building at the head:

  • Each new sentence named above is in its package's built output (2 files each; plugin-email's four descriptions read back from the built module's exported AUTH_EMAIL_CHANGE_NOTICE_TEMPLATES, all four id-free).
  • A TypeScript scan of every string literal and template text in the eight packages' built .js/.mjs/.cjs finds 0 tracker ids. Control: the same scan reads 86 in plugin-security's built output and 66 in service-automation's.
  • @objectstack/service-i18n is deliberately NOT in the changeset. Its only change is the route ledger, which no entry imports: I18N_ROUTE_LEDGER and the new note are in 0 files of its dist/ (positive control: registerRoutes in 4). The same holds for service-storage's ledger (STORAGE_ROUTE_LEDGER in 0, control reap guard: kept in 2), so that package's patch is for attachment-lifecycle.ts alone.

Text-only proof

A TypeScript-AST skeleton of each changed non-test .ts file: every string literal and template text is a placeholder, a run of adjacent string operands of a + chain is one string (embedded expressions kept in order), identifiers and numbers are kept, and comments are never read. The walk visits every child (no early exit). 383a00c7 against the head: 14 of 14 SAME, node counts identical per file. Control on scratch copies of mcp-provider.ts: renaming one identifier reads DIFF; changing only a string's text reads SAME; re-splitting one template into two + operands reads SAME.

Pins

Four assertions named a tracker id and now name the sentence that replaced it:

  • service-knowledge __tests__/event-sync-data-events.test.ts:166, :167: toContain('cannot be repaired from the event stream') and toContain('the lifecycle reap guard de-indexes those rows before they are deleted') instead of the two ids. The third half-pin (application-level predicate writes are not) is unchanged, so both halves of the honest line stay pinned.
  • trigger-record-change array-form-refusal-end-to-end.test.ts:116 and record-change-trigger.test.ts:183: toMatch(/multi-event arrays are deferred until/) instead of the id. The label now reads "states the standing decision".

No code or status assertion was touched; none of these strings is a coded refusal. Every other old fragment was searched repo-wide: no other test, doc or fixture quotes them.

Tests

All at head 7bcf2f613, through scripts/pm/os-verify-lock.sh, each VERDICT command-exit 0:

  • Build: turbo run build over the eight packages and their dependency closure (37/37), then turbo run build --filter=./packages/* --filter=./packages/*/* (71/71) for the dist-reading gates.
  • vitest run --maxWorkers=2 per package: plugin-email 31 files, 510 tests; service-storage 41 files, 629 tests; trigger-record-change 10 files, 101 tests; service-queue 5 files, 77 tests; service-i18n 5 files, 74 tests; service-sms 5 files, 74 tests; service-knowledge 4 files, 44 tests; connector-mcp 3 files, 23 tests. All passed. The three re-pinned files were also run with the verbose reporter, which names the three cases holding the four re-pinned assertions as passed.
  • typecheck for all eight, exit 0, including check:test-typecheck: OK where the package wires it.

Gates

  • node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands (no paths) at 7bcf2f613: 72 commands over 19 paths, run one at a time from the worktree, each exit code recorded before any pipe. --ran: "72 derived famil(ies) accounted for — 72 run, 0 NOT-MEASURED (a DERIVED zero — all 72 recorded an exit code and none of them is 3)".
    • check:dual-build-cjs-loads and check:i18n first answered PREREQUISITE NOT MET (exit 3, only the closure was built). After the full package build they and the other dist-reading gates were re-run, all exit 0: 105 require entry points across 66 packages load; "all bundles in sync"; check:dts-closure 71 built packages, 167/167 declaration files; check:sourcemap-no-sources-content 68 packages, 522 maps; check:lean-entry-closure and check:published-files green.
    • check-issue-citations: "no issue citations added against 383a00c (14 file(s) read)"; check:nul-bytes: OK, 9609 files, no raw ASCII control bytes; check:i18n-stale-fill: "no new stale fills"; check:type-check-debt: "none above its recorded number"; check:engine-double-contract: OK.
  • Outside the derived set, all exit 0: the eleven declared wide-population families (check:init-service-contract, check:live-db-isolation, check:meta-type-normalized, check:optional-error-sink, check:resume-authority-declared, check:route-envelope, check:runner-env-posture, check:settings-bind-window, check:startup-registry-verdict, check:verify-stand-in, check:wildcard-fallthrough) and the artifact-roster families whose roster sits under one of this PR's directories (check-changeset-fixed, check-published-list-mirrors and its self-test, check:authz-resolver, check:console-injection, check:error-code-casing, check:filter-alias-parity, check:published-readme-exports, check-dts-references --self-test). The path-scheduled CI jobs and the type-check lanes are CI's.
  • pnpm lint (eslint . --no-inline-config, repo-wide, not narrowed) at 7bcf2f613: exit 0, 128 s under the lock.
  • No gate run appended anything to AGENTS.md; the tree stayed clean throughout.

Acceptance notes

  • origin/main moved 8 commits to 393ae878 after the branch point. None touches any of this PR's 19 paths or any of the eight packages, and the ledger is not among them, so the recomputed ledger stands on the merged tree. The branch was not merged with main; the queue rebuilds it there.
  • Comments and test titles in these packages still cite numbers: a comment is the sanctioned home for an internal anchor, and the ledger does not read test files. Two conformance tests carry an id in their failure text (storage-route-ledger.conformance.test.ts:74, i18n-route-ledger.conformance.test.ts:91); they are test files, outside the ledger and this stage.
  • packages/lint/src/validate-flow-trigger-readiness.ts:557 carries the same 3457 deferral in the lint hint, and its test pins the id (validate-flow-trigger-readiness.test.ts:738). packages/lint is not in this stage. The trigger warning now uses the lint hint's own phrase ("multi-event arrays are deferred"), so the two read alike once that stage lands.
  • The remaining packages of this lane's share are later stages.

Generated by Claude Code

…me strings state each decision in words instead of a tracker number

Stage 2 of the services lane: every ledgered tracker-number occurrence in
connector-mcp, plugin-email, service-storage, service-knowledge,
service-queue, service-sms, trigger-record-change and service-i18n (22
occurrences in 21 string sites) is gone. Where the sentence already said
what was decided, only the citation goes; where it leaned on the number,
it now says the decision in words. The change-email notice template
description changes in all four locales.

The doc-authoring ledger is recomputed with --census-ledger: the 14 file
blocks of these packages are deleted and no other row moves. Four test
assertions that pinned an id now pin the sentence that replaced it. Text
only: no status, error code, field, route or control flow moves.

Claude-Session: https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Oct 2, 2026
@github-actions

github-actions Bot commented Oct 2, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 8 package(s): @objectstack/connector-mcp, @objectstack/plugin-email, @objectstack/service-i18n, @objectstack/service-knowledge, @objectstack/service-queue, @objectstack/service-sms, @objectstack/service-storage, @objectstack/trigger-record-change, touching 10 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/plugins/plugin-email/src/templates/auth-templates.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

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

  • content/docs/api/plugin-endpoints.mdx (via /labels/:object (route, a path literal in note))
  • content/docs/kernel/services-checklist.mdx (via /labels/:object (route, a path literal in note))
  • content/docs/permissions/system-context.mdx (via applyPermissionFilter (symbol, a method of class KnowledgeService))

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

  • content/docs/releases/v15.mdx (via applyPermissionFilter (symbol, a method of class KnowledgeService))
  • content/docs/releases/v17/17-0.mdx (via /labels/:object (route, a path literal in note))

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

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/plugins/plugin-email/src/templates/auth-templates.ts) — pages documenting those are invisible to this run
  • 2 name(s) were too generic to anchor anything (single lowercase words)
  • 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 — 17 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 393ae878d3d52fe843c56b4621c004b934dcf853 → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 393ae878d3d52fe843c56b4621c004b934dcf853

⚠️ 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 393ae878d3d52fe843c56b4621c004b934dcf853 → 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 October 2, 2026 04:26
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 2, 2026 04:26
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 2, 2026
Merged via the queue into main with commit 6091136 Oct 2, 2026
36 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-20751-services-strings-stage2 branch October 2, 2026 04:48
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
…rds instead of a tracker number (stage 3) (objectstack-ai#21346)

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

**Stage 3 of the `domain:services` lane under the maintainer's A / A
ruling (5902360492): the `service-datasource` and `plugin-approvals`
strings.** The card stays open for the later stages, so this PR carries
no closing keyword. Text only: no status, error `code`, field, route,
export or control flow moves (AST skeleton reads SAME for 12 of 12
changed sources, below).

## What this does

The two packages' refusals, warnings, log lines, datasource route-ledger
notes, the generated object-draft comment, and the `sys_approval_action`
/ `sys_approval_delegation` field help and description (with their
`es-ES`, `ja-JP` and `zh-CN` variants) sent the reader to a tracker
number for the reason behind them. In form D, as stages 1 and 2 applied
it, the number goes. Where the sentence already said what was decided,
only the citation goes. Where it leaned on the number, it now says the
decision in words.

All 34 ledgered occurrences in the two packages (claim `5945630892`),
re-derived from the ledger on `origin/main` at `1371dc98` and again on
the merged tree at `222ecc27`: `service-datasource` 9 (9 pairs, 3
files), `plugin-approvals` 25 (23 pairs, 7 files). They sit in 32 string
sites. None of them is in the three excluded files
(`datasource-secret-binder.test.ts`, `sys-approval-token.object.ts`,
`approval-token-internal-hash.integration.test.ts`); none of those files
is touched.

### Rewritten in words

Author- and administrator-visible text first, log lines last.

| Where (head line) | Cited | The text now says | Decision read from |
|---|---|---|---|
| `plugin-approvals` `sys-approval-action.object.ts:165`, `via_override`
help, and its three locale leaves | 3424 | "admitted to this action only
by the privileged override, which lets a platform or organization admin
act on any pending request so that one nobody in its slate can decide
never stays stuck" | the card's fix comment (an admin escape hatch
rather than refusing empty approvers at creation) and the
`isOverrideActor` contract it landed: a platform or tenant admin may
always act on a PENDING request |
| `service-datasource` `datasource-credential-migration.ts:269-272`, the
migration refusal `reason` | 8078, 8081 | "an alias spelling from before
inline credentials were refused at publish, which no connection builder
reads, or ... turso's `encryptionKey`, which has no secret slot of its
own: the one slot carries the `authToken`" | 8078 (the spec half:
`password` / `authToken` declared unwritable, the old alias spellings
carry the refusal, `encryptionKey` left writable because neither ruled
mechanism can carry it) and 8081 item 4 (the binder injects exactly one
secret slot; a second slot is undecided) |
| `service-datasource` `external-datasource-service.ts:925`, the
generated draft's remote-primary-key comment | 10997 | "For a COMPOSITE
key a driver's introspection can report only the first column, so treat
the list as a lower bound" | 10997 repaired the SQLite arm (every column
whose `pk` ordinal is above zero, ordered by key position, PR 11104), so
the old "some drivers report only the first column" named a defect that
is gone in-tree; the caveat stays because the generator renders whatever
key it was given and the driver population is open (the
`external-datasource-service.ts` docblock records both) |
| `plugin-approvals` `approval-service.ts:1765`, the `queue` approver
warning | 3508 | "because the platform has no ownership queue to expand
it from (the type is deprecated and no longer offered for authoring).
Route this step to a team, department or position instead." | the card's
resolution: deep deprecation rather than an implementation, since there
is no queue entity or member table; the routing advice is the same one
the `approval-approver-type-unsupported` lint hint gives |
| `plugin-approvals` `approval-service.ts:2629`, `:2636`, the
live-record warnings | 3447 | "resolving approvers against the trigger
snapshot instead of the live record they are normally resolved from" |
the card's P1 (approvers are resolved against the live record re-read at
node entry; the trigger snapshot is the fallback the code keeps when
that read finds nothing or fails) |
| `plugin-approvals` `approval-service.ts:3669`, the recall refusal's
developer log line | 3424 | "holds no admin override for a ... request
(the override reaches pending requests only)" | same as the
`via_override` row |
| `service-datasource` `datasource-route-ledger.ts:139`, list note |
3827 | "the retained connect verdict per datasource, so a datasource
that failed to connect reports `error` here instead of looking untested"
| the card's half A (`status` reports the retained connect result
instead of a constant `unvalidated`) |
| `service-datasource` `datasource-route-ledger.ts:149`,
migrate-credential note | 8155 | "drops the inline key only once the
secret is stored" | the maintainer-approved shape (b): the reference is
durably written before the inline key is removed |
| `service-datasource` `datasource-route-ledger.ts:161`, remote-tables
note | 7744, 4249, 7955 | "both stay mounted, because renaming a live
route to match a ledger would be an API break ... one failure contract
(a refusal answers 400 `EXTERNAL_DATASOURCE_ERROR`, the code registered
for the service that refused) and one request shape (both forward
`?schema=` with the same coercion)" | 7744's dispatch and outcome (the
ledger describes what is mounted; no rename), 4249's option 1 as landed
(the separately registered `EXTERNAL_DATASOURCE_ERROR`), 7955's route A
(forward `?schema=` with the twin's coercion) |

### Citation only (the sentence already stated the decision)

- `plugin-approvals`: `sys-approval-action.object.ts:194` `attachments`
help, and its three locale leaves (3266, "Files supporting this action —
e.g. a signed contract or evidence");
`sys-approval-delegation.object.ts:43` description, and its three locale
leaves (1322, "route this user's approver slots to a delegate within a
time window"); `approval-service.ts:1782` (3807, "the slot routes to no
one and the request cannot advance until someone is added or the
approver is re-pointed"); `:2044` (10230), `:2113` and `:2139` (10547),
`:2474` (10153), each already saying the team, member or manager is
dropped because routing to them "would put approval authority over the
record outside its tenant", and the truncated-read line already saying
routing is left unchanged rather than risk dropping a member; `:4245`
(3823, "that pause continues only through this service ... any other
node type there is resumable by anyone with the run id (amended
ADR-0044)").
- `service-datasource`: `external-datasource-service.ts:921` (11000,
"Preserved as a COMMENT because 'ServiceObject' has no authorable key
for a federated object's remote primary key: 'fields.FIELD.primaryKey'
is not part of the field schema").

Every cited card (19: 8078, 8081, 3827, 4249, 7744, 7955, 8155, 10997,
11000, 10153, 10230, 10547, 3424, 3447, 3508, 3807, 3823, 3266, 1322)
was read through REST, body and every comment, before its string was
rewritten. 8078 is a pull request; its body carries the decision.

### Translations

- `via_override` help: the `es-ES`, `ja-JP` and `zh-CN` leaves were real
translations; each now carries the same decision ("un administrador de
la plataforma o de la organización", "プラットフォーム管理者または組織管理者",
"平台管理员或组织管理员") and no number.
- `attachments` help (all three) and the `zh-CN`
`sys_approval_delegation` description were real translations; each
dropped its citation only.
- The `es-ES` and `ja-JP` `sys_approval_delegation` descriptions were
byte copies of the English source. They are now copies of the revised
English, and `node scripts/check-i18n-bundles.mjs --write
--filter=plugin-approvals` rewrote `en` from the object source and
recorded the two new digests in the `es-ES` / `ja-JP`
`*.source-hashes.generated.ts` companions. No digest and no `en` leaf
was hand-edited; the merge kept every hand-written translated value.
- Tracker numbers left in the package's locale bundles: 0.

## Ledger (`scripts/doc-authoring-prose-id.baseline.json`)

Stage 2 (PR objectstack-ai#21311) held this file. This branch was built while it sat
in the queue, then `origin/main` (`222ecc27`, which contains `6091136e`,
the stage-2 merge) was merged in, and only then was the ledger
recomputed with `node scripts/check-doc-authoring.mjs --census-ledger`
(exit 0, no growth refusal) into a scratch file and copied into place.
The diff deletes 52 lines and adds none: exactly the 10 file blocks of
the two packages. Every other row is unchanged (a scripted comparison of
the 62 other keys: 0 moved).

| | before (`222ecc27`) | after |
|---|---|---|
| `service-datasource` | 9 occurrences, 9 pairs, 3 files | 0 |
| `plugin-approvals` | 25 occurrences, 23 pairs, 7 files | 0 |
| whole ledger | 261 occurrences, 189 pairs, 72 files | 227 occurrences,
157 pairs, 62 files |

`pnpm check:doc-authoring` at the head: "sibling-package prose ids hold
the baseline — 204 pinned site(s) across 62 file(s), 85660 string(s)
read in 1247 parsed source(s), no growth, no burn-down unrecorded". No
gate is added or loosened; `scripts/check-doc-authoring.mjs` is
untouched.

## Changeset

`.changeset/20751-services-strings-stage3-state-the-decision.md`:
`patch` for `@objectstack/service-datasource` and
`@objectstack/plugin-approvals`. Measured after building at the head:
the new author-visible sentences are in each package's built output
(`plugin-approvals` `dist/index.js` and `dist/index.mjs`;
`service-datasource` `dist/index.js` and `dist/index.cjs`). A TypeScript
scan of every string literal and template text in the built output finds
0 tracker ids in `service-datasource`; in `plugin-approvals` its six
matches are all CSS hex colours (`#2563eb`, `#64748b`) in the
action-link page template, not tracker ids. Control: the same scan reads
82 in `plugin-security`'s built output and 66 in `service-automation`'s.
The datasource route ledger does not ship: `DATASOURCE_ROUTE_LEDGER` and
its new note text are in 0 files under
`packages/services/service-datasource/dist`, so the changeset does not
describe it.

## Text-only proof

A TypeScript-AST skeleton of each changed non-test `.ts` file, where
every string literal and template text is a placeholder, a run of
adjacent string operands of a `+` chain is one string (only its embedded
expressions are kept), identifiers and numbers keep their text, and
comments are never read. `222ecc27` against the head: 12 of 12 SAME.
Controls on scratch copies of `approval-service.ts`, each mutation's
marker counted once on disk first: a one-identifier rename reads DIFF; a
text-only change reads SAME; a re-split of one template into two
concatenated pieces reads SAME. (A first draft of the tool wrapped every
`+` chain in its own token and read `approval-service.ts` DIFF by two
tokens: the `queue` warning went from one template to a three-piece
chain. The corrected tool treats a chain of strings as one string, which
is what the re-split control pins.)

## Pins

Assertions that found a warning or comment by its tracker number now
find it by what it says. No `code` or `status` assertion was touched;
none of these strings is a coded refusal.

- `approval-service.test.ts:3165`: the `queue` warning by `'queue'` and
"no ownership queue to expand it from" instead of the id.
- `approval-service.test.ts:3218`: the expanded-to-nobody warning by
"cannot advance until someone is added or the approver is re-pointed"
instead of the id.
- `team-approver-org-screen.test.ts:223` and
`team-member-org-screen.test.ts:250`: the warning found by "team
'team_b' was dropped from the approver slate" and "member(s) of team
'team_a' were dropped from the approver slate"; both test titles drop
"and the card".
- `external-object-draft-primary-key.test.ts:218` and `:229`: "a
driver's introspection can report only the first column" and "not part
of the field schema" instead of the ids; the first test's title said
10997 "is unfixed and in another lane", which stopped being true when PR
11104 landed, and now says "a driver can under-report a composite key".
The `lower bound` and `no authorable key` assertions beside them are
unchanged.
- `external-object-draft-authorised-shape.test.ts:151` pins the comment
line "Preserved as a COMMENT because 'ServiceObject' has no authorable
key for a", which this PR leaves byte-identical.

Every re-pinned case was run with the verbose reporter and is listed as
passed.

## Tests

All through `scripts/pm/os-verify-lock.sh`, every verdict `VERDICT
command-exit 0`:
- Build: `turbo run build` over the closure of the two packages and
`@objectstack/cli` (59/59), then, after merging `origin/main`, `turbo
run build --filter=./packages/* --filter=./packages/*/*` (71/71).
- At the merged head `7740c0f0`: `@objectstack/service-datasource` 35
files, 707 tests passed; `@objectstack/plugin-approvals` 53 files, 824
tests passed.
- `typecheck` for both, including `check:test-typecheck: OK` for
`plugin-approvals`.

## Gates

- `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack
--commands` (no paths) at `7740c0f0` (18 paths vs merge base
`222ecc27`): 72 commands, run one at a time from the worktree, each exit
code recorded before any pipe. `--ran`: "72 derived famil(ies) accounted
for — 72 run, 0 NOT-MEASURED (a DERIVED zero — all 72 recorded an exit
code and none of them is 3)".
- `check:i18n`: "all bundles in sync, no undeclared authoring keys";
`check:i18n-stale-fill`: "no new stale fills, 0 baselined";
`check:issue-citations`: "no issue citations added against 222ecc2 (12
file(s) read)"; `check:nul-bytes`: OK, 9637 files;
`check:type-check-debt`: "none above its recorded number";
`check:dual-build-cjs-loads`: 105 require entry points across 66
packages load; `check:dts-closure`: 71 built packages, 167/167;
`check:sourcemap-no-sources-content`: 68 packages, 522 maps.
- Outside the derived set, all exit 0: the eleven declared
wide-population families (`check:init-service-contract`,
`check:live-db-isolation`, `check:meta-type-normalized`,
`check:optional-error-sink`, `check:resume-authority-declared`,
`check:route-envelope`, `check:runner-env-posture`,
`check:settings-bind-window`, `check:startup-registry-verdict`,
`check:verify-stand-in`, `check:wildcard-fallthrough`) and the
artifact-roster families whose roster sits under one of this PR's
directories (`check-changeset-fixed`, `check-published-list-mirrors` and
its self-test, `check:authz-resolver`, `check:console-injection`,
`check:error-code-casing`, `check:filter-alias-parity`,
`check:published-readme-exports`, `check-dts-references --self-test`;
`check:engine-double-contract` and `check:i18n-stale-fill` are already
in the derived set). The path-scheduled CI jobs and the type-check lanes
are CI's.
- `pnpm lint` (`eslint . --no-inline-config`, repo-wide, not narrowed)
at `7740c0f0`: exit 0, 146 s under the lock.

## Acceptance notes

Noted, not filed:
- `docs/qa/platform-checklist/areas/approvals.json:1249` quotes the
`queue` warning with its old tracker suffix. The part it quotes before
the suffix survives verbatim in the new text, so a runner still finds
the warning; the file is outside this stage's surface. Carrier: none.
- `external-object-draft-primary-key.test.ts` keeps a docblock (lines 63
to 67) saying 10997 is a separate, unfixed defect in the engine lane. It
is a comment above the fixture, outside the ledger; this PR changes only
the title and assertion of the test that pins the caveat. Carrier: none.
- `packages/lint/src/validate-approval-approvers.ts:463` carries the
same `queue` citation in its lint message, and its test pins the id.
`packages/lint` is outside this lane's ledger share.

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

---------

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/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant