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