Skip to content

fix(service-automation): a flow switched off in the activation ledger stays unbound after a restart, and a trigger-fired refusal logs no run-history claim (#20677) - #20702

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-20677-ledger-disabled-stays-unbound
Sep 29, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-20677-ledger-disabled-stays-unbound

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #20677
Clause-②: no

What was wrong

A packaged flow switched off through the ADR-0126 activation ledger came back armed after a cold restart. At boot AutomationServicePlugin.start() pulls the flows and runs hydrateFlowActivations(), which leaves a switched-off flow unbound. The trigger plugins register later, at kernel:ready, and AutomationEngine.registerTrigger armed every registered, unbound flow of the matching type through activateFlowTrigger without asking whether the flow may run. So GET /api/v1/automation/_status reported enabled: false, bound: true. Every matching event then fired a run that execute() refused with FLOW_DISABLED, and the trigger-fired callback logged it at ERROR as a failed run whose failure "is recorded in the flow's run history". No row was written.

The card's reproduction, reached on current main at the engine level (14f80e23, the pins in commit 1a7ce83b7 run against the unfixed engine): a boot replay over one activation store, in plugin.ts's order (pull, hydrate, kernel:ready protocol sync, trigger registration, seal), reads on the first restart:

AssertionError: restart 1: expected { enabled: false, bound: true } to deeply equal { enabled: false, bound: false }

That is the card's /_status shape. On the unfixed code 10 of the 11 new cases are red, and the one control (a run that dispatched and failed) is green.

What changes (packages/services/service-automation/src/engine.ts only)

  • One enablement gate, in activateFlowTrigger itself. It refuses to arm a flow isFlowEnabled answers false for, before the scheduled-work policy gate and before the trigger lookup. registerFlow, registerTrigger, the enable half of toggleFlow, and any arming path added later inherit it. registerFlow's own caller-side if (this.isFlowEnabled(name)) is folded into the gate, so there is one check, not two. registerTrigger's loop is unchanged; only its comment now names the gate.
  • The trigger-fired line states only what happened.
    • A FLOW_DISABLED answer that still reaches the callback (an event in flight at the switch-off, or a trigger whose stop() failed) is logged at info: the flow is disabled, the run was refused before it started, nothing ran, and no run-history row records it. It is no longer logged at error.
    • Every other failure keeps the existing error line. That line says "the terminal failure is recorded in the flow's run history" only when the result carries status: 'failed', the verdict execute()'s ran-and-failed exit and retryExecution's exit set after writing their failed row. A never-dispatched answer (no status) gets the same line with "it was refused before it dispatched" in place of the history claim.
    • Same logging shape as before, with the envelope in the structured slot. No new log dialect.

The dispatch's mechanism assumptions, measured

  • A1: confirmed. registerTrigger (was about :3368) called activateFlowTrigger(name) for every unbound flow of the matching type. Neither method consulted isFlowEnabled. registerFlow's only ledger check was the caller-side guard around activateFlowTrigger (was :4271). The one at :4257 guards the node-type warning, not arming. The ordering is in plugin.ts: hydrate runs in start(), and the protocol sync and trigger plugins run at kernel:ready.
  • A2: callers of activateFlowTrigger. There are three: registerTrigger, registerFlow and toggleFlow's enable branch. The only refused-flow bookkeeping is policyDisabledFlows (the scheduled-work refusal record). A disabled flow now returns before the policy gate, so it records no policy refusal. describeUnboundReason already answered undefined for a disabled flow, so neither the /_status reason nor getTriggerBindingAudit() changes. /_status's bound reads boundFlowTriggers, which is now false for the flow.
    • One behaviour change beyond the card's path: toggleFlow(name, true) on a flow whose status is obsolete or invalid no longer arms it. Before, it was armed and every event it fired was refused with FLOW_DISABLED (the status dimension) and logged at ERROR, the same defect through the toggle. A status-disabled flow registered before its trigger was re-armed by registerTrigger too. Both are pinned.
    • The enable path for a ledger-disabled flow still arms, and that is pinned as well.
  • A3: when FLOW_DISABLED reaches the log. execute()'s disabled exit returns before a run id is minted and before any recordLog, so no row is ever written for it. Before the fix, it reached the ERROR line on every matching event of a re-armed flow. With the gate, it is reached only by the residue named above.
    • The run-row fact is status: 'failed'. The four status-less failure exits are flow not found, FLOW_DISABLED, FLOW_NO_START_NODE and FLOW_INPUT_SCHEMA_INVALID. The last one does write a failed row but carries no status, so the line makes no history claim for it: true, if less informative.

PR #20551's arm-time api secret refusal is untouched. The gate sits before trigger.start. validateApiTriggerSecret at registration and ApiTrigger.start()'s throw (into the existing Failed-to-bind warn) are unchanged, and api-trigger-secret-registration.test.ts is green.

Tests (all read at 758967616, the last commit)

  • New src/flow-activation-late-trigger.test.ts, 11 cases:
    • A trigger registered after hydrateFlowActivations(), for each of record_change, schedule, time_relative and api: the switched-off flow stays { enabled: false, bound: false }, and its enabled sibling of the same kind arms (the control).
    • The status dimension through the same path.
    • The enable path re-arms a hydrated-disabled flow, and the store row reads active: true.
    • Re-enabling the ledger bit of an obsolete flow does not arm it.
    • A cold restart over one store, twice: enabled: false, bound: false on both boots, the sibling bound, and the binding audit empty.
    • The three log-line cases: FLOW_DISABLED is logged at info with no history claim and listRuns is empty. A dispatched failure is logged at error with the claim and one failed row. A never-dispatched failure is logged at error with no claim and no row.
  • pnpm --filter @objectstack/service-automation test: Test Files 155 passed (155), Tests 1924 passed (1924).
  • pnpm --filter @objectstack/service-automation typecheck: exit 0 (tsc --noEmit, then check:test-typecheck: OK). tsc -p tsconfig.test.json --listFiles lists the new test file (1 hit).
  • Ablation, via scripts/ablation-replace.mjs in wrap mode (anchor must hit once, the restore is armed on EXIT/INT/TERM, and a shell trap git checkout HEAD -- sits behind it). The subject resolves from src/ through the relative ./engine.js import, so no dist/ leg applies.
    • Gate deleted (if (!this.isFlowEnabled(flowName)) return;, anchor x1 then x0, blob 566ed4d67921 then d09694f76678): Tests 8 failed | 3 passed (11). The 8 are every arming pin. For example, restart 1: expected { enabled: false, bound: true } to deeply equal { enabled: false, bound: false }. The 3 log-line cases do not depend on the gate and stay green.
    • FLOW_DISABLED branch disabled: Tests 1 failed | 10 passed. The failing case is the info pin: expected [ Array(1) ] to deeply equal [], where the array holds an error line.
    • History condition forced true: Tests 1 failed | 10 passed. The failing case is the never-dispatched pin, which now claims a run-history row.
    • Every restore proven: ok restored: blob == HEAD (566ed4d67921) and git diff HEAD is empty. git status --porcelain is empty afterwards.

Gates

  • node scripts/pm/dispatch-gates.mjs --commands re-derived against the real diff gives the same 62 commands as the dispatch list, byte-for-byte after sort.
    • Every exit code was captured before any pipe. 60 exit 0.
    • 2 are NOT MEASURED: pnpm check:dual-build-cjs-loads and pnpm check:type-check-debt both exit 3 (PREREQUISITE NOT MET: they read the whole workspace's built dist/, and this worktree built only this package's dependency closure). CI builds that and runs both.
    • --ran: 62 derived famil(ies) accounted for — 60 run, 2 NOT-MEASURED (2 DERIVED from a recorded exit 3).
  • The roster families printed outside the runnable list all exit 0: node scripts/check-changeset-fixed.mjs, pnpm check:authz-resolver, pnpm check:error-code-casing and pnpm check:filter-alias-parity. Added for the log-level and arming edit, also exit 0: pnpm check:durability-log-level and pnpm check:startup-registry-verdict.
  • Lint, a declared narrowing (pnpm lint over the whole repo is CI's):
    • Run: eslint --no-inline-config --format json on engine.ts and the new test file.
    • Result: 2 files, 0 errors, 0 warnings.
    • The changeset .md has no matching config (--print-config answers undefined, and the JSON says "File ignored because no matching configuration was supplied"), so it is outside eslint's population.
    • eslint.config.mjs never enables type-aware linting (no parserOptions.project or projectService), so this diff cannot move a verdict on any untouched file.
  • pnpm check:nul-bytes: exit 0. A control-byte self-scan of the three changed files finds nothing.

Changeset

.changeset/20677-ledger-disabled-stays-unbound.md is a patch on @objectstack/service-automation. No export, option, route or response shape moves.

Acceptance notes

  • The cold-restart pin is engine-level: it replays plugin.ts's boot order over one shared InMemoryFlowActivationStore. A LiteKernel boot of the real plugin was not built. The plugin attaches the durable ledger only through an objectql data engine with find/insert/update, so it would need a new data-engine double under check:engine-double-contract. The defect lives in the engine's arming path.
  • A test comment in api-trigger-secret-registration.test.ts says "an obsolete flow is re-enabled by a toggle". Under ADR-0126 the toggle moves only the ledger bit, so it never re-enables a status-disabled flow. This is comment drift, outside this card's file surface. Carrier: none.
  • A pre-existing edge, not filed: when recordLog itself throws on execute()'s failure arm, the result still carries status: 'failed'. The trigger-fired line then says the failure is recorded, while the separate ERROR line from that catch says the row never landed. The result has no channel for "the write failed", and the bookkeeping line is itself loud. Carrier: none.
  • origin/main has moved 3 commits since the base (3b47a693c, 0be898499, f379f57f4). None touches packages/services/service-automation or packages/spec/src/contracts, so the branch is not merged forward. CI and the merge queue validate the merge ref.

Generated by Claude Code

…hydration leaves a disabled flow unbound (red on main)

The pins drive every arming path from outside: a trigger type registered
after hydrateFlowActivations(), a cold restart over one activation store,
the enable toggle, and the trigger-fired failure line's run-history claim.
Ten of eleven are red on the unfixed engine; the dispatched-and-failed
control is green.

Claude-Session: https://claude.ai/code/session_01XY5uCwTjZj7884yYtyur4H
Co-authored-by: Claude <noreply@anthropic.com>
…so a trigger registered after ledger hydration never arms a disabled flow

activateFlowTrigger now refuses to arm a flow isFlowEnabled answers false
for, so registerFlow, registerTrigger, the enable toggle and any later
arming path inherit it; registerFlow's caller-side check is folded into
it. The trigger-fired callback says a failure is in the run history only
for a run that dispatched and failed (status 'failed'), and a
FLOW_DISABLED refusal that still reaches it is logged at info as a
refusal, not at error as a failed run.

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

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/service-automation, touching 7 documentable anchor(s).

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

  • content/docs/api/client-sdk.mdx (via FLOW_DISABLED (literal, a string literal in activateFlowTrigger))
  • content/docs/api/declarative-endpoints.mdx (via FLOW_DISABLED (literal, a string literal in activateFlowTrigger), /api/v1/automation/:name/trigger (route, bridged from symbol toggleFlow — its route source's handler names it))
  • content/docs/api/plugin-endpoints.mdx (via FLOW_DISABLED (literal, a string literal in activateFlowTrigger))
  • content/docs/automation/flows.mdx (via registerFlow (symbol, a method of class AutomationEngine), FLOW_DISABLED (literal, a string literal in activateFlowTrigger), /api/v1/automation/:name/trigger (route, bridged from symbol toggleFlow — its route source's handler names it))
  • content/docs/data-modeling/formulas.mdx (via registerFlow (symbol, a method of class AutomationEngine))
  • content/docs/protocol/kernel/http-protocol.mdx (via FLOW_DISABLED (literal, a string literal in activateFlowTrigger), /api/v1/automation/:name/trigger (route, bridged from symbol toggleFlow — its route source's handler names it))
  • content/docs/ui/actions.mdx (via FLOW_DISABLED (literal, a string literal in activateFlowTrigger))

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

  • content/docs/releases/v16.mdx (via AutomationEngine (symbol, a top-level class))
  • content/docs/releases/v17/17-0.mdx (via AutomationEngine (symbol, a top-level class), registerFlow (symbol, a method of class AutomationEngine))
  • content/docs/releases/v17/17-1.mdx (via FLOW_DISABLED (literal, a string literal in activateFlowTrigger))
  • content/docs/releases/v17/17-4.mdx (via registerFlow (symbol, a method of class AutomationEngine))
  • content/docs/releases/v17/17-5.mdx (via registerFlow (symbol, a method of class AutomationEngine))

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

What this run could not see
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 6 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 0e9ad74fb4e315be2fa6392a6cf478c81dd37ad1 → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 0e9ad74fb4e315be2fa6392a6cf478c81dd37ad1

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

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review September 29, 2026 18:54
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 29, 2026
Merged via the queue into main with commit defc7f7 Sep 29, 2026
36 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-20677-ledger-disabled-stays-unbound branch September 29, 2026 19:15
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
…/src to the commits that decided them (objectstack-ai#20703)

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

## What changed

This is stage 6 of the `domain:cli` lane of the dead-citation sweep:
`packages/client/src`. Every comment site there that cited a tracker
number answering 404 now cites, in ruling C+D's form C (comment
5749154545 on objectstack-ai#19123), the commit in this repository's history that
decided what the line describes, and keeps saying in its own words what
that commit decided. PR objectstack-ai#20533 is the method and stages 1 to 5 of this
card (PR objectstack-ai#20624, PR objectstack-ai#20632, PR objectstack-ai#20656, PR objectstack-ai#20673, PR objectstack-ai#20689) are the
precedents. The card stays open for the lane's remaining packages, so
this PR says `Part of`.

That is **43 sites on 43 lines in 7 files, covering 13 numbers**,
rewritten to **12 distinct commits**:
- the census's **19 sites**, all in `src/index.ts` (8 numbers);
- **24 test-file comment sites** in 6 test files (the census defers
`*.test.ts`; stages 1 to 5 took test comments too).

One more line changed: `client.test.ts:2198`, the second half of the
`:2197` sentence ("byte-identical to the pre-(number) behavior" now
reads "byte-identical to the behavior before commit cf74a11").

Only comments changed: **44 lines out, 44 in**, and every touched file
keeps its line count, so no line citation into these files moves. **No
citation number is added**: over the 44 line pairs, added-minus-removed
numbers is empty, and no PR number stands on an added line. No ADR or
ruling-record file in `docs/adr/` or `scripts/adr-anchors/` records any
of these 13 decisions, so every anchor is a commit.

A **`patch` changeset** for `@objectstack/client` rides along, because
the rewritten docblocks reach `dist` (measured below).

## Census: `packages/client`, before and after

**Instrument.** The gate's own `node scripts/check-issue-citations.mjs
--census --json`, read-only and unchanged, run with the fleet token. The
count is its `allocated-but-absent` findings under `packages/client/`.
Both runs enumerated the whole board.

| reading | tree | board | whole-repo `allocated-but-absent` |
`packages/client` sites | lines | numbers | files |
|---|---|---|---|---|---|---|---|
| before | base `3b47a693c7`, run 2026-09-29T18:31:09Z to 18:35:42Z |
enumerated, 186 pages, frontier objectstack-ai#20701, 18,528 numbers | 1,298 | **19**
| 19 | 8 | 1 (`src/index.ts`) |
| after | `221a3f5e63`, run 18:39:47Z to 18:44:31Z | enumerated, 186
pages, frontier objectstack-ai#20702, 18,529 numbers | 1,279 | **0** | 0 | 0 | 0 |

The whole-repo drop of 19 is exactly these sites: a site-by-site diff of
the two JSON outputs has 19 findings gone, all in
`packages/client/src/index.ts`, and none added. `packages/client/src` is
byte-identical at `221a3f5e63` and at the head.

**Supplementary scan (test files included).** The gate's exported
`extractCitations` and `classifyCitation` over all 53 `.ts` files under
`src/`, with the board from the gate's own `probeBoard`: 963 citations
and 61 dead before (src comments 19, test comments 24, src strings 0,
test strings 18), 920 and 18 after (0, 0, 0, 18). Its before list of src
comment sites is identical to the census's. The 18 left are test titles,
the form-D stage (see Acceptance notes).

## Per-site table

`git blame` at the base ties each line to the commit that wrote it, and
each anchor was read in its message, changeset or diff, not only its
subject.

| number | sites (base line) | anchor: what it decided |
|---|---|---|
| `objectstack-ai#12195` | `index.ts:684`, `:728`, `:1834`, `:2021`;
`client.test.ts:2869`; `meta-automation-descriptors.test.ts:29` |
`7986d973f`, stage 3 of the compound-name retirement: un-mounts the
three `:section` arities and unifies the SDK's URL spelling on
`encodeURIComponent`. All six lines blame to it. |
| `objectstack-ai#12176` | `client.test.ts:357`;
`meta-automation-descriptors.test.ts:31` | `7986d973f` as well: the
lines say the card "retired compound-name addressing", and that commit
completes the retirement. Both lines blame to it, and the landed stages
give this number the same anchor. |
| `objectstack-ai#12194` | `index.ts:738`, `:1838`, `:2026`; `client.test.ts:360`;
`meta-automation-descriptors.test.ts:43` | `311433f6b`, stage 1: the
item-name grammar, refused at the publish door. |
| `objectstack-ai#12181` | `index.ts:799`, `:820`, `:911`, `:1866`;
`meta-delete-item-carriers.test.ts:4`, `:265` | `cf71d73f8`:
`meta.deleteItem` sends the reset door's `If-Match` pin and
`?state=draft` on both declarations. Its changeset also records the
withholding of `?dropStorage` that `:820` and the test's `:265`
describe. All six lines blame to it; stage 3 gave the number the same
anchor. |
| `objectstack-ai#14879` | `index.ts:3479`, `:3552`, `:3635`, `:7536`;
`client.data-prefix.test.ts:4`; `client.metadata-prefix.test.ts:7`;
`client.test.ts:2165`, `:2197` | `cf74a1128`: the SDK reads the CRUD
data prefix from discovery instead of restating `/data`
(`_dataPrefix()`). Six lines blame to it; `index.ts:3552` and
`client.metadata-prefix.test.ts:7` blame to `032452a54`, the
metadata-prefix sibling, and name the data-prefix change as their
precedent. |
| `objectstack-ai#14313` | `index.ts:4911`; `return-type-precision.test.ts:1031` |
`b1b978c8d`: binds the `auth.*` family to the wire shapes better-auth
sends, and deliberately leaves `auth.deleteUser` unbound (the member
`:4911` describes). Both lines blame to it. |
| `objectstack-ai#14312` | `return-type-precision.test.ts:891`, `:973`, `:981`,
`:1130` | `e944fdb24`: binds four `oauth.*` methods and deliberately
leaves `oauth.applications.delete` unbound, the "open decision" `:973`
and `:981` name. `:891` blames to it; `:973` and `:981` blame to the
later delete binding (`7beaaa32c`) and `:1130` to `b1b978c8d`, and each
refers back to what the `oauth.*` card did. |
| `objectstack-ai#14314` | `return-type-precision.test.ts:1138` | `7092d63e4`: binds
the `organizations.*` family. The line blames to it. |
| `objectstack-ai#6361` | `index.ts:6379`; `client.test.ts:878`, `:1390` |
`90bbf2510`: retires the notification-list `cursor` on both halves.
`:1390` blames to `0b4022b41`, which applies the same retirement one
door over and names this one as its precedent. Stages 1 and 2 and the
spec lane gave the number this anchor. |
| `objectstack-ai#9934` | `index.ts:7406`; `client.test.ts:27` | `79c46da90`: the
producer-side `userMessage` marking. Both lines blame to it; the anchor
the landed stages give this number. |
| `objectstack-ai#6239` | `index.ts:8122` | `f549a0d4a`: the ADR-0049 retirement sweep
that deleted `ViewProtocol` and its schemas. The line blames to it; the
spec lane's stage 2 gave the number this anchor. |
| `objectstack-ai#8480` | `client.test.ts:512` | `caaae2cca`: the typed
`security.explain()` request gains the `recordIds` batch spelling. The
line blames to it, and its changeset names the card. |
| `objectstack-ai#13208` | `return-type-precision.test.ts:1394`, `:1415` |
`74049254d`: `DeleteMetaItemResponseSchema` declares `seq` and
`projectionApplied`. `objectstack-ai#13208` was the pull request that landed as this
commit (its subject carries the number), and it answers 404 too.
`objectstack-ai#13155`, the issue beside it, answers 200 and stays. |

**Anchor checks.** Every cited sha matches exactly one object (`git
rev-parse --disambiguate`, count 1 for each of the 12), is a commit, has
one parent, and is an ancestor of `main` (`merge-base --is-ancestor`
against `31ed067639`, exit 0 for all 12). The checkout is not shallow.
The control leg `6f657f4f5` (2026-08-07, the parent of the oldest anchor
`f549a0d4a` of 2026-08-08) exits 0, and the negative control, this
branch's own `221a3f5e63`, exits 1. Six anchors reuse the landed stages'
(`7986d973f`, `311433f6b`, `cf71d73f8`, `90bbf2510`, `79c46da90`,
`f549a0d4a`), so each number carries one anchor across the tree; six are
new (`cf74a1128`, `b1b978c8d`, `e944fdb24`, `7092d63e4`, `caaae2cca`,
`74049254d`).

**Numbers.** All 13 dropped numbers answer 404 by REST (re-probed
2026-09-29T18:59Z). The three numbers kept on changed lines (`objectstack-ai#8326`,
`objectstack-ai#12104`, `objectstack-ai#13155`) answer 200. Five slash-joined numbers stand in
`packages/client/src`, which the citation grammar does not read
(`objectstack-ai#11925/objectstack-ai#12036`, `objectstack-ai#3431/objectstack-ai#3455`, `objectstack-ai#2567/objectstack-ai#3963`, `objectstack-ai#5449/objectstack-ai#5546`,
`objectstack-ai#5674/objectstack-ai#5787`); their second halves all answer 200, so none is dead.

**Wordings to check, each true of its commit:**
- `index.ts:820` "Maintainer-seat ruling, landed by commit cf71d73":
that commit's changeset states the withholding in the same terms ("no
caller was measured needing it from this client").
- `index.ts:3552` now reads "The defect commit cf74a11 fixed, one key
over"; `client.metadata-prefix.test.ts:7` reads "the `crud.dataPrefix`
defect commit cf74a11 fixed".
- `index.ts:738` keeps "Stage 1" beside the commit, and its "this stage"
is the docblock's own commit, tagged `7986d973f` at `:728`.
- `return-type-precision.test.ts:891`, `:1031`, `:1138` keep "card N of
3 of objectstack-ai#12104" (that number answers 200).

## Mechanical guard: no code token moves

**H2 holds on the comment-stripped reading; the emitted `dist` is NOT
byte-identical, because the docblocks ship.**

**Token guard.** It compares the TypeScript parser's leaf tokens
(TypeScript 6.0.3, JSDoc nodes excluded) of the 7 touched files at base
`3b47a693c7` and at `221a3f5e63`. Controls mutate the head text in
memory only.
- Real run: 68,102 base tokens, 0 differing (exit 0).
- Comment-insertion control: 0 differing (exit 0).
- Code-insertion control: all 7 files differ at token 0 (exit 1).
- String control (`'token'` to `'tokeN'` in the code literal at
`index.ts:4895`): exactly 1 differing `StringLiteral`, at token 14,882
of `index.ts` (exit 1).

**Emitted `dist`.** `pnpm --filter @objectstack/client build` at base
(the base tree of `packages/client/src` restored in place with a
trap-armed restore; blob-equal to HEAD and `git diff HEAD` empty
afterwards) and at the head, with the same dependency builds:
- `index.d.ts`, `index.d.mts`, `index.js` and `index.mjs` differ;
`index.js.map` and `index.mjs.map` are equal.
- The same parser comparison over the four differing `dist` files reads
0 differing tokens (12,637 / 12,637 / 25,487 / 25,184), so the whole
`dist` delta is comment text. Its code control (a code line appended to
`index.mjs`) reads COUNT DIFFERS. A first try at that control appended
the line onto the file's last line, which is the `sourceMappingURL`
comment with no trailing newline, so it landed inside a comment, read 0,
and was void; it was redone after a newline.
- The new wording is in `dist`: for example "commit cf74a11" appears 4
times in `index.d.ts` and `index.js`.
- Code-mutation control (`scripts/ablation-replace.mjs`, anchor
`searchParams.set('token'` hit 1 to 0, blob restored to HEAD
`19305adddb`, `git diff HEAD` empty): changes `index.js` and
`index.mjs`. `dist` was rebuilt to the head bytes and
`scripts/ablation-dist-preflight.mjs` reads the marker absent from all 6
files with a clean tree.

A raw scan of the 8 changed files for control bytes finds none (a
positive probe on a scratch file matched).

## Changeset

**`patch` for `@objectstack/client`**
(`.changeset/client-provenance-anchors.md`), in PR objectstack-ai#20632's form.
`@objectstack/client`'s `files[]` is `dist`, `README.md` and
`CHANGELOG.md`, and the build above emits different `index.d.ts` /
`index.d.mts` / `index.js` / `index.mjs` at base and head, so this diff
publishes. `check-changeset-no-major`, `check-empty-changeset`,
`check-adr-0087-registration` and `check-changeset-fixed` all exit 0.

## Gates (head `afa654081f`)

This host has no `flock`, so `os-verify-lock.sh` ran in its declared
unlocked mode. Its disclosure, verbatim, from each run at this head and
from the two `dist` builds:

```text
os-verify-lock: VERDICT command-exit 0 · UNLOCKED (declared) · no usable `flock` on this host, so the shared verify lock was NEVER taken and NOTHING was serialized · ran 59s · declare it in the PR body · pnpm --workspace-concurrency=2 --filter '@objectstack/client...' build
os-verify-lock: VERDICT command-exit 0 · UNLOCKED (declared) · no usable `flock` on this host, so the shared verify lock was NEVER taken and NOTHING was serialized · ran 121s (2m01s) · declare it in the PR body · pnpm turbo run build --filter='./packages/*' --filter='./packages/*/*' --concurrency=2
os-verify-lock: VERDICT command-exit 0 · UNLOCKED (declared) · no usable `flock` on this host, so the shared verify lock was NEVER taken and NOTHING was serialized · ran 21s · declare it in the PR body · pnpm --filter @objectstack/client exec vitest run --maxWorkers=2
os-verify-lock: VERDICT command-exit 0 · UNLOCKED (declared) · no usable `flock` on this host, so the shared verify lock was NEVER taken and NOTHING was serialized · ran 8s · declare it in the PR body · pnpm --filter @objectstack/client typecheck
os-verify-lock: VERDICT command-exit 0 · UNLOCKED (declared) · no usable `flock` on this host, so the shared verify lock was NEVER taken and NOTHING was serialized · ran 2s · declare it in the PR body · pnpm --filter @objectstack/client build
```

- **Build:** `@objectstack/client`'s closure (35 of 81 workspace
projects), then the whole workspace, `turbo run build
--filter='./packages/*' --filter='./packages/*/*'`, 71 of 71 tasks,
after the merge.
- **Tests:** `vitest run`: 50 files, 641 tests passed (every `*.test.ts`
under `src/`; `tests/integration/**` needs a running server and is
excluded by the package's own config).
- **Typecheck:** `pnpm --filter @objectstack/client typecheck` exits 0.
`tsc --listFiles`: `tsconfig.json` compiles the 3 non-test `src` files,
`tsconfig.test.json` all 53 including the 50 test files.
`check:test-typecheck`: 0 files, 0 errors, 0 pinned signatures.
- **Lint:** the repo-wide `pnpm lint` (`eslint . --no-inline-config`)
exits 0 (2026-09-29T18:57:45Z to 18:58:13Z).
- **Citation judging:** after merging `origin/main` (`31ed067639`),
`node scripts/check-issue-citations.mjs --base origin/main` reports "no
issue citations added against 31ed067 (1 file(s) read)" (exit 0);
pinned `--base 31ed067` reads the same.
- **Derived gates:** `node scripts/pm/dispatch-gates.mjs --repo
objectstack-ai/objectstack --commands` derived 61 families. All 61 exit
0, and `--ran` with the exit-coded record reads "61 derived, 61 run, 0
NOT-MEASURED, 0 UNRUN" (a derived zero). Among them:
`check:issue-citations`, `check:doc-authoring` (808 pinned sites, no
growth), `check:nul-bytes` (9,315 files, no raw control bytes),
`check:published-files`, `check:type-check-debt`.
- **Artifact rosters:** 36 of the 39 non-self-test roster rows exit 0,
including the four the derivation marks as keeping their roster under
one of this diff's paths (`check-changeset-fixed`,
`check:authz-resolver`, `check:error-code-casing`,
`check:filter-alias-parity`). The other three need a pull request's
context; they are run against this PR once it exists and reported on the
card. The 18 self-test-only rows grade their checkers' fixtures and
cannot judge this diff.

## Hypotheses (measured first)

- **H0 holds.** At base `3b47a693c7` the filtered census answers 19
sites on 19 lines, 8 numbers, all in `src/index.ts`. The whole-repo
count is 1,298, as on `0be898499f`.
- **H1 holds.** After the rewrite, the filtered census answers 0 for
`packages/client`. No site was left for an open PR (the file lists of
all open PRs were read at 18:36:50Z, 11 PRs, and at 19:01:46Z, 8 PRs:
only the Version Packages PR objectstack-ai#20639 touches `packages/client`, in
`CHANGELOG.md` and `package.json`) or for an unfound anchor.
- **H2 holds on the token reading, not on the `dist` reading.** The
parser leaf-token diff is empty with its controls firing. The emitted
`dist` differs, and the difference is comment text only (token-identical
`dist` with a code control). That is why the changeset ships.

## Acceptance notes

- **Test titles, the form-D stage.** 18 dead numbers remain in test
string literals in `packages/client/src` (`describe` and `it` titles, no
assertion text): `objectstack-ai#12195` 6, `objectstack-ai#12181` 5, `objectstack-ai#14879` 4, `objectstack-ai#9934` 1, `objectstack-ai#8480`
1, `objectstack-ai#6361` 1. They stay on the card for its form-D stage; no string
moved here.
- **Outside `src/**`, a later stage of the card:**
`packages/client/tsconfig.json:8` and
`packages/client/vitest.config.ts:33` cite `objectstack-ai#12181` (404), and
`packages/client/test-typecheck-debt.json:3` cites `objectstack-ai#6083` (404). The
other citations in `packages/client` outside `src/**` (`CHANGELOG.md`
excluded) answer 200.
- **The moving `origin/main`.** The branch merged `origin/main` once
(`afa654081f`, merging `31ed067639`: `lint`, `metadata-protocol` and
`service-datasource`). A later fetch advanced the shared ref to
`a8acee28dd`, two commits in `packages/spec` and a generated reference
page, none touching `packages/client`. There was no second merge; CI
judges the merge ref.

## Deviations

- **One companion line (`client.test.ts:2198`)** beyond the 43 sites,
the second half of the `:2197` sentence.
- **Commit trailers** are AGENTS.md's model-free pair (`Claude-Session`
plus `Co-authored-by: Claude`), and the pre-push trailer check passed on
every push. The harness's attribution reminder asked for a model-named
trailer and a different PR footer, and AGENTS.md overrides it. The merge
commit carries git's default message.

---
_Generated by [Claude
Code](https://claude.ai/code/session_local_1d2a197c-c20e-4e90-9be8-413d4d432289)_

---------

Co-authored-by: Jack Zhuang <50353452+hotlong@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
…mmits that decided them (objectstack-ai#20708)

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

## What changed

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

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

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

Each rewritten line now cites the commit in `origin/main` history that
decided what the line describes, and says in its own words what was
decided: **7 distinct shas**. No number in this package has an ADR or
ruling record of its own in the repository (a grep of `docs/adr/` for
all 8 finds none, and the repository keeps no other ruling-record file
for them), so every anchor is a commit, per ruling C's order. No number
was dropped.

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

**No citation number is added.** Every tracker number on an added line
was already on the line it replaces: `objectstack-ai#12069`
(`translations/index.ts:29`), `objectstack-ai#10246` (`storage-service-plugin.ts:392`)
and the cross-repo `cloud#1395`
(`backfill-sys-file-organizations.ts:86`). Over the whole diff, added
minus removed is 0 or negative for every number, and no number is new to
the diff. No PR number stands on an added line.

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

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

## Census: `service-storage`, 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-storage/`. Each run counts as a reading only
because its board frontier equals the newest issue number, read by a
separate request just before and just after the run.

| reading | tree | board | whole-repo `allocated-but-absent` |
service-storage sites | lines | files | numbers |
|---|---|---|---|---|---|---|---|
| before | base `31ed06763`, run 2026-09-29T18:42:47Z to 18:46:12Z |
enumerated, 186 pages, frontier objectstack-ai#20702 (newest objectstack-ai#20702 before and after),
18,529 numbers | 1,254 | **27** | 27 | 8 | 8 |
| after | head `5db5155a2`, run 18:55:34Z to 18:58:50Z | enumerated, 186
pages, frontier objectstack-ai#20702 (newest objectstack-ai#20702 before and after), 18,529 numbers
| 1,227 | **0** | 0 | 0 | 0 |

The before count matches the seat's census at the claim (27 sites in 8
files, at `6bff748b`). The whole-repo drop is 27, exactly this diff's
census sites. The `resolves` tally is 32,967 in both runs, and
`resolves-as-pull-request` (1,984) and `cross-repo-unjudged` (994) did
not move either. The after run was taken on `5db5155a2`; the head
`09d2ecc96` adds only the changeset. No run was truncated or discarded:
both enumerations read 186 pages at the newest frontier.

**Supplementary instrument, the whole scope.** The census does not read
test files or strings, and this stage's scope includes test comments. So
a second reading runs the gate's own exported `extractCitations`
(whole-file and comment-prose projections) and `namesThisRepository`
over every `.ts` file under `service-storage/src` (71 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 that census judged it on this
board anywhere (its `--list` extraction, 4,943 numbers) and did not
report it. The three numbers the census never saw, because they stand
only in test files (`objectstack-ai#13996`, `objectstack-ai#15607`, `objectstack-ai#17571`), were read one by one
on the issues endpoint, and each answers 200.

| reading | citations | dead | src comment | test comment | src string |
test string |
|---|---|---|---|---|---|---|
| before, `31ed06763` | 608 | **53** | 27 | 15 | 0 | 11 |
| after, `5db5155a2` | 566 | **11** | 0 | 0 | 0 | 11 |

Its src-comment column equals the census's 27, which is the control on
the second instrument. The 554 live citations and the 1 cross-repo
citation are the same in both readings, and the drop of 42 citations is
exactly the rewritten sites. A third, raw reading (every `#` followed by
2 to 6 digits, whatever surrounds it) finds 53 dead occurrences before
and 11 after, and its residue equals the gate's residue site for site.

## Per-number table

Sites and files count every dead occurrence in scope at the base
(comments and strings, tests included). `rewritten / left` counts the
sites rewritten and the sites left. Each anchor was read in its message
and diff, not only its subject, and `git blame` at the base puts each
rewritten line in that commit or in a later one that applied it.

| number | sites / files | rewritten / left | anchor: what it decided |
|---|---|---|---|
| `objectstack-ai#13178` | 19/5 | 14/5 | `f087c376f`: the `sys_file` /
`sys_upload_session` update and delete doors take the acting
organization and scope the statement to it (they stamp nothing), and the
upload routes bind the session they had resolved and discarded. New to
the sweep |
| `objectstack-ai#13279` | 11/4 | 11/0 | `6a180e42d`: a failed permission-store read
raises `AuthzStoreUnavailableError` (503) instead of reading as zero
grants, and the transports' fail-closed nets, this package's file-read
authorizer among them, re-raise it. The anchor of stages 2 and 5 and of
the rest, runtime and types stages |
| `objectstack-ai#10091` | 9/3 | 5/4 | `da891e0ef`: `sys_attachment` `beforeUpdate`
gated by the uploader-or-parent-editor rule, the attach rule on a
re-point, and the update-verb refusal of an unscoped multi-update. New
to the sweep |
| `objectstack-ai#11427` | 6/3 | 4/2 | `c3c72a4bc`: record file-field hydration asks
the reap guard's held-file question, through the batched `findHeldFiles`
this package adds, so hydration and the download path agree about a
tombstoned `sys_file`. Its message ends with a reference to `objectstack-ai#11427`.
New to the sweep |
| `objectstack-ai#6206` | 3/2 | 3/0 | `aa4b90d9a`: the full-envelope ruling applied to
the sharing contract; `ISharingService` takes the whole
`ExecutionContext`, and its docblock says callers "MUST NOT rebuild a
subset of it". Stage 2's anchor, named there as the full-envelope ruling
|
| `objectstack-ai#6523` | 3/2 | 3/0 | `aa4b90d9a`: the same commit, which was
`objectstack-ai#6523`'s change (its subject names it). Stage 2's and the spec stage's
anchor |
| `objectstack-ai#8778` | 1/1 | 1/0 | `7901b2dd2`: stamp-only
`tenancy.organizationField`, with its consumers scope-pinned by the
maintainer's ruling (the pin text is in its diff). The spec and
`plugin-security` stages' anchor |
| `objectstack-ai#11671` | 1/1 | 1/0 | `09b4f4e4e`: the source-hashes provenance
companion. The identical `translations/index.ts` line in
`service-messaging`, `plugin-sharing` and `plugin-security` already
cites it |

Every cited sha matches exactly one commit (`git rev-parse
--disambiguate`, count 1 for each of the 7), and every one is an
ancestor of the base (`merge-base --is-ancestor`, exit 0 for all 7; the
history is complete, `--is-shallow-repository` false, 15,120 commits).
Each of the 8 numbers answers 404 on the issues endpoint, read one by
one before the rewrite.

## Wordings to check

- **The full-envelope ruling, `attachment-access-hooks.ts:127` and
`:129`.** 「what the objectstack-ai#6206 ruling requires … (objectstack-ai#6523)」 became 「what the
full-envelope ruling requires … (commit aa4b90d)」. The quoted words
「MUST NOT rebuild a subset of it」 are the `ISharingService` docblock
that `aa4b90d9a` wrote, so the commit sits beside the quotation. The
same form at `attachment-access-hooks.test.ts:766`.
- **`attachment-access-hooks.test.ts:914-916`.** 「the objectstack-ai#6523 contract's
unit is the envelope, and objectstack-ai#6206 forbids rebuilding a subset of it」
became 「the contract's unit is the envelope (commit aa4b90d), and the
full-envelope ruling forbids rebuilding a subset of it」 (1 reflow line,
`:916`).
- **A heading that named its card,
`attachment-access-hooks.test.ts:621`.** 「objectstack-ai#10091 through the WIRED
engine」 became 「Commit da891e0's gate through the WIRED engine」.
- **The confusion the loud outage prevents,** `storage-routes.ts:201`,
`storage-service-plugin.ts:1057`,
`file-read-tenancy-posture-admission.test.ts:582` and
`storage-routes.authz-outage-relay.test.ts:16`. 「the confusion objectstack-ai#13279
exists to prevent」 became 「the confusion commit 6a180e4 was made to
prevent」: an outage answered as a capability denial is what that
commit's message says it removes.
- **The relay, `storage-service-plugin.ts:1048` and `:1149`.** 「the
objectstack-ai#13279 relay that block already runs」 became 「the relay that block has
run since commit 6a180e4」, and 「takes the objectstack-ai#13279 relay in」 became
「takes the relay (commit 6a180e4) in」. The re-raise in that `catch`
(`:1229`) is in `6a180e42d`'s diff.
- **A referent, `storage-service-plugin.ts:1058-1059`.** 「it had
swallowed the objectstack-ai#13279 permission-store outage at this door since that
card landed」 became 「it had swallowed the branded permission-store
outage at this door since commit 6a180e4 landed」: 「that card」 lost its
referent with the number (1 reflow line, `:1059`).
- **The update/delete halves, `file-reference-lifecycle.ts:111`.** 「the
update/delete halves objectstack-ai#13178)」 became 「the update/delete halves in commit
f087c37)」, beside the live `objectstack-ai#12745` and `objectstack-ai#12928`.
- **Present tense made past,
`tombstone-hydration-download-agreement.test.ts:320`.** 「the divergence
objectstack-ai#11427 fixes」 became 「the divergence commit c3c72a4 fixed」.
- **The scope pin, `backfill-sys-file-organizations.ts:86`.**
「scope-pinned by the objectstack-ai#8778 ruling (widened by name on cloud#1395)」
became 「scope-pinned by its ruling (commit 7901b2d; widened by name on
cloud#1395)」. 「its」 is the key's own ruling, which `7901b2dd2` carried
out and recorded as the pin; the widening is the cross-repo reference
that was already there.
- **Reflow, 2 lines with no dead site** (every file keeps its line
count): `attachment-access-hooks.test.ts:916`,
`storage-service-plugin.ts:1059`.

## The 11 sites left

- **Test titles, 11 sites.** `describe` / `it` titles, which are string
tokens, left as stages 1 to 5 left theirs:
`attachment-access-hooks.test.ts:232`, `:314`, `:640`, `:932`
(`objectstack-ai#10091`); `tenant-audit-update-delete-half-repairs.test.ts:151`,
`:224`, `:345`, `:552`, `:664` (`objectstack-ai#13178`);
`tombstone-hydration-download-agreement.test.ts:148`, `:326` (`objectstack-ai#11427`).
- There is no operator string, assertion message, generated header or
quoted ruling carrying a dead number in this package. The generated
`*.source-hashes.generated.ts` headers are untouched and carry none. The
verbatim maintainer quotations in scope (5 lines: 「同意」 three times,
「12745 A回,其他同意。」 and 「批 objectstack-ai#7 同意」) carry no dead number and are untouched.

## Mechanical guard: no code token moves

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

- Real run: 20,143 base leaf tokens, **0 files with a token change**
(exit 0).
- Comment control in `storage-routes.ts` (`Bound, not discarded` to
`Bound and not discarded`): 0 files changed, as expected (exit 0).
- Positive control, a code token added in `storage-routes.ts` (`const {
fileId, eTag } = req.body ?? {};` given a trailing `?? undefined`):
DIFFER (exit 1).
- Positive control, one digit changed inside a kept test title
(`tombstone-hydration-download-agreement.test.ts:148`): DIFFER (exit 1).

Every mutation went through `scripts/ablation-replace.mjs`, and each
landed (anchor 1 to 0, blob changed). Each restore was proven
byte-identical to the HEAD blob (`44ecc8e64ae0`, `ee84cf718a6f`), with
`git diff HEAD` empty and a clean tree afterwards.

## Changeset

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

Measured on the built package (A3): `files[]` is `dist`, `README.md` and
`CHANGELOG.md`. After the build, the rewritten comments reach `dist`:
`f087c376f` 6 times and `da891e0ef` once in each of `dist/index.d.ts`
and `index.d.cts`; `f087c376f` 4 times and `da891e0ef` once in each of
`index.js` and `index.cjs`. Positive controls: the unchanged line 「the
parent record — the delete rule, applied to the verb that could」 beside
the shipped rewrite at `attachment-access-hooks.ts:28` is found once in
each declaration file, and the unchanged line 「standard catalog code —
the same both-verbs pairing the derived」 beside the shipped rewrite at
`:470` once in each JS file. A never-written negative phrase appears
nowhere in `dist`. None of the 8 dead numbers is left anywhere in
`dist`.

## Gates (head `09d2ecc96`)

- **Citation judging, as CI runs it:** `pnpm check:issue-citations`
(self-test) exits 0. `node scripts/check-issue-citations.mjs` exits 0:
the diff-scoped run judged 3 citations (`objectstack-ai#12069` and `objectstack-ai#10246` resolve;
`cloud#1395` is cross-repo), each already on the line it replaces.
- **Doc authoring:** `pnpm check:doc-authoring` exits 0.
- **Derived gates:** `node scripts/pm/dispatch-gates.mjs --commands
--repo objectstack-ai/objectstack` at `09d2ecc96` derived 65 commands:
all 56 derived at dispatch, plus `check:duration-unit-keys`,
`check:dispatcher-error-vocabulary`, `check:engine-double-contract`,
`check:logger-receiver-detach`, `check:objectql-double-limit`,
`check:query-options-erasure`, `check:type-check-coverage`,
`check:type-check-debt` and `check:where-matcher`. Each ran with its
exit code captured before any pipe, and all 65 exit 0. `--ran`, fed each
command with its exit code, reports 65 run, 0 NOT MEASURED (a derived
zero), 0 unrun, and exits 0. A full `turbo run build` of `./packages/*`
and `./packages/*/*` ran first under the shared verify lock (71 of 71
tasks, exit 0), so no gate hit an unbuilt workspace.
- **Roster families the derivation lists outside its commands** (their
rosters sit in directories this diff touches): `node
scripts/check-changeset-fixed.mjs`, `pnpm check:authz-resolver`, `pnpm
check:error-code-casing` and `pnpm check:filter-alias-parity`, each exit
0.
- **Tests and typecheck, under the verify lock:**
- `pnpm --filter @objectstack/service-storage test`: 40 files pass and
627 tests pass. That is every test file in the package, the 7 touched
ones included.
- `pnpm --filter @objectstack/service-storage typecheck` exits 0 (`tsc`
on `tsconfig.json`, the scripts program, and the test layer on
`tsconfig.test.json`). `--listFiles` on both `tsconfig.json` and
`tsconfig.test.json` shows all 71 files under `src/`, the 40 test files
included, and all 15 touched files in the program.
- **Lint, as a proven narrowing:** `eslint --no-inline-config --format
json` over the 15 touched `.ts` files gives 15 files, 0 errors and 0
warnings. All 15 are in eslint's own population (`isPathIgnored` is
false for each; 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 16 changed files for control bytes finds none.

## Acceptance notes

- **The gate-invisible spellings, grepped as the claim asked.**
`CITATION_RE` refuses a hyphen after the digits and a `/` before the `#`
(objectstack-ai#20636). In this package there is no `#N-word` spelling at all. There
are 11 `#A/#B` lines carrying 13 second numbers
(`attachment-access-hooks.ts:215`, `:217`, `:434`;
`attachment-access-hooks.test.ts:217`; `attachment-lifecycle.ts:177`;
`file-reference-lifecycle.test.ts:226`;
`local-storage-adapter.test.ts:35`; `metadata-store.test.ts:41`;
`storage-route-ledger.ts:74`, which chains four;
`storage-routes.metadata-outage.test.ts:67`;
`tombstone-download-live-reference.test.ts:50`), and every second number
on them is live: `objectstack-ai#5574`, `objectstack-ai#9974`, `objectstack-ai#5541`, `objectstack-ai#5480`, `objectstack-ai#3833` and `objectstack-ai#3847`
by the census's own board, and `objectstack-ai#5197` and `objectstack-ai#3870` read one by one
(200). So nothing there needed rewriting. The claim counted 12 such
spellings on `main`; this reading is 11 lines and 13 second numbers,
with nothing dead among them either way. The raw scan above, which sees
both spellings, agrees.
- **The census instrument did not truncate in this stage.** Both
enumerations read 186 pages at the newest frontier.
- **Anchors the next stages can reuse**, each checked here: `objectstack-ai#13178` →
`f087c376f`; `objectstack-ai#10091` → `da891e0ef`; `objectstack-ai#11427` → `c3c72a4bc`; `objectstack-ai#13279` →
`6a180e42d`; `objectstack-ai#6206` / `objectstack-ai#6523` → `aa4b90d9a`; `objectstack-ai#8778` → `7901b2dd2`;
`objectstack-ai#11671` → `09b4f4e4e`.
- **Base.** The branch is 4 commits behind `main` (`defc7f7b5`, read at
19:31Z). None touches `service-storage`,
`scripts/check-issue-citations.mjs` or `.changeset/config.json`, so
there was no merge.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
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

2 participants