Skip to content

fix(runtime): POST /automation/:name/toggle joins the manage_metadata write set - #11660

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-10243-toggle-manage-metadata
Aug 24, 2026
Merged

os-zhuang merged 1 commit into
mainfrom
claude/issue-10243-toggle-manage-metadata

Conversation

@claude

@claude claude Bot commented Aug 24, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #10243

Implements the maintainer ruling of 2026-08-23 (comment 5386676024), quoted unmodified:

「10950 不考虑存量,其他接受你的建议」

Ruled: A — POST /api/v1/automation/:name/toggle joins the manage_metadata write set. One arm on the existing isFlowAuthoringWrite predicate; ⛔ no new capability name (option C was declined on the startup-scope axis). Nothing here re-opens the decision.

The breaking surface, stated plainly

caller before after
authenticated with manage_metadata 200, flow toggled 200 — unchanged
authenticated without it 200, flow toggled 403 PERMISSION_DENIED, toggleFlow never entered
anonymous 401 401 — unchanged (#5519 floor answers first)
engine self-invocation (isSystem) 200 200 — unchanged

The changeset is minor with this table spelled out. The execution doors did not move: POST /:name/trigger, the legacy POST /trigger/:name and POST /:name/runs/:runId/resume keep their posture, and GET /_status still serves enablement to any authenticated caller — this narrows mutating the bit, not observing it.

The fix is one arm, and it is matched to the router rather than approximated

if (method === 'POST' && parts[1] === 'toggle') return parts[0] !== 'trigger';

Two boundary decisions, both deliberate, both pinned by their own test:

  • No upper bound on depth. The router's toggle arm tests parts[1] === 'toggle' with no length check, so POST /automation/:name/toggle/anything still reaches toggleFlow. A gate written parts.length === 2 would have been narrower than its own route — a bypass, not a style difference.
  • parts[0] === 'trigger' is excluded. POST /automation/trigger/:name is the legacy execution door and the router answers it above the toggle arm, so for a flow literally named toggle the path /automation/trigger/toggle RUNS that flow. Gating it would over-block an execution door — the one thing the ruling did not do, and the mistake finding: GET /automation/:name/runs/:runId/screen still discloses record-derived values to any authenticated caller who knows a run id #7968 records for the paused-run screen read.

Five surfaces carried the old policy. Seven moved.

The ruling named two pins. The claim comment added two prose surfaces. Implementing it turned up two more that would have gone red — not false prose, actually failing tests:

# surface what moved named in dispatch?
1 packages/runtime/src/domains/automation.ts the predicate + its docblock, the route map, the gate call-site comment the fix
2 .../automation-write-capability-gate.test.ts audit pin flipped; toggle joins AUTHORING_WRITES so it inherits the whole battery ✅
3 packages/qa/dogfood/test/automation-toggle-tenant-scope.dogfood.test.ts re-pointed (below) ✅
4 packages/runtime/src/route-ledger.ts toggle removed from the "execution doors outside it" list; the bare toggle row gains a note in its siblings' voice ✅
5 packages/qa/dogfood/test/authz-conformance.matrix.ts "three flow-AUTHORING writes" → four; toggle removed from the execution-door list ✅
6 .../automation-toggle-unknown-flow.test.ts ⚠️ would have gone red — its CTX was { userId: 'user_1' } with no capability, so every #7535 error-mapping case would have stopped at the 403 in front of the behaviour it is named after ❌ found here
7 packages/runtime/src/http-dispatcher.test.ts ⚠️ would have gone red — the toggle routing case drove AUTHED_CALLER(); switched to the file's existing FLOW_AUTHOR(), and the FLOW_AUTHOR docblock's "every EXECUTION route (trigger / toggle / resume) keeps AUTHED_CALLER, deliberately" is now false and was corrected ❌ found here

For 6 and 7 only the caller changes — every mechanism, assertion and expected value is untouched, and each carries a comment saying so, matching how #7019 / #7033 / #10145 handled the same situation in the same files.

Also in surface 1: the handler's route map annotated ⚑ capability markers on the three #10145 authoring writes, which had none. They were not false, but a map that marks the toggle gate while leaving its three siblings unmarked reads as "those are ungated". Comment-only, declared here rather than slipped in.

What the dogfood pin measures now

⛔ Not deleted. Re-pointed, and which legs changed is the substance:

  • Flipped — the cross-tenant read-back table. That measured a consequence of the route being reachable; the unprivileged tenant is now refused at the door in both directions and all three principals still read the flow enabled: true. Asserting the spy-equivalent (state unmoved) matters: "toggle first, refuse second" would satisfy a status-only assertion and still be the defect.
  • Kept, deliberately — it('mutates ENGINE state, not the persisted definition'). The gate changed who may toggle, not where the bit lives. That leg is what made the original result independent of the harness's missing row wall (multiTenant: 'posture-only'), and it is still the leg that would fail loudly if enablement ever became org-stamped state a wall could scope. Only its driver changed — an entitled caller, since an unentitled one no longer gets that far.
  • Added — the positive control (an entitled caller still toggles, 200, both directions) and a leg proving the neighbouring execution door still answers tenant A.
  • The "tenant A is genuinely unprivileged" control deliberately still excludes toggle: a control has to be independent of the route under test.

Consumer inventory — measured, and it comes back zero

Axis ② of the accepted analysis flagged this as the one place option A could misstep:「「今天谁在调 toggle」没有被测量」. Measured now.

This repo — git grep -n "automation\.toggle\|toggleFlow(\|/toggle", all packages:

  • packages/client/src/index.ts:3202 — the SDK method automation.toggle itself. A library binding, not a UI caller; its test mocks fetch.
  • docs/qa/platform-checklist/areas/automation.json — a manual QA procedure that toggles a showcase flow. Runs as the seeded platform admin.
  • Everything else is oauth2/toggle-disabled (unrelated), UI filter vocabulary (dropdown/toggle), or CHANGELOG prose.
  • Zero callers in packages/console, packages/mcp, packages/rest, packages/cli, examples/*.

The Console UI — packages/console ships no tracked source (4 files: .gitignore, CHANGELOG.md, README.md, package.json), so the question is not answerable from this repo. Measured in the sibling repo instead — objectstack-ai/objectui at e52fac0:

  • git grep "automation/[^\"']*toggle|automation.toggle|toggleFlow"` ⇒ zero hits.
  • Reverse-check (a zero is never self-certifying): the same corpus returns 107 files matching automation, and its source POSTs to automation routes in three places — useConsoleActionRuntime.tsx:546 and RecordDetailView.tsx:802 (/trigger), FlowRunner.tsx:159 (/runs/:runId/resume). Both are execution doors and both stay ungated.
  • FlowStatusDot / StudioDesignSurface.tsx:3227 display { enabled, bound } read from GET /_status. Display only — no enable/disable control exists to lock anyone out of.

⇒ No console surface exposes flow enable/disable to ordinary members, so the over-block risk axis ② named does not materialize. The expected migration surface is programmatic SDK callers.

Anti-vacuity — the flipped pin shown failing without the fix

Prediction recorded before running: removing the arm makes exactly 4 cases in automation-write-capability-gate.test.ts fail — the AUTHORING_WRITES refusal case for toggle, the flipped audit assertion, the deeper-spelling case, and the gate-before-body case — while the toggle anonymous-401 case still passes (that floor is domain-wide and independent of this gate), and the positive control, the isSystem bypass and the legacy-execution-door case all still pass because an ungated route answers them the same way.

Measured: 4 failed | 22 passed (26) — and the same four cases, by name:

× POST /automation/:name/toggle (toggleFlow): 403 PERMISSION_DENIED, and the registry is untouched
× [#10243] the gate is ahead of the body checks on toggle too
× [#10243 FLIPPED] POST /:name/toggle is NO LONGER in this block - it is gated now
× [#10243] a deeper spelling cannot slip past the arm - `/:name/toggle/anything`

Predicted 4, measured 4, same identities. With the arm restored: Test Files 1 passed (1) / Tests 26 passed (26).

⚠️ The first attempt at this ablation was a no-op, and it is reported rather than quietly re-run. A perl -0pi -e "s/\Q...\E//" with an embedded newline inside \Q...\E matched nothing and exited 0. The on-disk check caught it — arm-count-after=1 — and the run aborted itself as Measurement void instead of reporting a green ablation over an unmutated file. The second attempt removed the line by exact string match and proved arm-count-before=1 → arm-count-after=0, numstat 0 1, with the neighbouring parts.length === 0 arm still present (so the mutation removed the arm, not the function). Restore confirmed by the trap: arm back on disk = 1, git status --porcelain empty.

Mutation proved on disk by grepping the arm text before (1) and after (0) — a zero-match perl -0pi exits 0, so the edit tool's exit code proves nothing. Restore ran under trap … EXIT INT TERM, and the fix was committed first so the restore had a real anchor. No rebuild was needed for this ablation and none is claimed: the audit pin imports ../http-dispatcher.js — a relative path inside the same package — so vitest compiles the mutated source directly and no dist/ sits between the mutation and the measurement.

Verification

All at commit fa0aa1aa, git status --porcelain empty. Every exit code captured before any pipe (cmd > log 2>&1; echo $?), and each gate quoted by its own verdict line, never a bare $?.

Touched test files, each run individually

file result
automation-write-capability-gate.test.ts Test Files 1 passed (1) · Tests 26 passed (26)
automation-toggle-tenant-scope.dogfood.test.ts Test Files 1 passed (1) · Tests 8 passed (8)
automation-toggle-unknown-flow.test.ts Test Files 1 passed (1) · Tests 6 passed (6)
http-dispatcher.test.ts Test Files 1 passed (1) · Tests 246 passed (246)
automation-body-validation.test.ts (drives toggle, untouched) Test Files 1 passed (1) · Tests 18 passed (18)
automation-register-error-class.test.ts (drives toggle, untouched) Test Files 1 passed (1) · Tests 9 passed (9)

pnpm --filter @objectstack/runtime typecheck and pnpm --filter @objectstack/dogfood typecheck both exit 0, and both logs echo > tsc --noEmit — checked deliberately, because a --filter matching zero scripts exits 0 and reads as a pass.

Gate union — derived with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack (no paths; the script takes its own change set from the merge base) and re-derived on the final diff: 8 paths, 20 path-matched families + 6 convention-triggered, unchanged between the two derivations. All run, all green. Verdict lines:

  • check-adr-0087-registration — ✓ 1 declared-breaking changeset(s), each carrying an ADR-0087 disposition · not-required (no-migration-prescription)
  • check:type-check-debt — OK — 32 ledger entr(ies) re-measured in 237.1s, 1898 raw tsc error(s) total, none above its recorded number — MEASURED, not refused, after the full closure build (Tasks: 71 successful, 71 total)
  • check:slot-lookup — ✓ ratchet holds: 107 unswept site(s) in 25 file(s), none new
  • check:engine-double-contract — OK · check:where-matcher — OK · check:query-options-erasure — ✓
  • check-cross-package-test-inputs — OK: 16 package(s) read outside themselves, all declared
  • check-ci-filter-parity — OK: all 95 declared cross-package glob(s) (80 unique) are covered
  • check:route-envelope — ✓ self-test passed · check:published-files — ✓ · check:test-source-alias — OK — 72 packages with tests scanned
  • check:type-source-resolution — OK — 77 packages with a tsconfig.json scanned · check:type-check-coverage — ✓
  • check-plugin-teardown-shape — ✓ 63 Plugin implementation(s) across 4583 source(s)
  • check-empty-changeset — ✓ No empty-frontmatter changeset introduced · check-changeset-no-major — ✓ no major bump · check:objectui-changeset — ✓ · check:changeset-gate-self-tests — ✓ 118 assertions
  • docs-audit/check-affected-docs — ✓ 381 cases pass · release-rehearsal-clone --self-test — ✓
  • spec liveness family — check:empty-state ✓ all classified, check:liveness ✓, check:strictness-ledger ✓ 60 file(s), check:variant-docs ✓ 18 discriminated union(s)
  • check:nul-bytes — ✓ 75 assertions, plus a manual control-byte grep over all changed paths: no hits

⛔ Not claimed: the repo-wide pnpm lint. CI owns that run. No other narrowing — every family the derivation named was run at the final head.


Generated by Claude Code


Generated by Claude Code

… write set

Implements the maintainer ruling of 2026-08-23 on #10243 (option A): flow
enablement is an authoring write, gated by one arm on the existing
isFlowAuthoringWrite predicate. No new capability name.

BREAKING: a caller holding a session but not `manage_metadata` is answered
403 PERMISSION_DENIED where it previously received 200 with the flow toggled.
The execution doors (trigger / legacy trigger / resume) and every read are
untouched.

The measurement that settled it: the enabled bit is not a row, so no
organization wall scopes it. toggleFlow writes an in-process map keyed by flow
name only, getFlowRuntimeStates() reads it with no caller and no organization,
and the automation service is one instance per environment — so an unentitled
tenant org owner switched a shipped flow off and an unrelated tenant in a
different organization, plus the platform admin, read it off.

The predicate matches the router exactly rather than approximately: no upper
bound on path depth (the router's toggle arm has none, so parts.length === 2
would have been a bypass), and POST /automation/trigger/:name is excluded so a
flow literally named `toggle` keeps its execution door.

Both named pins are flipped deliberately in this commit, plus three prose
surfaces that could never go red (route-ledger, the authz conformance matrix,
the handler's own route map) and two test files that would have gone red
unannounced (automation-toggle-unknown-flow, http-dispatcher) where only the
caller changes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019siH5jDmk5hrayvfyojUqR
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/dogfood, @objectstack/runtime, touching 11 documentable anchor(s).

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

  • content/docs/api/client-sdk.mdx (via automation.trigger (sdk))
  • content/docs/api/plugin-endpoints.mdx (via /automation/trigger/:name (route), /trigger/:name (route))

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

  • content/docs/releases/v17.mdx (via /:name/toggle (route), /automation/:name/toggle (route))

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
  • 2 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 45 of 222 client-bound route-ledger rows — the other 177 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run: node scripts/docs-audit/affected-docs.mjs --bridge-coverage

Coarse fallback — 24 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 cad8b42f009a4a703ccec6c58bdf2995f48f6362 → packageMentionDocs.

Which tree this was computed on

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

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

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

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Aug 24, 2026

Copy link
Copy Markdown
Contributor

ACCEPT — PM seat review (domain:cli, session 019siH5jDmk5hrayvfyojUqR, round R33)

CI at fa0aa1aa: 30 of 36 complete, nothing red; Test Core (1/6), (2/6), the three
Dogfood Regression Gate shards and Lint & Repo Gates still running. Arming when they land, ⛔ not
before, and I will not re-review then.

The surface count went 2 → 5 → 7, and the last two steps were the ones that mattered

The maintainer's ruling named two pins to flip. My dispatch measured five surfaces and made a
point of the two that were prose — a route ledger and a conformance matrix — because prose can never
go red, only become false.

You found two more, and both would have gone RED:

So the lesson I wrote into the dispatch — git grep the predicate, hand the dev the surfaces, not
the ruling's list
— was right in direction and still short by two. The general form, which I am
taking rather than the specific: a grep for the predicate finds the code that references it; it does
not find the tests whose fixtures assume the old answer.
Those are found by asking which callers
change status, not which files name the symbol.

⭐ And the handling was right: only the caller changes in both, every mechanism and expected value
untouched, each with a comment saying so, matching how #7019 / #7033 / #10145 handled the same
situation in the same files. That is a repair, not a test edit to get green.

⭐ The depth bypass — the sharpest thing in this PR

The arm is matched to the router, not approximated to it:

parts.length === 2 would have left POST /:name/toggle/anything reaching toggleFlow ungated
— a bypass

On a card that exists because a route was ungated, shipping a gate with an off-by-one that leaves a
neighbouring spelling ungated would have been the same defect wearing the fix's clothes. The router's
toggle arm tests parts[1] === 'toggle' with no length check, and the gate now matches that exactly.

The paired exclusion is equally well-reasoned in the other direction: parts[0] === 'trigger' is
excluded, because POST /automation/trigger/:name is answered above the toggle arm — so for a flow
literally named toggle, /automation/trigger/toggle runs that flow, and gating it would
over-block an execution door the ruling did not touch. Both boundaries pinned.

The consumer inventory — the axis-② risk, measured and cleared

The accepted analysis named this as the one place option A could misstep («「今天谁在调 toggle」没有被
测量»). Answer: zero callers, and the method is what makes it trustworthy:

  • packages/console ships no tracked source in this repo (4 tracked files, bundle gitignored), so
    the question is not answerable here — and instead of reporting a zero from a repo that cannot see
    the code, you read objectstack-ai/objectui at e52fac0 in a shallow read-only clone. Zero hits,
    reverse-checked: 107 files match automation there, and its three POST sites
    (useConsoleActionRuntime.tsx:546, RecordDetailView.tsx:802 → /trigger; FlowRunner.tsx:159 →
    /runs/:runId/resume) are all execution doors that stay ungated. FlowStatusDot only
    displays {enabled, bound}.
  • The one in-repo hit that could have mattered — the manual QA procedure in
    docs/qa/platform-checklist/areas/automation.json — runs as the seeded platform admin, and the
    dogfood positive control passing through that principal is what proves it still works.

So nobody is locked out on day one. That is the fact the ruling's weakest axis was waiting on.

⭐ The ablation aborted itself rather than reporting a false green

a perl -0pi -e "s/\Q...\E//" with an embedded newline inside \Q...\E matched nothing and exited
0. The on-disk grep caught it (arm-count-after=1) and the script ABORTED itself as 'Measurement
void'
instead of reporting a green ablation over an unmutated file.

This is the exact failure this lane keeps writing rules about — a zero-hit edit exits 0 — and it is the
first time this round the guard fired on itself and stopped rather than being caught downstream.
Reported rather than quietly re-run, which is the half that makes it useful. Predicted 4 red, measured
4, identities matched; positive control at both layers (an AUTHOR toggles 200 both directions,
isSystem bypasses, platform admin toggles 200 over real HTTP and the engine state actually moves).

The ruling's riders — both discharged

Changeset is minor with the 200 → 403 breaking surface spelled out as a table, plus the ADR-0087
disposition marker. And a platform fact worth keeping: check-adr-0087-registration reads the
changeset via git show HEAD:<file>, not the working tree, so a marker written to disk reads as
absent until it is committed.

Docs — checked, nothing to do

The drift check listed content/docs/api/plugin-endpoints.mdx and client-sdk.mdx. Both were pulled
in by the sibling trigger anchors: neither mentions toggle at all (git grep -n toggle on each
returns nothing at origin/main; reverse-checked — automation appears 5× in plugin-endpoints.mdx,
so the search runs). Neither states a permission for the route this PR changes. releases/v17.mdx
names the route and is read-only, correctly untouched.

Your open question — A, and B is filed rather than dropped

Keep the shared FLOW_WRITE_DENY_MESSAGE. It is what the ruling authorizes literally (one predicate,
no new surface), it names the capability that would admit any caller — #7450's actual requirement — and
it is accurate about the policy even where it is not literally about the verb.

You are right that B is the better long-term shape and that this domain already has the precedent
one screen over (SCREEN_READ_DENY_MESSAGE beside RUN_READ_DENY_MESSAGE). A user-visible copy change
the ruling did not ask for should not ride this PR. Filed as a finding so it is graded rather than
lost with your report.


Generated by Claude Code

@os-zhuang
os-zhuang marked this pull request as ready for review August 24, 2026 11:07
@os-zhuang
os-zhuang added this pull request to the merge queue Aug 24, 2026
Merged via the queue into main with commit 266436a Aug 24, 2026
39 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-10243-toggle-manage-metadata branch August 24, 2026 11:22
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
… commits and ADR that decided them (objectstack-ai#20816)

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

## What changed

This is the fourteenth stage of the `domain:services` lane of the
dead-citation sweep. It covers
`packages/services/service-automation/src/**` and nothing else. By the
seat's claim (`5905919247`), it is the largest package left in the lane,
and it was free once the `engine.ts` work of the previous holder landed
as `c8111a575`. Later stages cover the other packages, so this PR says
`Part of` and the card stays open.

Every comment or docblock site in scope that cited a tracker number
answering 404 has been rewritten in ruling C+D's form C (comment
5749154545 on objectstack-ai#19123), by the method of stages 1 to 13 (the latest is PR
objectstack-ai#20789, landed as `8acdae9d8`). That is **73 sites on 73 lines in 27
files, covering 14 numbers**:

- 44 census sites (every census site this package has at the base);
- 24 sites in test comments, which the census defers; 3 of their numbers
(`objectstack-ai#11504`, `objectstack-ai#16709`, `objectstack-ai#8778`) stand only in test files here, and each
was read on its own and answers 404;
- 5 sites the census grammar cannot see: the `objectstack-ai#13398-class` spelling (a
hyphen after the digits), 2 in `engine.ts` and 3 in test files.

Each rewritten line now cites the record in this repository that decided
what the line describes, and says in its own words what was decided:
**15 distinct commit shas, plus ADR-0126 §7.2** on 2 lines, per ruling
C's order (the ADR first where it records the decision; see the
per-number table). No number was dropped.

Only comments changed. Every touched source file keeps its line count
(76 lines out, 76 in, over 27 files), so no line citation into these
files moves. 73 of the 76 changed lines carried a dead citation; the
other three are listed under Wordings. No code token moves (see the
guard below).

**No citation number is added.** The only tracker numbers on added lines
are the live `objectstack-ai#14095`, `objectstack-ai#14456`, `objectstack-ai#8287` and the cross-repo
`hotcrm#1206`, each once and each on the line it already stood on. No
number is new to the diff, no number grew, and no PR number is the
citation on an added line.

17 dead sites are left on purpose: 16 test titles and 1 runtime string
(see the list below).

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

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

**Instrument (A1).** The gate's own `node
scripts/check-issue-citations.mjs --census --json`, read-only and
unchanged. The count below is its `allocated-but-absent` findings under
`packages/services/service-automation/`. Each run counts as a reading
only because its board frontier equals the newest issue or pull-request
number, read by a separate request just before and just after the run.
In two of the three runs a new number was opened while the run was
enumerating; each frontier equals the newest number at the run's end,
which is the criterion (stages 7, 11 and 13 met the same shape).

| reading | tree | board | whole-repo `allocated-but-absent` |
service-automation sites | lines | files | numbers |
|---|---|---|---|---|---|---|---|
| before | base `8acdae9d8`, run 2026-09-30T07:03:52Z to 07:07:25Z |
enumerated, 187 pages, frontier objectstack-ai#20798 (newest objectstack-ai#20796 before, objectstack-ai#20798
after) | 796 | **44** | 44 | 11 | 11 |
| after | `3511e88cc` (comments and changeset), run 07:32:02Z to
07:35:27Z | enumerated, 187 pages, frontier objectstack-ai#20803 (newest objectstack-ai#20803 before
and after) | 752 | **0** | 0 | 0 | 0 |
| after, final head | head `a602c4003`, run 07:58:50Z to 08:02:19Z |
enumerated, 187 pages, frontier objectstack-ai#20809 (newest objectstack-ai#20807 before, objectstack-ai#20809
after) | 752 | **0** | 0 | 0 | 0 |

The whole-repo drop is 44, exactly this diff's census sites. The
`resolves` tally is 33,096 in all three runs, and
`resolves-as-pull-request` (1,984) and `cross-repo-unjudged` (1,003) did
not move either. No run was truncated or discarded: all three
enumerations read 187 pages at the newest frontier.

The seat's census counted 45 here at `6bff748b`. The difference is one
`objectstack-ai#10243` comment line that `36d043be1` (PR objectstack-ai#20724) removed from
`engine.ts` in the meantime; the other seven commits since then add or
remove no dead site in this package's non-test `src`.

**Supplementary instrument, the whole scope.** The census does not read
test files or strings, and this stage's scope includes test comments. So
a second reading runs the gate's own exported `extractCitations`
(whole-file and comment-prose projections) and `namesThisRepository`
over every `.ts` file under `service-automation/src` (191 files). It
takes its verdicts from the before census's own board reading rather
than from a second enumeration: a number is dead when that census
reported it `allocated-but-absent`, and alive when the gate's own
census-scope extraction judged it and the census did not report it. 39
numbers are covered by neither, because they stand only in test files or
strings here; each was read on its own through the read-only tools (2
answer 404: `objectstack-ai#11504` and `objectstack-ai#16709`; 18 answer 200 as issues; 19 answer
200 as pull requests, 18 of them also the squash suffix of a commit on
`main`).

| reading | citations | dead | src comment | test comment | src string |
test string |
|---|---|---|---|---|---|---|
| before, `8acdae9d8` | 3,019 | **85** | 44 | 24 | 1 | 16 |
| after, `a602c4003` | 2,951 | **17** | 0 | 0 | 1 | 16 |

Its src-comment column equals the census's 44, which is the control on
the second instrument. The 2,874 live citations and 60 cross-repo
citations are the same in both readings, and the drop of 68 citations is
exactly the rewritten sites the gate's grammar can see. A third, raw
reading (every `#` followed by 2 to 6 digits, whatever surrounds it)
finds 3,078 occurrences before and 3,005 after: the 68, plus the 5
`objectstack-ai#13398-class` sites only this reading sees.

## Per-number table

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

| number | sites / files | rewritten / left | anchor: what it decided |
|---|---|---|---|
| `objectstack-ai#11060` | 17/4 | 12/5 | `815585513` (PR objectstack-ai#11347): flow value
expressions gain exactly `round` / `floor` / `ceil` / `abs` / `min` /
`max`, mirrored 1:1 from the CEL stdlib, and an unknown name in call
position becomes the loud `FlowExpressionFunctionError` instead of a
silent `null`. Its message records the maintainer ruling on `objectstack-ai#11060`
(2026-08-23, option A) and its diff names the number 18 times; `git
blame` puts 11 of the 12 lines in it, and the twelfth
(`end-node-refused-outcome.test.ts:333`) was written later. The lint
lane's anchor for the same number |
| `objectstack-ai#10243` | 14/5 | 13/1 | Three rungs, per line. **ADR-0126 §7.2** on 2
lines (`engine.ts:2315`, `flow-activation-ledger.test.ts:1024`): the
durable ledger row replaces the process-local `flowEnabled` map,
"retiring the objectstack-ai#10243 leak's mechanism rather than refining it"
(`docs/adr/0126-packaged-metadata-customization-model.md:339-340`),
which is what both lines say is the point. **`02b41232d`** (PR objectstack-ai#10996)
on 9 lines, the ones that say the map "measured" leaking: the recorded
measurement that tenant A's toggle answered 200 and tenant B and the
platform admin read the flow back off. **`266436a7f`** (PR objectstack-ai#11660) on 2
lines: it implements the maintainer ruling on `objectstack-ai#10243` (option A, toggle
joins the `manage_metadata` write set), which is the gate
`flow-activation-store.ts:67` says the difference "turned on", and it
wrote the "mitigating but not exculpating" record that
`flow-activation-ledger.test.ts:272` quotes. The runtime lane's anchors
for the same number |
| `objectstack-ai#14419` | 14/4 | 11/3 | `c5a7448d5` (PR objectstack-ai#14948): `create_record`
surfaces `engine.insert`'s classified `DUPLICATE_RECORD` code on the
node result, the engine copies it onto `$error`, and `try_catch`
preserves it across its own binding; deliberately scoped to
`create_record`. Its message's last line names `objectstack-ai#14419` as its card, its
diff names the number 13 times, and `git blame` puts 8 of the 11 lines
in it (the other 3 were written by later commits it precedes). The spec
lane's anchor for the same number |
| `objectstack-ai#13398` | 9/4 | 9/0 | `e238c79f0` (PR objectstack-ai#13592): the earliest text in
this repository that records the maintainer's published-sink ruling as
made — raising a log level by widening a published sink that declares no
`error` is "refused as actively harmful". Every line here states that
ruling ("forbids raising a site to `error` where doing so means GROWING
`error?` onto a published sink"). Stage 3's anchor, and the one the
stage-3 review recommended for this package |
| `objectstack-ai#16659` | 9/3 | 9/0 | `ecdfc9411` (PR objectstack-ai#17334): a time-triggered flow
declares its acting organization on its start node, the engine lifts it
onto the binding, and the triggers run the flow as it. `git blame` puts
the 4 `engine.ts` / `suspended-run-store.ts` lines in it. The 5 lines in
`notify-zero-delivery-visibility.integration.test.ts` were written by
`ae6dcf6a4`, an ancestor of `ecdfc9411`, in the future tense ("once
objectstack-ai#16659 lands"); they now name the landed commit (see Wordings). Stage
12's anchor |
| `objectstack-ai#13648` | 9/3 | 6/3 | `7307191db` (PR objectstack-ai#14388): the public `resume`
door normalises an absent signal to `{}`, and the chokepoints take a
non-optional signal, so a signal-less resume is held to the screen
contract. Its diff names `objectstack-ai#13648` 8 times; `git blame` puts 5 of the 6
lines in it |
| `objectstack-ai#13681` | 6/4 | 4/2 | `18d816a50` (PR objectstack-ai#14452): the spec half of the
contained-failure contract, which declares the run-level `failed`, the
loop iteration through `try` / `catch`, and row identity on `$error`.
Its subject names `objectstack-ai#13681`. The lines were written by the services
halves (`d30ccb9bd`, `b7225477b`), both descendants. The spec lane's
anchor for the same sentence ("one level up") |
| `objectstack-ai#17123` | 4/2 | 2/2 | `ae6dcf6a4` (PR objectstack-ai#17339): a `notify` node
reports `selected`, the recipients it addressed, so a zero-delivery run
stops reading like a run with nothing to notify. Its diff writes
`objectstack-ai#17123` 4 times, and `git blame` puts both lines in it. New to the
sweep |
| `objectstack-ai#8707` | 2/2 | 2/0 | `1408fe385` (PR objectstack-ai#8777): audit rows are stamped
from the record's own organization. Stage 7's anchor |
| `objectstack-ai#16709` | 2/1 | 1/1 | `8c7cca1ce` (PR objectstack-ai#16739): its item 1 pins the
restore verb's drop of a stale hot consumed-suspension copy, and it
created this test file. Stage 7's anchor |
| `objectstack-ai#10062` | 1/1 | 1/0 | `fa5d137ab` (PR objectstack-ai#12942): published `src` may
import only declared workspace dependencies; its diff moved this import
to `@objectstack/metadata-core`. Subject names it |
| `objectstack-ai#14390` | 1/1 | 1/0 | `9d7f7259f` (PR objectstack-ai#14603): both driver exits of
`engine.update` answer a unique violation with the `DUPLICATE_RECORD`
envelope. Subject names it. The runtime and rest lanes' anchor |
| `objectstack-ai#11504` | 1/1 | 1/0 | `f90e82024` (PR objectstack-ai#12611): registers
`FLOW_INPUT_SCHEMA_INVALID`, the line's subject; its diff names `objectstack-ai#11504`
5 times. The spec and runtime lanes' anchor |
| `objectstack-ai#8778` | 1/1 | 1/0 | `7901b2dd2` (PR objectstack-ai#8905): the stamp-only
`tenancy.organizationField` declaration the line names. Stage 6's anchor
|

Every cited sha matches exactly one commit (`git rev-parse
--disambiguate`, count 1 for each of the 15), and all 15 are ancestors
of the base (`merge-base --is-ancestor`, exit 0 for each; reverse leg,
base against each anchor, exit 1 for each; control legs exit 0: stage
1's landing `422db788a`, and the repository's root commit, which lies
deeper than every anchor; the history is complete,
`--is-shallow-repository` false, 15,178 commits). Each of the 14 numbers
answers 404 on the issues endpoint, which serves pull requests too, read
one by one.

No ADR, `scripts/adr-anchors/` file or other `docs/` page records the
decision of any of the 14 (a `docs/audits` census row names `objectstack-ai#10062` as
a gate's origin, and `docs/qa` checklist rows name `objectstack-ai#10243`; neither is
a decision record), except ADR-0126, which records the retirement
`objectstack-ai#10243`'s measurement led to (§7.2) and the operator gate (§5). The
`objectstack-ai#10243` lines that describe the measurement or the ruling cite those
commits; the two lines that describe the retirement cite the ADR.

## Wordings to check

- **Tag swaps in brackets or parentheses.** 「[objectstack-ai#16659]」 became 「[commit
ecdfc94]」 on 4 lines, 「[objectstack-ai#10062]」 became 「[commit fa5d137]」, and
every parenthesised `(#N)` became `(commit SHA)` in place.
- **Headings.** 「objectstack-ai#14419 —」, 「objectstack-ai#17123 —」, 「objectstack-ai#11060 —」, 「objectstack-ai#16709 item 1 —」 at
the head of a docblock or comment became 「Commit c5a7448 —」 and so on,
the form stage 13 used.
- **Two headers name the card or issue by its commit.**
`notify-zero-delivery-visibility.integration.test.ts:4` now opens 「The
card behind commit ae6dcf6:」, and
`flow-field-expression-scale.integration.test.ts:4` 「The end-to-end
oracle for the issue behind commit 8155855」. The docblocks below them
go on speaking of 「the card's reading」, 「the card's ⭐」 and 「the third
value-producing surface the issue names」; the headers keep that
antecedent. This landed as its own commit, `a602c4003`, and every
reading was re-run on it.
- **`objectstack-ai#13398`.** 「a objectstack-ai#13398-class raise: that ruling forbids」 became 「a
raise under the published-sink ruling (commit e238c79): that ruling
forbids」; 「outside objectstack-ai#13398's class」 became 「outside the sink ruling's
(commit e238c79) class」; 「(objectstack-ai#13398-class)」 became 「(the published-sink
ruling, commit e238c79)」.
- **`objectstack-ai#10243`.** 「the map objectstack-ai#10243 measured leaking」 became 「the map commit
02b4123 measured leaking」 (and alike on 9 lines); 「the whole point of
objectstack-ai#10243」 became 「the whole point of ADR-0126 §7.2」; 「the one objectstack-ai#10243
turned on」 became 「the one the toggle ruling (commit 266436a) turned
on」; 「objectstack-ai#10243 — the retired mechanism is GONE」 became 「ADR-0126 §7.2 —
the retired mechanism is GONE」.
- **`flow-activation-ledger.test.ts:271-272`.** 「the objectstack-ai#10243 map's "cold
boot reads enabled: true again" was recorded as
mitigating-but-not-exculpating」 became 「the retired map's … was recorded
(commit 266436a) as mitigating-but-not-exculpating」. The anchor moved
one line down, onto the verb it dates; `:272` is one of the three
changed lines that carried no dead number.
- **`crud-nodes.ts:439-441`, a statement that was stale when it
landed.** 「`engine.update` still leaks the raw driver error (objectstack-ai#14390, not
yet fixed) — those node results have nothing structured to surface yet」
became 「`engine.update` gained the same `DUPLICATE_RECORD` envelope for
a unique violation (commit 9d7f725), but those node results are
untouched here — this repair was scoped to `create_record` alone.」
`9d7f7259f` is an ancestor of `c5a7448d5`, the commit that wrote the
sentence, so the update door already carried the envelope when "not yet
fixed" landed; `c5a7448d5`'s message states the `create_record`-only
scope. `:439` and `:441` are the other two changed lines with no dead
number.
- **`objectstack-ai#16659` in the notify test, future tense.** 「Why objectstack-ai#16659 landing
does not close this」 became 「Why commit ecdfc94 does not close this」;
「the shape a scheduled flow has once objectstack-ai#16659 lands」 became 「the shape a
scheduled flow takes under commit ecdfc94」; 「as it fires once objectstack-ai#16659
lands」 became 「as it fires under commit ecdfc94」.
- **`objectstack-ai#13681`.** 「the measurement these tests reproduce (objectstack-ai#13681) found」
became 「the measurement behind commit 18d816a, reproduced here,
found」: the measurement was the card's, and `18d816a50` is the contract
that answered it.
- **`objectstack-ai#11060`.** 「the LOUD half of the objectstack-ai#11060 ruling」 became 「the LOUD
half of the ruling commit 8155855 records」; 「the exact silence objectstack-ai#11060
removes」 became 「the exact silence commit 8155855 removed」; 「before
objectstack-ai#11060 this wrote the field as undefined」 became 「before commit
8155855 …」.
- **`objectstack-ai#14419`.** 「a different door than objectstack-ai#14419's original bug」 became 「a
different door than the bug commit c5a7448 fixed」; 「the whole point of
objectstack-ai#14419」 became 「the whole point of commit c5a7448」.
- **`objectstack-ai#16709`.** 「objectstack-ai#16709 item 1 —」 became 「Commit 8c7cca1, item 1 —」:
the item numbering is that commit's own.

## The 17 sites left

- **Test strings, 16 sites on 16 lines**, all `describe` / `it` titles,
left as stages 1 to 13 left theirs:
`contained-failure-visibility.test.ts:184` and
`loop-dying-body-steps.test.ts:298` (`objectstack-ai#13681`);
`create-record-duplicate-code.test.ts:51`, `:133`, `:255` (`objectstack-ai#14419`);
`notify-zero-delivery-visibility.integration.test.ts:403`, `:535`
(`objectstack-ai#17123`); `screen-resume-signal-less.test.ts:81`, `:134`, `:187`
(`objectstack-ai#13648`); `template-functions.test.ts:44`, `:162`, `:202` and
`flow-field-expression-scale.integration.test.ts:83` (`objectstack-ai#11060`);
`flow-activation-ledger.test.ts:1027` (`objectstack-ai#10243`);
`stale-hot-consumed-suspension.test.ts:157` (`objectstack-ai#16709`).
- **One runtime string**: `builtin/template.ts:192`, the tail of the
unknown-function refusal ("(Before objectstack-ai#11060 this name was silently
rewritten to null …)"). A runtime string takes form D, not this card's
comment-only form C, and the shrink-only `doc-authoring-prose-id`
baseline already holds it (`template.ts`: `objectstack-ai#11060: 1`), so
`check:doc-authoring` sees no growth.
- No operator log string, assertion message, quoted maintainer ruling or
generated file in this package carries a dead number.
- **Outside `src`, listed and left, not edited in this stage:** the
shipping `README.md` names no dead number (`objectstack-ai#4336`, `objectstack-ai#4414`, both live).
`tsconfig.test.json` names the dead `objectstack-ai#13176` on 2 lines (5, 73) and the
dead `objectstack-ai#14916` on 1 line (54); its other numbers are live.
`vitest.config.ts` names only live numbers. The release-owned
`CHANGELOG.md` names 7 of the 14 numbers on 13 lines.

## Mechanical guard: no code token moves

The guard compares, base `8acdae9d8` against head, over all 27 touched
`.ts` files:

- **Reading 1**, the TypeScript parser's leaf nodes (a `forEachChild`
walk, so comments are trivia and JSDoc nodes are never visited). String
and template literals are therefore read in full.
- **Reading 2**, the full token stream in parser context (a
`getChildren` walk, so punctuation and keywords are included; JSDoc
nodes skipped).

Results:

- Real run at the final head `a602c4003`: 47,982 base leaf tokens, **0
files with a token change** on either reading (exit 0).
- Comment control in `engine.ts` (「which folds nothing and rejects
nothing.」 to 「which folds nothing and refuses nothing.」): 0 files
changed, as expected (exit 0).
- Positive control, a code token renamed in `engine.ts` (`function
applyResumeSignal(` to `function applyResumeSignalX(`): DIFFER on the
identifier (exit 1).
- Positive control, one digit changed inside a kept test title
(`flow-activation-ledger.test.ts:1027`, `objectstack-ai#10243` to `objectstack-ai#10244`): DIFFER on
the string literal (exit 1).

Every mutation went through `scripts/ablation-replace.mjs` (wrap mode)
under a shell trap that restores by absolute path, and each landed
(anchor 1 to 0, blob changed). Each restore was proven byte-identical to
the HEAD blob (`26e9be7dc174`, `e78e505fa3be`), with `git diff HEAD`
empty and a clean tree afterwards. The controls ran on `3511e88cc` and
again on `a602c4003`.

## Changeset

This change ships bytes, so a `patch` changeset for
`@objectstack/service-automation`
(`.changeset/20596-service-automation-provenance-anchors.md`) is
included. Its body is stage 4's (`plugin-security`, the other stage with
an ADR anchor), word for word, with the package name changed.

Measured on the built package (A3), after a full workspace build in
which this package was a cache miss (71 of 71 tasks, at `9b0d34213`,
which holds every source-line change): `files[]` is `dist`, `README.md`
and `CHANGELOG.md`, and the package is not private.

- The declaration files `dist/index.d.ts` and `dist/index.d.cts` carry
the rewritten docblocks at `engine.ts:354`, `:362`, `:529`, `:2112`,
`:2119`, `:2315`, `:2331`, `:5224`, `:7984` and
`flow-activation-store.ts:67` (e.g. `02b41232d` 4 times, `c5a7448d5`
twice, `ecdfc9411` and `7307191db` once each).
- The JS entries `dist/index.js` and `dist/index.cjs` carry the kept
comments at `engine.ts:2315`, `:2331`, `:3565`, `:3581`, `:5224`,
`:7984`, `notify-node.ts:449`, `suspended-run-store.ts:714` and
`sys-automation-run.object.ts:112`.
- The other rewrites are stripped by the bundle or sit in test files.
- Positive controls, unchanged lines beside the rewrites, land exactly
where their neighbours do: the line before `engine.ts:2112` and before
`:2119` once in each declaration file and 0 in the JS; the
`FlowActivationStore` docblock opener beside
`flow-activation-store.ts:67` once in each declaration file; and the
neighbours of three stripped rewrites (`crud-nodes.ts:438`,
`suspended-run-store.ts:952`, `template.ts:24`) 0 everywhere.
- A never-written negative phrase appears nowhere in `dist`.
- None of the rewritten numbers is left in `dist`; the one `objectstack-ai#11060` in
each JS entry is the kept runtime string at `template.ts:192`.

The two commits after `9b0d34213` add the changeset and change two
test-file comment lines, which the bundle does not include.

## Gates (final head `a602c4003`)

- **Citation judging, as CI runs it:** `pnpm check:issue-citations`
exits 0 (self-test, 114 cases, 8 batteries). `node
scripts/check-issue-citations.mjs` exits 0: the diff-scoped run judged
the 2 citations the change adds on its surface (the live `objectstack-ai#14095` and
`objectstack-ai#8287`, each on the line it already stood on), and both resolve.
- **Doc authoring:** `pnpm check:doc-authoring` exits 0 (the
sibling-package prose-id baseline holds, no growth).
- **Derived gates:** `node scripts/pm/dispatch-gates.mjs --commands
--repo objectstack-ai/objectstack` at `a602c4003` (after a fresh fetch)
derived 63 commands. They are the 64 derived at dispatch less `pnpm
check:error-code-casing`, which the derivation now lists as a roster
family (below).
- Each ran with its exit code captured before any pipe, and all 63 exit
0; none exited 3.
- `--ran`, fed each command with its exit code, reports 63 run, 0 NOT
MEASURED (a derived zero), 0 unrun, and exits 0.
- A full `turbo run build` of `./packages/*` and `./packages/*/*` ran
first under the shared verify lock (71 of 71 tasks, exit 0), so no gate
hit an unbuilt workspace.
- The same 63 had also all exited 0 on `3511e88cc` before the referent
commit.
- **Roster families the derivation lists outside its commands** (their
rosters sit in directories this diff touches): `node
scripts/check-changeset-fixed.mjs`, `pnpm check:authz-resolver`, `pnpm
check:error-code-casing` and `pnpm check:filter-alias-parity`, each exit
0.
- **Tests and typecheck, under the verify lock, at `a602c4003`:**
- `pnpm --filter @objectstack/service-automation test`: 157 files pass
and 1,974 tests pass, every tracked test file under `src/`, the 16
touched ones included.
- `pnpm --filter @objectstack/service-automation typecheck` exits 0
(`tsc --noEmit` plus the test-layer check on `tsconfig.test.json`). `tsc
--listFiles` puts all 27 touched files in both programs.
- **Lint, as a proven narrowing:** eslint with inline config disabled,
over the 27 touched `.ts` files, gives 27 files, 0 errors and 0 warnings
(its `--format json` output). All 27 are in eslint's own population
(none reported ignored; a `dist` file, as the control, is ignored).
`eslint.config.mjs` never enables type-aware linting (no
`parserOptions.project`, as its own lines 327-328 state), so a comment
edit here cannot move the verdict on any untouched file. The repo-wide
`pnpm lint` is CI's run.
- **Control bytes:** `pnpm check:nul-bytes` exits 0, and a raw scan of
the 28 changed files for control bytes finds none.

## Acceptance notes

- **The gate-invisible spellings, grepped as the claim asked** (objectstack-ai#20636).
At the base: `#N-word` 11 lines, of which the 5 `objectstack-ai#13398-class` lines
were dead and are rewritten here; the 6 left are live (`objectstack-ai#5912` twice,
`objectstack-ai#5048`, `objectstack-ai#5186`) or cross-repo (`hotcrm#548` twice). `#A/#B` 13 lines
of the number-slash-number shape, plus 6 lines where the slash follows
`ADR-0049` (`ADR-0049/objectstack-ai#1888`); every second number is live. `option #N`
none. URL-spelled none. So the claim's 11 / 13 / 0 / 0 hold, and at the
head the first is 6.
- **A sibling of the `option #N` position, dormant.**
`NON_CITATION_HEADS` also excuses a number after 「clause」, and
`suspended-run-store-consume-log-cause.test.ts:408` reads 「The
consequence clause objectstack-ai#6299 asked for」, a real citation of the live
`objectstack-ai#6299`. Across `packages/**/src` there are 4 such sites, all in test
files, a surface the gate defers anyway, so nothing dead hides there
today. Recorded for objectstack-ai#20636's family, not filed.
- **Two drifts left as they are, wording only.**
`notify-zero-delivery-visibility.integration.test.ts:72` still calls the
organization-less cron tick 「the shape production builds now」, written
before `ecdfc9411` landed; it carries no number and lost no referent
here. `suspended-run-store.test.ts:904` names
`tenancy.organizationField`, which `502f179cc` has since retired from
the authorable surface (stage 7 recorded the same drift in
`plugin-approvals`).
- **`update_record` still surfaces no `code`.** The corrected sentence
at `crud-nodes.ts:439-441` is now true that `engine.update` carries the
`DUPLICATE_RECORD` envelope while `update_record` does not surface it.
That is an observation with no reported pull, recorded here.
- **「The card」 phrases.** 134 comment lines in 50 files of this package
speak of 「the card」 or 「this card」. They carry no number and neither
instrument sees them. The two whose antecedent this diff would have
removed are handled above; the rest are unchanged, as in stages 8 to 13.
- **The census instrument did not truncate in this stage.** All three
enumerations read 187 pages at the newest frontier.
- **Anchors the next stages can reuse**, each checked here: `objectstack-ai#11060` →
`815585513`; `objectstack-ai#10243` → `02b41232d` (the measurement), `266436a7f` (the
ruling) or ADR-0126 §7.2 (the retirement), per line; `objectstack-ai#14419` →
`c5a7448d5`; `objectstack-ai#13648` → `7307191db`; `objectstack-ai#13681` → `18d816a50`; `objectstack-ai#17123` →
`ae6dcf6a4`; `objectstack-ai#14390` → `9d7f7259f`; `objectstack-ai#11504` → `f90e82024`; `objectstack-ai#10062` →
`fa5d137ab`; and the reused `objectstack-ai#13398` → `e238c79f0`, `objectstack-ai#16659` →
`ecdfc9411`, `objectstack-ai#16709` → `8c7cca1ce`, `objectstack-ai#8778` → `7901b2dd2`, `objectstack-ai#8707` →
`1408fe385`.
- **Base.** The branch is on `main` at `8acdae9d8`. `main` has since
moved five commits (`41dcf1188`, `96e724475`, `df67985b0`, `cfa931535`,
`5bed1f6ca`). None touches `packages/services/service-automation/src`,
`scripts/check-issue-citations.mjs` or `.changeset/config.json`, and
none is a path in this diff. Two of them move gate inputs
(`scripts/doc-authoring-prose-id.baseline.json`, whose change is
`driver-sql` rows only, and one `scripts/adr-anchors/` file for
`packages/spec`), so those families ran here against the base's copies;
this diff moves no code token and no runtime string, so nothing here can
interact with them. No merge was taken; the merge queue rebuilds on the
merged generation.

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

---------

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

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants