Skip to content

fix(spec): author-facing describes and refusals drop service-interface names, ruling dates and foreign example ids - #22125

Merged
objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/issue-22093-author-strings-internal-refs
Oct 8, 2026
Merged

objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/issue-22093-author-strings-internal-refs

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

Part of #22093
Clause-②: no

Wording only: no schema, key, type, export or error-code change. Patch changeset for @objectstack/spec, @objectstack/service-automation and @objectstack/platform-objects. objectui#11785 is the Studio half (splitting an author message from the detail); this PR changes the server-side strings only. The card keeps two named remainders, listed under "Remainder of the card and its route" below.

What changed

Form help and refusals that an author reads no longer carry service-interface names (IXxxService.method()), ruling dates ("maintainer ruling DATE", "ruled DATE") or another product's example ids. The rationale they carried moves into the code comment beside each string. Each describe and refusal still states the rule, why it exists and the repair.

How "author-facing" was measured (instrument and radius)

Studio reaches these strings through five routes. I measured each one at a543e244f:

Route What Studio renders Instrument
R1 /meta/types schema: every DEFAULT_METADATA_TYPE_REGISTRY type, derived as metadata-protocol derives it (z.toJSONSchema, unrepresentable: 'any', with the io: 'input' retry) Every description in all 27 type schemas
R2 /meta/types form: all 17 METADATA_FORM_REGISTRY layouts Every description / helpText / label / placeholder / hint
R3 The package dialog, which derives its form from ManifestSchema client-side (objectui package-schema.ts) Every description
R4 The flow inspector, which reads node configSchema from GET /api/v1/automation/actions An AST string scan of every file that declares a configSchema (13 files)
R5 Refusals read at a door An AST scan of every string literal in packages/spec/src (1284 files, 80190 string parts, comments excluded)

The class patterns were: interface \bI[A-Z]\w*(Service\|Driver\|Engine\|…)\b; ruling \bruling\b\|\bruled\b\|\b20\d\d-\d\d-\d\d\b; foreign steedos\|superset\|salesforce\|apache\|airtable\|….

Positive control: both strings the filer measured are found.

  • The notify Template help is found by R4 at service-automation/src/builtin/notify-node.ts:203, as both an interface hit and a ruling hit. The spec copy is found by R5 at io-node-config.zod.ts:250.
  • com.steedos.crm is found by R5 at manifest.zod.ts:283.

R1 to R3 found neither string (needle count 0). So the Studio-rendered Template help is produced by the service-automation descriptor, not by the spec describe. That is why the producer side is changed too.

After the change, R1 to R3 hold no interface, ruling or foreign-id hit. The one exception is the "Airtable parity" wording (see Acceptance notes). The needles II18nService, IEmailService, com.steedos.crm and maintainer ruling count 0. The R5 hit count drops by 22.

Every string changed

Lines are at a543e244f. Before is the internal reference that was removed; After is what replaced it. No changed string carried a tracker number, so no tracker number moved.

# Where Reach Before After Rationale now lives in
1 services/service-automation/src/builtin/notify-node.ts:203, notify configSchema.properties.template.description R4: Studio notify Template help (filer-measured) "else the deployment default (II18nService.getDefaultLocale()) … (maintainer ruling 2026-09-01). A producer-set payload.locale is not consulted." "else the deployment default locale … The node's payload.locale is not consulted." Comment above template: in the descriptor
2 spec/src/automation/io-node-config.zod.ts:250, NotifyConfigSchema.template describe Reference docs and published JSON Schema; the contract row 1 mirrors Same as row 1, with backticks Same as row 1 TSDoc above template (it already named both; one sentence added)
3 spec/src/kernel/manifest.zod.ts:283, MANIFEST_ID_EXAMPLES R5: every package-id refusal (filer-measured on POST /api/v1/packages) 'com.steedos.crm', 'org.apache.superset' 'com.acme.crm', 'org.example.help-desk' Doc comment on the constant
4 manifest.zod.ts:305, manifestIdRefusal R5: same doors "Invalid package id 'VALUE' on manifest.id. Expected reverse-domain notation ('com.steedos.crm', 'org.apache.superset') — …" "Invalid package id 'VALUE'. A package id (manifest.id) is written in reverse-domain notation, like 'com.acme.crm' or 'org.example.help-desk' — …", with the rule and suggestion arm unchanged TSDoc on manifestIdRefusal
5 manifest.zod.ts:360-361, @example on id TSDoc; held equal to row 3 com.steedos.crm, org.apache.superset com.acme.crm, org.example.help-desk (none)
6 spec/src/data/field.zod.ts:1142, required describe R1: object, field "(maintainer ruling 2026-08-18)" Removed TSDoc above required
7 field.zod.ts:1170, multiple describe R1: object, field "(maintainer ruling 2026-09-13), and on radio by the narrower 2026-08-22 ruling … (maintainer ruling 2026-08-18)" "on any other type, radio included, is REFUSED at parse …" TSDoc above multiple (new Declarability paragraph)
8 spec/src/ui/view.zod.ts:1175 / :1221, GROUPING_FIELD_RULING in the padded grouping-field refusal R5: view save "… (ruled 2026-09-10.)" Removed, constant deleted Comment where the constant was
9 view.zod.ts:3444, form-view field visibleWhen describe R1: view "refused at parse (ruled 2026-08-27):" "refused at parse:" TSDoc above
10 view.zod.ts:3655, section collapsible describe R1: view "(ruled 2026-09-18)" Removed The collapse-pair TSDoc (already records the ruling)
11 view.zod.ts:3663, section collapsed describe R1: view "(ruled 2026-09-18; refusing the combination at the declaration, and warning on it, were both rejected — nobody can depend on a section that cannot be opened)" ", so writing collapsed: true alone is a correct way to say "collapsed by default"" The collapse-pair TSDoc (already records the rejected letters)
12 view.zod.ts:3715, form-view section visibleWhen describe R1: view "(ruled 2026-08-27)" Removed TSDoc above
13 view.zod.ts:3959, SUBMIT_REDIRECT_RULING in seven submitBehavior.url refusals (:3988, :3993, :4003, :4010, :4018, :4029, :4039) R5: form-view save "(ruled 2026-08-11)" Removed, constant deleted Comment where the constant was
14 view.zod.ts:4097 / :4123, FORM_VIEW_FEATURES_RULING in the features.* predicate refusal R5: form-view save "scope root (ruled 2026-08-27)." "scope root." Comment where the constant was
15 view.zod.ts:4444, redirect-arm url describe R1: view "…successful submit. Ruled 2026-08-11: (1) …" "…successful submit. (1) …" The checkSubmitRedirectUrl comment
16 spec/src/ui/page.zod.ts:941 (kind) and :990 (source) describes R1: page "(ADR-0065; ADR-0080 amendment 2026-06-30)" "(ADR-0065; ADR-0080)" TSDoc above kind (new) and source (already)
17 spec/src/system/email-template.form.ts:19, Identity section help. Its three translated values in platform-objects/src/apps/translations/{zh-CN,ja-JP,es-ES}.metadata-forms.generated.ts:2183 are hand-written, and en is regenerated by pnpm i18n:extract R2: email_template form "Template identifier resolved by IEmailService.sendTemplate({ template: name, locale, ... })." "Senders address this template by its name; the locale selects which language version of it is sent." Comment above description:
18 spec/src/data/filter.zod.ts:475, null ordering comparand refusal R5: filter validation "Ruled 2026-09-01: a null ordering comparand is refused at the validation entrance." Sentence removed Builder TSDoc
19 filter.zod.ts:564, { $field } in a list position refusal R5 "Ruled 2026-08-11: declared = enforced (ADR-0049)." Sentence removed Comment inside the builder
20 filter.zod.ts:589, null list member refusal R5 "Ruled 2026-08-31: a null list member is refused at the validation entrance." Sentence removed Builder TSDoc
21 filter.zod.ts:801, blank $between bound refusal R5 "Ruled 2026-09-17: a blank $between bound is refused at the validation entrance." Sentence removed Builder TSDoc
22 spec/src/ui/action.zod.ts:985, action description describe (added in patch round 1) R1: action, in the authoring (io: 'input') derivation; the action Description help objectui#11785 measured "(one dialog, not two —)": the dash an earlier strip of the cited ruling left dangling "(one dialog, not two)" Docblock above description (already cites the ruling; one sentence added)

The regenerated content/docs/references/** files follow from rows 2, 6, 7, 9 to 12, 15, 16 and 22.

Remainder of the card and its route

This PR delivers the card minus two named rows. It is therefore Part of #22093, and the card stays open for both.

  1. The governed submitBehavior row.
    • What: view.zod.ts:4478, the top-level submitBehavior describe, keeps "(ruled 2026-08-11)".
    • Why it is not here: gen:react-blocks projects this describe into skills/objectstack-ui/references/react-blocks.md, which is a governed Tier H surface, so changing it here would make this whole sweep a Tier H landing. Measured: the regenerated skill file differs in exactly that one row.
    • Route: a separate Tier H draft PR carrying the one describe edit, the regenerated react-blocks.md row and its reference-docs row, landed on the maintainer's approval.
    • A comment beside the describe records the deferral.
  2. The sys_email field help naming IEmailService.send.

Pins

  • Refusal code is asserted with the sentence:

    • manifest.test.ts asserts invalid_format.
    • view-form-features-root.test.ts asserts custom.
  • Negative pins on what the author reads. No interface name, ruling date or foreign id appears in:

    • io-node-config.test.ts
    • notify-node.test.ts, on the Studio-served descriptor text
    • manifest.test.ts. Its headline sentence carries no manifest.id, the key is still named as a locator, and there is no steedos/superset/apache.
    • filter.test.ts
    • view-form-features-root.test.ts
    • view-submit-redirect-url.test.ts
  • Patch round 1: action-description.test.ts. It derives ActionSchema the way /meta/types serves it (io: 'input') and asserts three things about the description help:

    • it is present;
    • no dash is left dangling before a close paren;
    • it has no ruling date, service interface or tracker id.
  • Pins that changed direction. These earlier pins asserted that the date was present:

    • view-submit-redirect-url.test.ts, 7 assertions: "cites the ruling so the refusal is traceable"
    • view-form-features-root.test.ts:68
    • filter.test.ts:644
    • io-node-config.test.ts:347 and its 2026-09-01 pin

    They are now negative pins. filter.test.ts:661 used the date as its discriminator between two messages. It now discriminates on the blank-bound headline.

  • No whole-prose pin was added.

Where the ruling dates in refusals come from (lineage)

This PR reverses no ruling.

Tests and gates

Patch round 1, on the merged head 026204631.

  • Merge: origin/main was merged at 54ace18c6 with bash scripts/pm/os-regen-merge.sh.
    • Merge commit 75cec8cd1, parents 34bf72933 and 54ace18c6. There were no conflicts.
    • Step 2 kept the branch's bytes of the 7 reference-docs files main had not moved.
    • The regeneration ran only after the merge commit (never in MERGE state). It reproduced main's side byte for byte; the only regenerated delta is the action row, in 3 docs files.
    • Main's landed migration entries are still present on the merged head. Three were checked by name: flow-edge-unresolved-or-repeated-refused, sys-presence-organization-column-retired and sys-job-organization-column-retired.
    • The delta against main is exactly this branch's 30 paths.
  • Builds and regeneration, all under the verify lock:
    • pnpm --filter @objectstack/spec build passed.
    • gen:schema and gen:docs passed.
    • The service-automation^... closure build (spec excluded) passed.
  • spec tests: pnpm --filter @objectstack/spec exec vitest run --maxWorkers=2 over 26 targeted files (round 0's 25 plus action-description.test.ts): 26 files, 2475 tests passed. This is a declared narrowing; CI Test Core runs the full suite.
  • service-automation: notify-node.test.ts, 16 passed.
  • platform-objects: src/apps/translations/*.test.ts, 24 files, 430 passed.
  • Typecheck: @objectstack/spec typecheck passed.
  • Generated artifacts: pnpm --filter @objectstack/spec check:generated reports all 15 generated artifacts up to date (react-blocks and api-surface included).
  • Reverse verification, patch round 1 (one-off):
    • action.zod.ts went back to the 54ace18c6 bytes; landing confirmed by grep -c = 1.
    • 1 of 16 tests in action-description.test.ts went red: "carries no residue of a stripped reference".
    • The file was restored from HEAD, hash-identical to its HEAD blob, and the suite was green again (16 of 16).
  • Reverse verification, round 0 (at 34bf72933):
    • The 5 touched sources went back to the a543e244f bytes.
    • 39 of 347 spec tests went red across all 5 changed spec test files, and 1 of 16 in notify-node.test.ts. Every red is a pin this PR added or flipped.
    • Restored byte-identical to HEAD.
  • Lint: pnpm exec eslint --no-inline-config --format json over the 20 changed lintable files at 026204631: 20 files, 0 errors, 0 warnings.
    • Population: eslint.config.mjs matches **/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs} minus NEVER_LINTED.
    • Invariance: the config never enables type-aware linting (no parserOptions.project, see its note near line 328), so this diff cannot move the verdict on any file it does not touch.
  • Gates: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at 026204631 derived 113 families (30 paths against merge base 54ace18c6).
    • 109 run, all exit 0, each exit code captured before any pipe.
    • 4 NOT MEASURED, recorded as reasoned claims and read from CI on the landing head: check:skill-examples, check:dual-build-cjs-loads, check:i18n and check:type-check-debt. In round 0 each exited 3 (PREREQUISITE NOT MET). check:type-check-debt's re-measure is a 45-task workspace build and is not run outside the verify lock.
    • --ran reconciliation: 113 accounted, 109 run, 4 NOT MEASURED (claimed), 0 unrun.

Acceptance notes

  • Measured but not changed, as outside the three classes or outside reach:
    • stack.zod.ts:458-459 (IJobService, IEmailService.sendTemplate): defineStack collection describes. R1 to R3 do not serve them, and they are not refusals.
    • Strings whose reader is a plugin, driver or API developer, for whom the interface is the contract they implement or call. None is in R1 to R3:
      • data-engine.zod.ts:165,190 (IDataEngine.find())
      • hook.zod.ts:1269 (IScopedContext)
      • query.zod.ts:429
      • the driver.zod.ts:287-388 refusals (IDataDriver)
      • automation-api.zod.ts:452
    • execution-context.zod.ts:441 ("the 2026-09-03 ruling"): preserveAudit is server-constructed and never authored.
    • plugin-rest-api.zod.ts:138 ("ruling record, 2026-09-01"): a tombstone on RestApiEndpointSchema. Its own comment records that nothing parses that schema outside its unit tests, so no door reaches it.
    • component.zod.ts:365 ("Since the console release of 2026-08-21 (c86185eb5)"): a release date and a commit sha, not a ruling.
    • Undated ruling words. These carry no date and no "maintainer ruling", so they fall outside the class as the seat scoped it:
      • action.zod.ts:1139 and :2157
      • page.zod.ts:93
      • view-grouping-query.ts:433
      • context.zod.ts:50
      • protocol.zod.ts:1681
      • endpoint-publish-gate.ts:368,416
      • the error-code-ledger.zod.ts notes
    • "Airtable parity" wording is the only R1 to R3 hit left: page.zod.ts:911, page.form.ts:113, view.form.ts:164, plus view.zod.ts:1706, page.zod.ts:647 and component.zod.ts:1766. It is a design note naming a competitor, not an example id.
    • Salesforce mentions in export.zod.ts:292 and object.zod.ts:1528 are comparisons, not ids.
    • src/migrations/**, about 1,240 dated or "ruling" hits: upgrade-guide records, not a form or a door.
    • manifest.zod.ts:546, TSDoc @example on dependencies (@steedos/plugin-auth): TSDoc only. packages/core/src/artifact-packages.ts:114 cites that exact example, so changing it moves a comment in core.
    • The sys_email field help naming IEmailService.send is the second named remainder; see "Remainder of the card and its route".
  • objectui: it has no copy of any changed string at 9990f9e (grep for II18nService, IEmailService, steedos.crm, Invalid package id).

Generated by Claude Code

claude added 3 commits October 7, 2026 19:14
…rface names, ruling dates or foreign example ids

Studio renders spec describes as form help (/meta/types schema + form) and the
notify node's Template help from the service-automation descriptor; refusals
are read at the save and install doors. The rationale those strings carried
(II18nService/IEmailService names, "maintainer ruling <date>", "ruled <date>")
moves into the code comments beside them; MANIFEST_ID_EXAMPLES becomes
com.acme.crm / org.example.help-desk, and the package-id refusal leads with a
product-words headline and names the key as a locator after it.

WIP: generated artifacts not yet regenerated.

Claude-Session: https://claude.ai/code/session_01RPo7FUd6bSnAfkWMAKi848
Co-authored-by: Claude <noreply@anthropic.com>
…itBehavior describe for a governed change

The top-level `submitBehavior` describe is projected into
skills/objectstack-ui/references/react-blocks.md by gen:react-blocks, a
governed surface, so its ruling date stays in this wording sweep and the
deferral is recorded beside it.

Claude-Session: https://claude.ai/code/session_01RPo7FUd6bSnAfkWMAKi848
Co-authored-by: Claude <noreply@anthropic.com>
…the email-template Identity help

`pnpm i18n:extract` output; the zh-CN / ja-JP / es-ES values were
re-translated by hand in the first commit of this branch.

Claude-Session: https://claude.ai/code/session_01RPo7FUd6bSnAfkWMAKi848
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/platform-objects, @objectstack/service-automation, @objectstack/spec, touching 29 documentable anchor(s).

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

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

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

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 54ace18c669a776c7e849708039c7876ac534cee

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

claude added 3 commits October 7, 2026 22:40
…rence left behind

"(one dialog, not two —)" read as a truncated sentence in Studio's action
inspector; the ruling it once cited stays in the docblock above the key.
Pinned on the authoring-shape JSON Schema Studio renders: no dash left
dangling before a close paren, no ruling date, no service interface, no
tracker id.

Claude-Session: https://claude.ai/code/session_01RPo7FUd6bSnAfkWMAKi848
Co-authored-by: Claude <noreply@anthropic.com>
gen:schema && gen:docs after merging origin/main (54ace18): the only
delta is the action description row. Main's regenerated docs reproduce
byte-for-byte from the merged sources.

Claude-Session: https://claude.ai/code/session_01RPo7FUd6bSnAfkWMAKi848
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 0262046319a13c871d5427ac8cc4181ce54a3c5e
Local-runs: none

Read-only review of PR #22125 against main at merge base 54ace18c6 (30 paths, +252/−115), card #22093 (body and all 7 comments), the PR body, its one bot comment, and the head's check-runs. Nothing built, run or re-run; the record template and the stamped-post relay were the only scripts invoked. Rendered 2026-10-07T23:40Z.

Check-runs on the head. First read at 2026-10-07T23:31:32Z: 46 runs, 45 completed (38 success, 7 skipped), 1 in_progress (a re-triggered Check Changeset). Re-read at 2026-10-07T23:35:34Z: 46 runs, all completed — 39 success, 7 skipped; newest run per name: 31 success, 4 skipped (Auto Label, Check PR Size, Packed-tarball smoke (opt-in), Console Pin Gate). No failure, no neutral, nothing pending. Governed Surface Queue Guard success; no path in the file list hits the GOVERNED_SURFACES register, so this record is owed on the packages/spec/src/** non-test path limb alone.

① Derived judgments

Accept-set: unchanged, and the PR says so correctly. Every edit is to a .describe() string, a refusal message string, a TSDoc or code comment, or a module-private constant. Checked one by one:

  1. manifestIdRefusal (manifest.zod.ts): headline reordered to "Invalid package id 'VALUE'." with the key as a parenthetical locator in sentence 2; examples joined with " or ". Zod code stays invalid_format (pinned in manifest.test.ts); the regex MANIFEST_ID_PATTERN is untouched. Right — message only; the key is still named, which keeps the refusal-names-the-key contract the POST /api/v1/packages door depends on, and the changeset carries a matcher-migration sentence for anyone who matched the old "on KEY. Expected reverse-domain notation" wording. Right.
  2. MANIFEST_ID_EXAMPLES value ['com.steedos.crm','org.apache.superset'] → ['com.acme.crm','org.example.help-desk']. This IS a public export: kernel/index.ts re-exports ./manifest.zod and the package publishes ./kernel; it sits in api-surface/kernel.json. The api-surface gate records name (kind) rows only, so the change is invisible to it by design, and because the tuple is as const its literal type narrows to the new strings. In-repo readers are manifest.zod.ts and manifest.test.ts only; the existing pin that every example matches the pattern still holds. Right at patch — the constant's contract is "the examples the refusal shows", the changeset names the old and new values, and no consumer reads the literals — but the PR body's "no … type … change" is one notch too absolute; the literal tuple type did move. Noted, not a defect.
  3. Four filter.zod.ts builders (nullOrderingComparandMessage, listPositionFieldReferenceMessage, nullListComparandMemberMessage, blankRangeBoundMessage): one trailing "Ruled DATE: …" sentence dropped each; rule, reason and repair sentences intact; the ADR-0049 anchor and [spec] FieldReferenceSchema is declared in the $between endpoints but NO backend resolves a $field inside a list #7596 moved into the builder comment. Right. The re-pointed discriminator in filter.test.ts (not.toContain('A blank value is not a valid $between endpoint')) names the real headline of blankRangeBoundMessage at the head, so the negative pin is non-vacuous. Right.
  4. view.zod.ts: GROUPING_FIELD_RULING, SUBMIT_REDIRECT_RULING, FORM_VIEW_FEATURES_RULING deleted — all three were module-private consts (no export), so no public surface moves; their dates and tracker ids now live in the comments where the constants were. The seven checkSubmitRedirectUrl refusals, the padded grouping-field refusal and the features.* root refusal keep rule + reason + repair; custom code pinned. Right.
  5. Describes that lost a date or an interface name only: field required / multiple (the radio clause folded into "any other type, radio included" — same refusal set as before), form-view field and section visibleWhen, section collapsible / collapsed (the new "so writing collapsed: true alone is a correct way to say collapsed by default" restates what the describe already asserts), redirect-arm url, page kind / source (ADR ids kept, the 2026-06-30 amendment date moved to TSDoc), action description (dangling dash removed), notify template (spec describe and the service-automation descriptor stay mirrored). Right.
  6. email-template.form.ts Identity help and its four *.metadata-forms.generated.ts leaves (en regenerated, zh-CN / ja-JP / es-ES hand-translated; the translations read as faithful renderings). Right.
  7. Generated content/docs/references/** (9 files) follow the describes byte-for-byte on the merged head; Type Check · source gates (check:generated --reconcile-only) and Type Check · consumer gates (check:api-surface) are green. Right.
  8. Negative pins added or flipped (interface \bI[A-Z]\w*Service\b, ruling \bruled\b|\bruling\b|\b20\d\d-\d\d-\d\d\b, tracker #\d{3,5}) across io-node-config.test.ts, notify-node.test.ts, manifest.test.ts, filter.test.ts, view-form-features-root.test.ts, view-submit-redirect-url.test.ts, action-description.test.ts (derived with io: 'input', the shape /meta/types serves). Each pairs with a positive pin so none can pass on an empty string. Right.
  9. One downstream reader the PR did not list: docs/qa/platform-checklist/areas/cli.json (around lines 1539 and 1541) quotes the OLD headline verbatim — "Invalid package id '' on manifest.id. Expected reverse-domain notation …" — as a clause's expected refusal. Its verify text ("the reverse-domain refusal naming manifest.id with the received ''") still holds, but the quoted sentence is now stale and a literal runner would mis-score it. Not a gate, not a governed path, not in the dispatch's file surface. Escalated under ③.

Refusal codes, exports, keys, schemas and the strict-object key sets are unchanged throughout. Console Pin Gate skipped because the filter job reported no console paths changed — expected: nothing exported is removed or renamed, and the dev's grep of objectui at 9990f9e finds none of the changed strings.

② Semver level

.changeset/22093-author-strings-internal-refs.md: patch for @objectstack/spec (17.7.0), @objectstack/service-automation (17.7.0), @objectstack/platform-objects (17.7.0) — exactly the three published packages the diff touches (content/docs/** is not a package). Check Changeset green on the head. Patch is the right level: wording of describes and refusals, a module-private constant removal, and one public example-constant value change with no accept-set effect; the body names the manifest headline's matcher migration and the old/new example values, which is the disclosure a patch of this shape owes.

Clause-②: no — correct. No parse accepts or refuses anything it did not before, so neither (widening) nor (narrowing) applies; the PR body, the changeset body and the claim/triage all carry the same Clause-②: no.

③ Boundary flags

Dev flags and open questions, each with its answer:

Escalated to the seat (none blocks this verdict):

  • E1 — stale QA clause. docs/qa/platform-checklist/areas/cli.json quotes the pre-change package-id headline (item ① 9). One clause revision with a revision entry, either folded onto this branch before landing (non-governed path, no single-writer conflict in the open-PR scan) or carried as a card. The seat decides which.
  • E2 — hand-written docs restate the old Template help. content/docs/automation/email-templates.mdx (around lines 171–176) still names II18nService.getDefaultLocale() and "(maintainer ruling, 2026-09-01)" in prose that mirrors the describe this PR rewrote. It is a reader-facing docs page, not form help or a refusal, so it is outside the card's class; flagged so the seat can decide whether docs and help should read alike. content/docs/releases/v17/17-1.mdx carries the same phrase and is release-owned (read-only).

Implemented-by: claude/issue-22093-author-strings-internal-refs
Reviewed-by: session_01RPo7FUd6bSnAfkWMAKi848

VERDICT: PASS


Generated by Claude Code

akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 9, 2026
…e; QA clause requotes the package-id refusal (objectstack-ai#22309)

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

This PR delivers remainders (b) and (c) of objectstack-ai#22093, as the seat's
partial-landing record (`6049519522`) names them. Remainder (a), the
governed `submitBehavior` describe, is not in this PR and stays on the
card. Wording only: no field, option, key, type or export changes. It
ships a patch changeset for `@objectstack/platform-objects`. The
`docs/qa` change publishes nothing.

## (b) The `sys_email` field help no longer names a service interface

Studio shows a field's `description` as its help in the object forms:
the `help` leaf in each locale's `objects` bundle. Two `sys_email`
fields named the `IEmailService.send` service interface there. Each one
now says what the field holds and when it is set, in product words, and
keeps every fact it stated before. The implementation reference moves
into the code comment beside each field (`sys-email.object.ts`), in the
same form PR objectstack-ai#22125 used for the other 22 rows.

| Field | Locale | Before | After |
| :--- | :--- | :--- | :--- |
| `headers_json` | en (source) | "Custom headers supplied to
IEmailService.send, as a JSON object of name → value." | "Custom headers
supplied with the message when it was sent, as a JSON object of name →
value." |
| `headers_json` | zh-CN | "传给 IEmailService.send 的自定义邮件头,…" |
"发送邮件时随附的自定义邮件头,…" |
| `headers_json` | ja-JP | "IEmailService.send に渡されたカスタムヘッダーを、…" |
"メッセージの送信時に指定されたカスタムヘッダーを、…" |
| `headers_json` | es-ES | "Cabeceras personalizadas facilitadas a
IEmailService.send, como un objeto JSON …" | "Cabeceras personalizadas
facilitadas con el mensaje al enviarlo, como un objeto JSON …" |
| `status` | en (source) | "Lifecycle state — queued by
IEmailService.send before transport call" | "Lifecycle state — queued
when the message is submitted for sending, before the first delivery
attempt" |
| `status` | zh-CN | "生命周期状态——在调用传输层之前由 IEmailService.send 排队" |
"生命周期状态——邮件提交发送时即进入排队,早于首次投递尝试" |
| `status` | ja-JP | "ライフサイクル状態 — トランスポート呼び出し前に IEmailService.send
がキューに入れます" | "ライフサイクル状態 — 送信が要求された時点で、最初の配信試行の前にキューに入ります" |
| `status` | es-ES | "Estado del ciclo de vida; se pone en cola mediante
IEmailService.send antes de la llamada al transporte." | "Estado del
ciclo de vida; queda en cola al solicitarse el envío del mensaje, antes
del primer intento de entrega." |

For `headers_json`, only the first sentence changes. The rest of each
leaf is unchanged. The `status` wording uses the vocabulary of the
neighbouring `attempt_count` help in each locale ("delivery attempts",
投递尝试, 配信試行, intentos de entrega).

### How the bundles were produced

- **en:** regenerated by the package's documented generator, `pnpm
i18n:extract`, after the source edit. It rewrote exactly the two `en`
leaves.
- **zh-CN, ja-JP, es-ES:** the translated leaf is itself where the
translated text lives. The generator's merge mode never overwrites an
existing translated leaf. The bundle header ("Edit translations in place
… Do not hand-edit the structure — only the leaf string values") and
AGENTS.md's Documentation Guardrails row both name re-translating the
leaf in place as the maintenance path. So I re-translated the two leaves
in place and then ran the generator. It wrote all 11 files and left the
three translated bundles and their `*.source-hashes.generated.ts` tables
byte-identical. That shows the edit changed leaf values only, never
structure. Neither leaf had a recorded source hash, so no provenance
entry moves. A second `pnpm i18n:extract` at the head leaves `git
status` empty.
- The dispatch named es-ES and ja-JP. zh-CN carried the same phrase, so
it is rewritten too.

### Pin


`packages/platform-objects/src/apps/translations/objects-help-no-service-interface.test.ts`
walks every string leaf of the four `objects` bundles. It asserts that
none matches the service-interface pattern the notify Template help pin
already uses (`\bI[A-Z]\w*Service\b`). It also checks the two
`sys_email` source descriptions. A positive control asserts that the
walk reaches both rewritten leaves in every locale, so a walk that
reaches nothing cannot pass. Today no other leaf in the four bundles
matches, so the pin covers the whole class for this package's object
help, not just these two rows.

Ablation (one-off, no file kept): `scripts/ablation-replace.mjs` put the
old zh-CN `status` leaf back, landing confirmed on disk (anchor 1 → 0,
blob `5cf8f2e7e81d` → `64f05c216782`). The pin went red as 1 failed and
5 passed, naming `sys_email.fields.status.help` in zh-CN only. The file
was restored with the blob equal to HEAD and `git diff HEAD` empty.

## (c) The QA clause quotes the current package-id refusal

`docs/qa/platform-checklist/areas/cli.json`, item
`cli.plugin-manifest-build-contract`, acceptance clause A2:

| Before | After |
| :--- | :--- |
| "a located Zod error ('Invalid package id '' on `manifest.id`.
Expected reverse-domain notation …')" | "a located Zod error ('Invalid
package id ''. A package id (`manifest.id`) is written in reverse-domain
notation, like …')" |

The new quote is taken from `manifestIdRefusal`
(`packages/spec/src/kernel/manifest.zod.ts`) at `6729e107e`. I checked
it against the live refusal: `ManifestSchema.safeParse` with `id: ''`
answers on path `id` with "Invalid package id ''. A package id
(`manifest.id`) is written in reverse-domain notation, like
'com.acme.crm' or 'org.example.help-desk' — …". The item's `revision`
moves from 2 to 3, with one `history` entry in the file's convention
(`revision`, `date`, `change`, `ref`). The scored property is unchanged,
and so is A2's `verify` text: probe (d) still reads a located
reverse-domain refusal that names `manifest.id` and echoes the received
`''`.

## Verification (at `acc26751b`)

- **Build:** the extract closure under the verify lock, `pnpm turbo run
build --filter='@objectstack/lint...'
--filter='@objectstack/platform-objects...' --concurrency=2`: 8 of 8
tasks, `VERDICT command-exit 0`.
- **Tests:** `pnpm --filter @objectstack/platform-objects exec vitest
run --maxWorkers=2 src/apps/translations src/audit/sys-email`: 27 files,
443 tests passed.
- **Typecheck:** `pnpm --filter @objectstack/platform-objects
typecheck`, all three legs including `check:test-typecheck`: exit 0.
- **Lint:** `eslint --no-inline-config --format json` on the 6 changed
TS files: 6 files, 0 errors, 0 warnings. The population comes from
`eslint.config.mjs`: `**/*.{ts,…}` minus `NEVER_LINTED`. Type-aware
linting is not enabled, so this diff cannot move the verdict on any
untouched file.
- **Gates:** `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derived 65 commands. I ran them together
with the 52-row artifact-roster block, 115 unique commands. 107 exited
0.
- Three are PR-context gates that print NOT WIRED (exit 2) without
`PR_BODY` or `PR_NUMBER`. `check:partof-closing-keyword` was re-run
against this body.
- Four exited 3 with PREREQUISITE NOT MET, because they read a
whole-workspace build this run did not make:
`check:dual-build-cjs-loads`, `check:i18n` (the built CLI),
`check:lean-entry-closure` and `check:published-readme-exports`. For
`check:i18n`, the documented command it runs for this package (`pnpm
i18n:extract`) was run directly, and it is idempotent at the head.
- `check:type-check-debt` was not run locally: its re-measure is a
workspace build outside the verify lock. CI reads it.

## Acceptance notes

- The zh-CN, ja-JP and es-ES `headers_json` help leaves omit the last
English sentence ("Never returned on the generic data path — …"). This
translation lag predates this PR, and the source-hash mechanism treats a
leaf with no recorded hash as legacy-trusted, so nothing flags it. This
PR re-translates only the sentence that named the interface. Noted, not
filed. Carrier: none.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 9, 2026
…bjectstack-ai#22322)

Fixes objectstack-ai#22093
Clause-②: no

The card's last remainder, (a): the form view `submitBehavior` describe
in `packages/spec/src/ui/view.zod.ts` no longer ends in a ruling date.
Help text only: no key, type, accept/reject or export change, with a
`patch` changeset for `@objectstack/spec`. The diff reaches
`skills/objectstack-ui/references/react-blocks.md`, a Tier H governed
surface, through the generator alone, so this PR stays a draft and lands
on the maintainer's approval (route A of the seat's comment
`6047193742`).

## What changed

| Where | Before | After |
| :--- | :--- | :--- |
| `FormViewSchema.submitBehavior` describe (`view.zod.ts`), the property
help that Studio, the reference docs and the published skill show for
this key | "Post-submit behavior. On the `redirect` arm, `url` is
relative-only and interpolates only declared record fields as
`{{record.field_name}}`, URL-escaped (ruled 2026-08-11)." | "Post-submit
behavior. On the `redirect` arm, `url` is relative-only and interpolates
only declared record fields as `{{record.field_name}}`, URL-escaped." |

Every fact the sentence stated stays. The ruling's date and its tracker
reference (objectstack-ai#7496) now live in the code comment directly above the
describe, in the form PR objectstack-ai#22125 used for the other 22 rows, and that
comment's paragraph that deferred this edit to this card is gone.

## Regenerated projections (generator output, not hand-edited)

`pnpm --filter @objectstack/spec check:generated` after the edit named
exactly two stale artifacts, `check:react-blocks` and `check:docs`.
`check:generated --fix` regenerated those two and nothing else, and a
re-run reads all 15 artifacts up to date.

| File | Rows changed | Reading |
| :--- | :--- | :--- |
| `skills/objectstack-ui/references/react-blocks.md` | 1 (the
`submitBehavior` row) | The row used to be clipped at the generator's
160-char limit ("… URL-escaped (ruled…"). The new sentence is 153 chars,
so the row now reads whole: "… URL-escaped." |
| `content/docs/references/ui/view.mdx` | 5 `submitBehavior` rows |
"(ruled 2026-08-11)" dropped; nothing else moves |
| `content/docs/references/api/protocol.mdx` | 2 `submitBehavior` rows |
same |

Published-skill readings (`skills/**` is token-ratcheted by
`check-skills-token-ratchet`, counting `ceil(utf8 bytes / 4)`), at
`4e4111ca0` → this head:

- `react-blocks.md`: 115 → 115 lines; 13,633 → 13,624 bytes; 3,409 →
3,406 tokens.
- the whole `skills/` package (65 files): 13,343 → 13,343 lines; 627,340
→ 627,331 bytes; 156,835 → 156,833 tokens. Net −9 bytes, no new content,
no re-wrap.

## Pin

`packages/spec/src/ui/view-submit-redirect-url.test.ts` gains section 6.
It reads the `submitBehavior` help from the published JSON Schema
projection (`projectPublishedJsonSchema`, the projection
`gen:react-blocks` and `gen:docs` render from) and asserts: the positive
control (the sentence still names `Post-submit behavior`, the `redirect`
arm, `relative-only`, `{{record.field_name}}`, and ends on
"URL-escaped."), that it matches neither the file's `RULING_DATE`
pattern nor a tracker id, and that the source node's `.description`
equals the projection.

Reverse verification (the fix committed first; one-off, no file kept):
`view.zod.ts` restored to its `4e4111ca0` bytes with `git restore
--source` (tree only; blob `e60abeaa9` == the base blob; old-marker
count 1, new-marker count 0), the file ran **2 failed / 52 passed**, the
two new cases by name. Restored with `git checkout HEAD --` (blob
`48b21f8d9` == HEAD; `git diff HEAD` empty; porcelain clean), **54
passed**. The pin imports the schema from `src` by relative path, so no
dist leg applies.

## Verification (at `acba7086d`, under `os-verify-lock` on a shared box)

- `pnpm --filter @objectstack/spec build`: exit 0 (held 128s).
- The eight spec test files that pin `submitBehavior` (incl.
`scripts/nested-shape.test.ts`): 8 files, 700 tests passed.
- `pnpm --filter @objectstack/spec typecheck` (tsc,
`check:scripts-typecheck`, `check:test-typecheck`, which compiles the
test layer): exit 0.
- `check:generated`: 15 of 15 artifacts up to date.
- `eslint --no-inline-config --format json` on the two changed TS files:
2 files, 0 errors, 0 warnings. Population: `eslint.config.mjs` `files:
**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}` minus `NEVER_LINTED`; no
`parserOptions.project` (no type-aware linting), so this diff cannot
move the verdict on any untouched file.
- Gates: `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` at `acba7086d` derived 114 commands (6
paths, +67/−16 vs merge base `4e4111ca0`). Each ran with its exit code
captured before any pipe: 110 exit 0 (three docs gates,
`check:doc-formula-expressions`, `check:doc-security-posture` and
`check:docs-transcript-drift`, first refused on an unbuilt
`@objectstack/lint`; after `turbo run build
--filter='@objectstack/lint...'` under the lock they ran green); 3 exit
3 PREREQUISITE NOT MET, read from CI (`check:skill-examples` needs the
client-react dist, `check:dual-build-cjs-loads` and
`check:lean-entry-closure` need every package's dist);
`check:type-check-debt` claimed NOT-MEASURED (its re-measure is a
whole-workspace build; CI). `--ran` reconciliation: 114 derived, 110
run, 4 NOT-MEASURED, 0 UNRUN. The 49-row artifact-roster block also ran:
45 exit 0; 3 exit 2 NOT WIRED without PR context
(`check-closing-target-claim`, `check-partof-closing-keyword`,
`check-single-claim-paths`; the Part-of/closing-keyword gate re-run with
`PR_BODY` set to this body: exit 0); `check:published-readme-exports`
exit 3 (every package's dist; CI).

## 维护者速读(草稿)

**改了什么:** 表单视图 `submitBehavior` 属性的帮助文案(Studio 属性面板、参考文档、发布的
`objectstack-ui` 技能里显示的是同一句)不再以「(ruled 2026-08-11)」结尾;规则本身一字未改。裁决日期与
objectstack-ai#7496 进了代码注释。`react-blocks.md`、`view.mdx`、`protocol.mdx`
的对应行由生成器重生成,`react-blocks.md` 那一行因此不再被截断。

**为什么改:** 这是 objectstack-ai#22093 的最后一项:作者看到的帮助文案里不该出现内部裁决日期。其余 22 行已在 PR objectstack-ai#22125
落地;这一行投影到受管的发布技能目录 `skills/**`,所以单独成 PR、等维护者批准。

**风险与代价(含回滚):** 纯文案,不改接受/拒绝行为、键、类型或导出;`@objectstack/spec` patch
变更集。`react-blocks.md` 行数不变、净减 9 字节。回滚 = revert 本 PR。

**席位意见:** (席位填写)

**你要做的:** 批准(approve)本 PR 一次;之后由席位落地。

## Acceptance notes

- `packages/spec/scripts/lib/generated-output.ts` records that the
governed-merge register carves generator-owned files inside `skills/**`
out of the human-merge fork when the queue leg reproduces them
byte-exact. Whether that applies to this PR is the seat's read; this PR
follows the dispatch route (draft, Tier H). Noted, not filed.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 9, 2026
… cited decision in words instead of a tracker number (stage 29) (objectstack-ai#22376)

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

Stage 29 of this card: the next area of class (e), the test strings
shipped under `packages/spec/src`, as ruled in `5902360492` on objectstack-ai#20513.
This stage takes the second name-ordered group of the test files
directly in `packages/spec/src/`: 15 files,
`root-entry-migrations-split.pin.test.ts` through `stack.test.ts`. 12 of
them carried 57 tracker ids in test titles (59 ids), one of which sits
in a declared describe label rather than a literal title. All 57 now
either state what their record decided, in words (form D), or drop the
number where the title already says it. Text only: no assertion,
identifier, test count, code comment, file name or non-test file
changes. No file directly in `src/` carries an id in a string any more
(`type-alias-convention.pin.test.ts`, which sorts after the group, reads
0); the card stays open for the subdirectory files named under
Acceptance notes.

## Census (re-taken first)

The instrument is stage 28's `census28.cjs`, byte-identical (md5
`31d8488b5194b8d3048e3fcaec0efaed`, the value stage 28 published): an
AST walk over the `packages/spec/src` test files, one message per folded
string (a lone literal, a template, or a plus chain) that matches the
gate's id pattern, a title when the folded root is argument 0 of a
describe / it / test / suite / bench call, comments never read. It was
run against the three published readings before it was trusted, and all
three reproduce exactly: 128 messages / 130 ids in 37 files at stage
27's landing `f7b8a5932b`, 191 / 200 in 53 files (titles 162 / 168,
other 29 / 32) at `aa09db58c9`, and 73 / 76 in 24 files at stage 28's
landing `b7e01fbbd`.

| reading | messages / ids | files |
|:--|--:|--:|
| the group, base `43fc50051` | 57 / 59 (titles 56 / 58, other 1 / 1) |
12 of 15 |
| class (e) whole, base `43fc50051` | 73 / 76 (titles 61 / 64, other 12
/ 12) | 24 |
| class (e) whole, this head | 16 / 17 (titles 5 / 6, other 11 / 11) |
12 |
| the group, this head | 0 / 0 | 0 of 15 |

The group reads 57 / 59, stage 28's landing figure: no re-cut. The
census at `3f80f1716` (the dispatch base) and at stage 28's landing read
the same per file as at `43fc50051`.

Per file, messages at base: root-entry-migrations-split.pin 1,
stack-artifact-crossref 11 (12 ids), stack-artifact-packages 3,
stack-cross-reference-envelope 2 (one title, one declared label),
stack-dev-logins 4, stack-email-template-locale-floor 6,
stack-inline-action-crossref 13, stack-json-stage-package-body 4,
stack-refusal-envelopes 2, stack-requires 4 (5 ids),
stack-top-level-strict 5, stack 2. `stack-conversions-record`,
`stack-duplicate-action-key` and `stack-provenance` carry no id in any
literal.

Controls:
- Pathspec: the 15 named paths hit the control word `describe(` in 15 of
15 files, and a nonsense word in none; the census scanned 15 of 15.
- Lit outside the group, at base and head: `ai/build-progress.test.ts` 2
/ 2 and `ui/dashboard.test.ts` 2 / 2.
- Lit at base and dark at head, inside the group:
`stack-artifact-crossref` 11 / 12, `stack-inline-action-crossref` 13 /
13, `stack-requires` 4 / 5 and `stack-cross-reference-envelope` 2 / 2
each read 0.
- Dark at both ends: `stack-conversions-record` has no id anywhere;
`stack-duplicate-action-key` and `stack-provenance` carry an id in one
comment line each and read 0. The 15 group files read 0 at head while 55
of their comment lines still carry ids (112 id-bearing lines at base,
comments included, 14 files).
- Planted, in a scratch tree: an id in a describe title, a plus-chain
title, an expect message, an it.each row name, a template title and a
declared string each read once (6 / 6); a comment, a six-digit number,
an HTML entity and a markdown heading read 0.

## What changed

57 literals, one line each, in 12 files: +57 / -57. Every file keeps its
line count.
- 6 state the decision in words: "a modal target names a page, only" for
the inline and the embedded modal-target refusals
(stack-inline-action-crossref 133 and 330); "it was never built by a
stack producer" for the hand-built stack object composeStacks refuses
before either artifact-scoped rule runs (stack-artifact-crossref 380);
"compatibility with the multi-package artifact" (stack-artifact-crossref
270); "unchanged from before the dangling-`objectName` refusal"
(stack-inline-action-crossref 399); and the declared label
"hooks[].object (the hook-ownership rule)", the rule ADR-0130 states as
"the split must follow hook ownership" (stack-cross-reference-envelope
91).
- 4 get their subject back where the number was the only subject: "the
no-floor report: warn-once bookkeeping" and "the floor guard's declared
scope boundary" (stack-email-template-locale-floor 159 and 179), "the
strict top-level door: the accept side does not move" and "defineStack
surfaces the unknown-top-level-key refusal" (stack-top-level-strict 173
and 225).
- 47 drop a number the title already explains, with its connector: a `#N
— ` or `#N ` prefix, a `[#N] ` prefix, or a trailing `(#N)`. That
includes the twin title `stack-artifact-packages.test.ts:355`, which
read `does not warn about an undeclared composition rule (objectstack-ai#5005 rule 3)`
and now reads `does not warn about an undeclared composition rule`, the
same text stage 28 gave its copy at
`compose-stacks-manifest-preserve.test.ts:308`.

## Cited records

26 distinct ids. 25 answer 200 and were read: the body and the comment
thread as the API serves it, with the rulings and landings that the
rewritten titles state read in full. objectstack-ai#10485 answers 404; its decision
was read from its landing commit `35ad101bc` (the `themes` carrier
retired under ADR-0049 enforce-or-remove; `app.branding` is the one
colour surface) and from the unknown-key strictness ledger, which
records the maintainer's 2026-08-21 disposition as retiring the
authorable surface, the phrase the title keeps verbatim. The comments
counter of four records differs from what the comments endpoint serves
(objectstack-ai#14122 serves 21 of 22, objectstack-ai#17614 5 of 6, objectstack-ai#16449 0 of 5, objectstack-ai#18056 5 against
a counter of 4); everything served was read, and objectstack-ai#16449's decision is in
its body.

What each record decided, as the titles now say it:
- objectstack-ai#6739, ruling A (2026-08-09): a `type: 'modal'` target names a page,
only; the object fallback is consumer leniency and enters retirement.
- objectstack-ai#20367, ruling B: one authoring shape; `defineStack` and
`composeStacks` stamp a provenance mark and `composeStacks` refuses an
unmarked input with `STACK_PROVENANCE_MISSING` / 422.
- objectstack-ai#7456, Option A: the embedded `objectName` gets the registered rule's
existence check, and only a dangling name newly refuses.
- objectstack-ai#19926, ruling A: `packages: null` is malformed, not absent.
- objectstack-ai#19784, ruling C: a non-array concat key is refused with the envelope.
- objectstack-ai#5005 (2026-08-04): same value passes, a conflict throws a
prescriptive error, an undeclared top-level key warns.
- objectstack-ai#8687, Shape B: the top-level stack door is strict, and the lint
yields to it.
- objectstack-ai#14122 / objectstack-ai#18202: one artifact carries N packages; the artifact-scoped
permission and seed references resolve against the artifact, a stack
that does not opt in is untouched.
- objectstack-ai#17518, ruling A prime: the artifact-stage and record-stage package
bodies are declared beside the assembled one.
- objectstack-ai#6889 and objectstack-ai#7397: inline page-element actions and object-embedded
actions get the registered action rule's target cross-references.
- objectstack-ai#17063: the `type: 'page'` list-view mount is retired. objectstack-ai#17556: an app
declares its own `devHint` / `devLogins`. objectstack-ai#17614 and objectstack-ai#18056: the
no-`en-US`-floor report and its declared scope. objectstack-ai#14552 and objectstack-ai#15963: every
`defineStack` refusal carries an ADR-0112 envelope; objectstack-ai#16449 registers
each shipped code in the ledger. objectstack-ai#3265 and objectstack-ai#3308: `requires` is
validated at the producer and the camelCase aliases are removed. objectstack-ai#14153
and objectstack-ai#20332: trigger capability validation and its `triggers` without
`automation` pair. objectstack-ai#20646: the root entry stops carrying the migration
registries. objectstack-ai#2611: named import mappings are wired into the import path.

## Readers

173 needles (each old literal, a 24-character window around each id, and
the text left and right of each id) were searched with `git grep -F`
over the tracked tree at the base outside the 15 files, with a lit
control (`composeStacks` in `stack.zod.ts`, hit) and a dark control (a
nonsense string, no hit). One needle hit, one line: a `//` comment in
`packages/qa/dogfood/test/fixtures/override-composite-fixture.ts` that
quotes the tail of one title with its id. It is a comment, not a reader.
The only reader of the declared label is the file's own
`describe(row.label, ...)`. No group file reads a test name
(`currentTestName`, `expect.getState`, snapshots: 0). The 15 file names
appear elsewhere only in prose and comment lines, in
`packages/spec/test-typecheck-debt.json` (keyed by file and error text,
not by title) and in two `dispatch-gates.mjs` /
`check-changeset-no-major.mjs` fixtures that name a path or a changeset
file. Every old literal occurs exactly once across the 15 files (its own
site).

## Text-only proof

- `textonly28.cjs` (stage 28's, md5 `957eff6b3837d762b8e03d070155930a`),
per file, base text against head text: both texts with every string
token masked are byte-equal (code and comments), the string-token counts
are equal, and every changed token sits in a describe / it / test title
position or on a declared line, carried an id at base and carries no `#`
plus digits at head. 15 / 15 files SAME; 57 changed tokens, 56 titles
and 1 declared label (run with `--declared 91`); per-file counts exactly
as predicted in writing at 2026-10-08T22:57Z, before the edit. The 3
untouched files read SAME with 0 changed.
- 17 / 17 controls on scratch copies of the head files behave as
predicted: an identifier rename, a numeric literal, a comment edit, a
non-title string given an id, a rewritten title given a new id, a title
that was id-free at base edited, one rewritten title reverted to base
(SAME, 0 changed), an it.each title given an id, an expect message
changed, a title re-split into a plus chain, a template title given an
id, a test added, an id appended to a rewritten title in a many-title
file, an untouched file (SAME, 0 changed), the declared label run
without `--declared` (refused), the declared line plus another changed
string (refused) and the declared line alone (SAME, 2 changed).
- `git diff -U0`: 57 plus and 57 minus lines, each the planned line; 0
added lines carry `#` plus digits; control-byte scan of the 12 files and
of this body: 0 hits.

## Tests

- The 15 files with `--project local --project repo` and the JSON
reporter, at the base (same worktree, before the edit) and at the head
`65b42b7d1`: 405 / 405 passed in 15 files, 108 suites each side;
per-file test count and status sequence identical in 15 / 15; 229 full
names changed, 0 mismatches against the plan (each head name equals the
base name with the planned replacement applied); 0 duplicate full names
either side; names with `#` plus digits 229 at base, 0 at head.
- spec `vitest run --project local`: 626 files passed, 18740 tests
passed | 1 todo, exit 0.
- spec `typecheck`: `VERDICT command-exit 0`; `check:test-typecheck` 52
files / 246 errors / 135 pinned signatures held, unchanged; `tsc
--listFilesOnly -p tsconfig.test.json` holds 15 of 15 group files.
- turbo build of all packages before the gates: `Tasks: 71 successful,
71 total`. Heavy runs went through `scripts/pm/os-verify-lock.sh` (slot
`dev-20749-s29`).
- No reverse verification or ablation applies: no assertion, gate or
reader moved.

## Gates

All at `65b42b7d1`. The dispatch list (77 commands, derived at
`3f80f1716`) equals the list re-derived from the real change set at this
head, 12 paths against merge base `43fc50051` (114 changed lines):
nothing added, nothing dropped. All 77 exit 0 and `dispatch-gates --ran`
reads "77 derived, 77 run, 0 NOT-MEASURED, 0 UNRUN". Also run, exit 0:
the five roster families that share a directory with the diff
(`check:meta-url-spelling`, `check:spec-changes`,
`check:authz-resolver`, `check:error-code-casing`,
`check:filter-alias-parity`) and `check:generated` ("All 15 generated
artifacts are up to date", measured against the dist the full build
wrote at this head). ESLint, narrowed and proven: 15 files linted with
`--no-inline-config`, 0 errors, 0 warnings; the population read from
ESLint's own config is 15 configured, 0 ignored; no file has
`parserOptions.project` or `projectService`, so type-aware linting is
not enabled and this diff cannot move the verdict of a file it did not
touch.

## Packaging and landing

- No changeset: `skip-changeset`. `npm pack --dry-run --json
--ignore-scripts` in `packages/spec` lists 2069 files, none of the 12
changed files and no `*.test.ts`; controls `src/stack.zod.ts`,
`dist/index.mjs` and `package.json` are present; an old and a new title
phrase read 0 files in `dist/`, the control `Unrecognized key` reads 42.
- Not governed: `check-governed-merges --test` on the 12 paths reads 0
of 12; 114 changed lines.
- `git merge-tree` onto `origin/main` `6a53564b9`: clean. None of the 17
open PRs touches any of the 15 files (file lists read; 6 of them touch
`packages/spec/src`, so the scan sees spec files).

## Acceptance notes

- Class (e) is not finished: 16 messages / 17 ids remain in 12 files,
all in subdirectories. By directory: `ui/` 9 / 9 in 7 files,
`automation/` 2 / 3 in 1, `ai/` 2 / 2 in 1, and `api/`, `contracts/` and
`kernel/` 1 / 1 in 1 each. Their plan is the cutting seat's: stage 27's
landing sets the needles stage apart (one stage, with an at-tier review)
and keeps `ui/`'s four colour literals as decided, and this stage
measured neither.
- Regrowth for stage 30's plan: five subdirectory test files carry 6
messages / 7 ids that were added after stage 27's landing. The files are
older; the ids entered their strings in three commits.
`api/meta-item-response-shapes.test.ts` 1 / 1 (a title, from objectstack-ai#22126);
`automation/builtin-node-config.test.ts` 2 / 3 (two titles, from objectstack-ai#22259,
which re-added ids to a title stage 10 had rewritten);
`kernel/manifest.test.ts` 1 / 1, `ui/action-description.test.ts` 1 / 1
(titles) and `ui/view-submit-redirect-url.test.ts` 1 / 1 (an `other`
string, to be needle-checked there), all three from objectstack-ai#22125. Nothing
regrew since stage 28's landing: the per-file census at `b7e01fbbd`
equals the one at `43fc50051`. Test files are outside
`check:doc-authoring`'s ledgered leg, so nothing refused these on
arrival. Not edited here.
- Code comments in the 15 files still carry live ids (55 lines).
Comments are not this card's share. Carrier: none.
- `origin/main` moved from `43fc50051` to `6a53564b9` while this ran;
one commit touches `packages/spec` (`api/error-code-ledger.zod.ts`, none
of the 15 files), so nothing was merged into the branch.

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

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 9, 2026
…n in words instead of a tracker number (stage 30) (objectstack-ai#22414)

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

Stage 30 of this card: the class (e) remainder, the test strings shipped
under the `packages/spec/src` subdirectories, as ruled in `5902360492`
on objectstack-ai#20513. The census at the base reads 17 messages / 18 ids in 12
files. This stage rewrites 7 of them (6 titles and 1 expect message, 8
ids) in 5 files: each now states what its record decided, or drops the
number where the title already says it. The other 10 messages / 10 ids
stay, 4 because earlier stages decided they are not citations and 6
because an assertion matches the string against text this claim does not
let the stage edit; both groups are named under "What stays". Text only:
no assertion, identifier, test count, code comment, file name or
non-test file changes. No file is deferred.

## Census (re-taken first)

The instrument is stage 28's `census28.cjs`, byte-identical (md5
`31d8488b5194b8d3048e3fcaec0efaed`, the value stages 28 and 29
published): an AST walk over the `packages/spec/src` test files, one
message per folded string (a lone literal, a template, or a plus chain)
that matches the gate's id pattern, a title when the folded root is
argument 0 of a describe / it / test / suite / bench call, comments
never read. It was run against the three published readings before it
was trusted, and all three reproduce exactly: 128 messages / 130 ids in
37 files at `f7b8a5932b`, 191 / 200 in 53 files at `aa09db58c9`, 73 / 76
in 24 files at `b7e01fbbd`.

| reading | messages / ids | files |
|:--|--:|--:|
| base `11d119ab1` | 17 / 18 (titles 6 / 7, other 11 / 11) | 12 |
| stage 29's landing `0ef9029da` | 17 / 18, per file equal to the base |
12 |
| this head | 10 / 10 (titles 0 / 0, other 10 / 10) | 7 |
| the 5 edited files, this head | 0 / 0 | 0 of 5 |

Stage 29's ACCEPT carried 16 / 17 (ui 9 / 9 in 7 files, automation 2 /
3, ai 2 / 2, api 1 / 1, contracts 1 / 1, kernel 1 / 1). The base reads 1
/ 1 more, all in `ui`: the title `carries no ruling date and no tracker
id (objectstack-ai#22093)` at `view-submit-redirect-url.test.ts:341`, which PR objectstack-ai#22322
(`ad381fd94`, landed 2026-10-09T00:35Z) added after stage 29's head. So
`ui` reads 10 / 10 in 7 files, and nothing else moved. The census at
`origin/main` `e75dceddd` (8 commits past the base, none touching the 12
files) reads the same 17 / 18 in the same 12 files, so nothing regrew
while this stage ran. The reading is within one message of the claim's,
so there was no re-cut.

Per file, messages at base: `ai/build-progress` 2,
`api/meta-item-response-shapes` 1, `automation/builtin-node-config` 2 (3
ids), `contracts/approval-service` 1, `kernel/manifest` 1,
`ui/action-description` 1, `ui/component-props-unknown-members.pin` 1,
`ui/dashboard-chart-structure-refusal` 2, `ui/dashboard` 2,
`ui/notification` 1, `ui/strictness-batch14` 1,
`ui/view-submit-redirect-url` 2.

Controls:
- Pathspec: the 12 named paths hit the control word `describe(` in 12 of
12 files and a nonsense word in none; the census scanned 12 of 12.
- Planted, in a scratch tree: an id in a describe title, a plus-chain
title, an expect message, a template literal, a cross-repo spelling and
a ledger-style string each read once (6 / 6); a comment, a six-digit
colour, an HTML entity, a two-digit number and a hex colour with a
letter read 0.
- Lit and dark inside the group: the 5 edited files read 1, 2, 1, 1 and
2 messages at base and 0 at the head; the 7 untouched files read the
same at both ends.

## Deferral

At the census (2026-10-09T03:03Z) 17 PRs were open; at the re-scan
before opening this PR (04:13Z), 13. Every file list was read through
REST (605 and 593 rows). None touches any of the 12 files: lit control
`api/protocol.test.ts` (PR objectstack-ai#22323) found, dark control 0. Of the four
PRs the claim named, objectstack-ai#22380 has landed and objectstack-ai#22315, objectstack-ai#22323 and objectstack-ai#22215 are
open; none of them touches a file in this group. Deferred files: none.

## What changed

7 literals, one line each, in 5 files: +7 / -7. Every file keeps its
line count.
- `api/meta-item-response-shapes.test.ts:226`: the `[objectstack-ai#22114] ` prefix
goes; the title already says what objectstack-ai#22126 landed, that the read serves
the version token and the 409 carries the current one as data.
- `automation/builtin-node-config.test.ts:434`: "a CEL envelope beside
literals; the `{token}` dialect retired". objectstack-ai#14149's ruling A made an
assignment value a CEL envelope beside literals, and objectstack-ai#19939 retires the
`{token}` dialect in flow value slots; the title already stated both, so
only the two numbers go.
- `automation/builtin-node-config.test.ts:485`: the `[objectstack-ai#19939] ` prefix
goes from the REFUSES title.
- `kernel/manifest.test.ts:681` and `ui/action-description.test.ts:235`:
the trailing `(objectstack-ai#22093)` goes. objectstack-ai#22093 decided that author-visible help
and refusals carry no service-interface name, ruling date or foreign
example id, and both titles already say what their bodies pin.
- `ui/view-submit-redirect-url.test.ts:341`: the trailing `(objectstack-ai#22093)`
goes from the title.
- `ui/view-submit-redirect-url.test.ts:118`: the expect message `states
the rule, not its ruling date (objectstack-ai#22093)` drops the number. It is an
assertion's failure message, so it was needle-checked first (below) and
is the one declared non-title string.

All seven are "drop a number the title already explains". None needed a
rewrite in new words, because each title already carried the decision.

## What stays, and why

10 messages / 10 ids in 7 files, none edited.

Four are CSS hex colours, not citations. `colors: ['objectstack-ai#111', 'objectstack-ai#222']` at
`ui/dashboard-chart-structure-refusal.test.ts:94` and `palette: ['objectstack-ai#111',
'objectstack-ai#222']` at `ui/dashboard.test.ts:124` are fixture input the schema
under test reads. Stage 21's ACCEPT (`6001279159`, decision A) kept them
by file and line, and every later stage carried them forward.

Six are strings that an assertion matches against text outside this
stage's edit surface. Moving one at the same strength means editing a
non-test source docblock (and, for the first two, its generated
reference page) or the assertion that matches it. The claim forbids both
and says to stop and report, so none is touched; `open_questions` in the
report carries the decision.
- `ai/build-progress.test.ts:236` `'cloud#2172'` and `:237`
`'objectui#7388 block 2'`: `toContain` over the source text of
`ai/build-progress.zod.ts` (docblock lines 8, 27 and 85), which
`content/docs/references/ai/build-progress.mdx` renders.
- `contracts/approval-service.test.ts:274` `'objectstack-ai#16495'`: `toContain` over
the docblock above `continueRestoredRun` in
`contracts/approval-service.ts` (line 999).
- `ui/notification.test.ts:123` `'// [objectstack-ai#4610]'`: the locator of the
tombstone note in `ui/notification.zod.ts:94`; the file's own
`toMatch(/^\[objectstack-ai#4610\]/)` at `:134` reads the same note.
- `ui/strictness-batch14.test.ts:395` `'objectstack-ai#5015'`: `toContain` over
`ui/notification.zod.ts` and `ui/sharing.zod.ts`.
- `ui/component-props-unknown-members.pin.test.ts:322` `ruling:
'decision card objectstack-ai#21704, fork 4, letter B (record 5979239990)'`: the
file's own assertion at `:417` matches the value with `/objectstack-ai#21704/`. Stage
20's ACCEPT (`5998488373`) kept it for this reason and sent it to the
needles' stage.

Readers of the seven rewritten strings: none. `git grep -F` at HEAD over
the tracked tree outside the 12 files, with the full literal, a
24-character window around each id, and the text on each side of each id
(29 needles over all 17 sites): the only hits are the readers of the
kept strings named above, the lit control (`composeStacks` in
`stack.zod.ts`) hits and the dark control does not. The same needles
searched inside the 12 files, outside each literal's own span: the only
hits are two code comments beside `:322`. The five short needles
(`cloud#2172`, `objectstack-ai#16495`, `// [objectstack-ai#4610]`, `objectstack-ai#5015`, `objectstack-ai#21704`) fall under the
script's 12-character floor, so their readers were confirmed by direct
`git grep -F` with a dark control.

## Cited records

Read with their comments as the API serves them: objectstack-ai#22114 (8 of 8
comments; landed as PR objectstack-ai#22126), objectstack-ai#14149 (12 of 12; ruling A `5507504961`,
landed as PR objectstack-ai#15113), objectstack-ai#19939 (15 of 15; pass 1 landed as PR objectstack-ai#22259, the
card stays open), objectstack-ai#22093 (17 of 17; PRs objectstack-ai#22125, objectstack-ai#22309 and objectstack-ai#22322), and
the four PRs themselves. objectstack-ai#19939 is still open: its pass 1 refuses the
`{token}` dialect in flow value slots and keeps two spellings (the date
macros and `{$User.*}`) until CEL can write them. The describe's
PRESERVATION test still accepts those two, and the title keeps the
record's own verb, "retired", as PR objectstack-ai#22259 wrote it. The title and the
test body say the same thing the record says.

## Verification

At head `8885dbf1c` (one commit on base `11d119ab1`):
- **Text only.** `textonly28.cjs` (stage 28's, md5
`957eff6b3837d762b8e03d070155930a`) on all 12 base copies against their
heads: 12 / 12 SAME. 7 changed tokens, as predicted in writing at
2026-10-09T03:08Z before any edit or test run: 6 titles and 1 declared
string (`--declared 118`). Every other string token, identifier, number,
punctuation mark and comment is byte-equal. 16 controls, expectations
written in the script before the first run, 16 / 16 as predicted: an
identifier rename, a numeric literal, a comment edit, an undeclared
expect message, a rewritten title given a new id, an id-free title
edited, one title reverted to base (SAME, 0 changed), a declared label
without `--declared`, a declared line plus another changed string, the
declared line alone (SAME, 2 changed), a title re-split into a plus
chain, a test added, an untouched file (SAME, 0 changed), an id appended
to a rewritten title, a kept needle rewritten, a kept hex colour
rewritten. The two controls that mutate a string beside the declared
line fail at the mutated line, not at 118.
- **Tests, 12 files, base and head.** `--project local --project repo`
with the JSON reporter, 618 tests in 88 suites each side, all passed.
Per-file test count and status sequence identical in 12 / 12. 23 full
names changed (4 + 14 + 1 + 3 + 1), 0 mismatches against the plan. Names
carrying `#` plus digits: 23 at base, 0 at head. Duplicate full names: 3
and 3, the same three `[object Object]` it.each rows at both ends.
- **Full spec unit tier at the head**, under the verify lock: `Test
Files 626 passed (626)`, `Tests 18743 passed | 1 todo (18744)`.
- **Build and typecheck**, under the verify lock: `turbo run build` over
`packages/*` and `packages/*/*`, `Tasks: 71 successful, 71 total`;
`@objectstack/spec` `typecheck` exit 0 with `check:test-typecheck`
holding 52 files / 246 errors / 135 pinned signatures, the same figures
as stage 29; the 12 files are all in the `tsconfig.test.json` program.
- **Gates.** `dispatch-gates.mjs --commands` at the head derives 79
(stage 29's 77 plus `check:authorable-surface` and
`check:yaml-examples`); all 79 exit 0, and `--ran` reconciles 79
derived, 79 run, 0 NOT-MEASURED. The five artifact-roster families that
keep their roster in a directory one of the paths is in
(`check:meta-url-spelling`, `check:spec-changes`,
`check:authz-resolver`, `check:error-code-casing`,
`check:filter-alias-parity`) and `check:generated` (all 15 artifacts up
to date) also exit 0.
- **ESLint**, `--no-inline-config`, 12 files: 0 errors, 0 warnings.
Population from ESLint's own config: 12 configured, 0 ignored, 0 with a
type-aware parser option, so this diff cannot move the verdict of a file
it does not touch.
- **Skip-changeset.** `npm pack --dry-run --json --ignore-scripts` in
`packages/spec`: 2069 files, 0 `*.test.ts`, 0 of the 5 edited files;
controls `src/stack.zod.ts`, `dist/index.mjs` and `package.json`
present. The rewritten expect message occurs in 0 files of `dist/`; the
control `Unrecognized key` occurs in 42. Nothing published changes.
- **Governed.** `check-governed-merges.mjs --test` on the 5 paths: 0 of
5, not governed; 14 changed lines.
- **Merge.** `git merge-tree --write-tree` onto `origin/main`
`e75dceddd`: clean.
- **Bytes.** 0 added lines carry `#` plus digits; 0 control bytes in the
changed files.

Declared narrowing: the 12-file base and head comparison ran outside
`os-verify-lock.sh`, after three queue turns (about 28 minutes) ended
without a grant. It is a 12-file run with two workers; the workspace
build, the typecheck and the full unit tier all ran under the lock. The
gates are `check:*` runs, which do not use the lock.

## Acceptance notes

- **Regrowth continues.** Since stage 27's landing, four PRs (objectstack-ai#22125,
objectstack-ai#22126, objectstack-ai#22259 and objectstack-ai#22322) added 7 messages / 8 ids to test strings in
files that already existed, one of them to a title objectstack-ai#22322 wrote while
stripping a ruling date from a describe. Test files sit outside
`check:doc-authoring`'s ledgered leg, and the ruling adds no gate, so
the per-stage census is the only instrument. An observation about the
burn-down's denominator, not a class a / b / c finding.
- **Comments are untouched.** Code comments in these files still cite
ids (for example `// ─── assignment (objectstack-ai#14149) ───` at
`builtin-node-config.test.ts:432`); comments are objectstack-ai#20234's share.

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

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants