Repository navigation
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
Conversation
…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>
📓 Docs Drift CheckThis PR changes 1 package(s): 7 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 5 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 6 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # 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
|
…/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>
…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>
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 runshydrateFlowActivations(), which leaves a switched-off flow unbound. The trigger plugins register later, atkernel:ready, andAutomationEngine.registerTriggerarmed every registered, unbound flow of the matching type throughactivateFlowTriggerwithout asking whether the flow may run. SoGET /api/v1/automation/_statusreportedenabled: false, bound: true. Every matching event then fired a run thatexecute()refused withFLOW_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
mainat the engine level (14f80e23, the pins in commit1a7ce83b7run against the unfixed engine): a boot replay over one activation store, inplugin.ts's order (pull, hydrate,kernel:readyprotocol sync, trigger registration, seal), reads on the first restart:That is the card's
/_statusshape. 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.tsonly)activateFlowTriggeritself. It refuses to arm a flowisFlowEnabledanswersfalsefor, before the scheduled-work policy gate and before the trigger lookup.registerFlow,registerTrigger, the enable half oftoggleFlow, and any arming path added later inherit it.registerFlow's own caller-sideif (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.FLOW_DISABLEDanswer that still reaches the callback (an event in flight at the switch-off, or a trigger whosestop()failed) is logged atinfo: 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 aterror.errorline. That line says "the terminal failure is recorded in the flow's run history" only when the result carriesstatus: 'failed', the verdictexecute()'s ran-and-failed exit andretryExecution's exit set after writing their failed row. A never-dispatched answer (nostatus) gets the same line with "it was refused before it dispatched" in place of the history claim.The dispatch's mechanism assumptions, measured
registerTrigger(was about :3368) calledactivateFlowTrigger(name)for every unbound flow of the matching type. Neither method consultedisFlowEnabled.registerFlow's only ledger check was the caller-side guard aroundactivateFlowTrigger(was :4271). The one at :4257 guards the node-type warning, not arming. The ordering is inplugin.ts: hydrate runs instart(), and the protocol sync and trigger plugins run atkernel:ready.activateFlowTrigger. There are three:registerTrigger,registerFlowandtoggleFlow's enable branch. The only refused-flow bookkeeping ispolicyDisabledFlows(the scheduled-work refusal record). A disabled flow now returns before the policy gate, so it records no policy refusal.describeUnboundReasonalready answeredundefinedfor a disabled flow, so neither the/_statusreasonnorgetTriggerBindingAudit()changes./_status'sboundreadsboundFlowTriggers, which is nowfalsefor the flow.toggleFlow(name, true)on a flow whosestatusisobsoleteorinvalidno longer arms it. Before, it was armed and every event it fired was refused withFLOW_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 byregisterTriggertoo. Both are pinned.FLOW_DISABLEDreaches the log.execute()'s disabled exit returns before a run id is minted and before anyrecordLog, 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.status: 'failed'. The fourstatus-less failure exits are flow not found,FLOW_DISABLED,FLOW_NO_START_NODEandFLOW_INPUT_SCHEMA_INVALID. The last one does write a failed row but carries nostatus, so the line makes no history claim for it: true, if less informative.PR #20551's arm-time
apisecret refusal is untouched. The gate sits beforetrigger.start.validateApiTriggerSecretat registration andApiTrigger.start()'s throw (into the existing Failed-to-bindwarn) are unchanged, andapi-trigger-secret-registration.test.tsis green.Tests (all read at
758967616, the last commit)src/flow-activation-late-trigger.test.ts, 11 cases:hydrateFlowActivations(), for each ofrecord_change,schedule,time_relativeandapi: the switched-off flow stays{ enabled: false, bound: false }, and its enabled sibling of the same kind arms (the control).active: true.obsoleteflow does not arm it.enabled: false, bound: falseon both boots, the sibling bound, and the binding audit empty.FLOW_DISABLEDis logged atinfowith no history claim andlistRunsis empty. A dispatched failure is logged aterrorwith the claim and onefailedrow. A never-dispatched failure is logged aterrorwith 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, thencheck:test-typecheck: OK).tsc -p tsconfig.test.json --listFileslists the new test file (1 hit).scripts/ablation-replace.mjsin wrap mode (anchor must hit once, the restore is armed on EXIT/INT/TERM, and a shell trapgit checkout HEAD --sits behind it). The subject resolves fromsrc/through the relative./engine.jsimport, so nodist/leg applies.if (!this.isFlowEnabled(flowName)) return;, anchor x1 then x0, blob566ed4d67921thend09694f76678):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_DISABLEDbranch disabled:Tests 1 failed | 10 passed. The failing case is theinfopin:expected [ Array(1) ] to deeply equal [], where the array holds an error line.Tests 1 failed | 10 passed. The failing case is the never-dispatched pin, which now claims a run-history row.ok restored: blob == HEAD (566ed4d67921) and git diff HEAD is empty.git status --porcelainis empty afterwards.Gates
node scripts/pm/dispatch-gates.mjs --commandsre-derived against the real diff gives the same 62 commands as the dispatch list, byte-for-byte after sort.pnpm check:dual-build-cjs-loadsandpnpm check:type-check-debtboth exit 3 (PREREQUISITE NOT MET: they read the whole workspace's builtdist/, 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).node scripts/check-changeset-fixed.mjs,pnpm check:authz-resolver,pnpm check:error-code-casingandpnpm check:filter-alias-parity. Added for the log-level and arming edit, also exit 0:pnpm check:durability-log-levelandpnpm check:startup-registry-verdict.pnpm lintover the whole repo is CI's):eslint --no-inline-config --format jsononengine.tsand the new test file..mdhas no matching config (--print-configanswersundefined, and the JSON says "File ignored because no matching configuration was supplied"), so it is outside eslint's population.eslint.config.mjsnever enables type-aware linting (noparserOptions.projectorprojectService), 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.mdis apatchon@objectstack/service-automation. No export, option, route or response shape moves.Acceptance notes
plugin.ts's boot order over one sharedInMemoryFlowActivationStore. A LiteKernel boot of the real plugin was not built. The plugin attaches the durable ledger only through anobjectqldata engine with find/insert/update, so it would need a new data-engine double undercheck:engine-double-contract. The defect lives in the engine's arming path.api-trigger-secret-registration.test.tssays "anobsoleteflow 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.recordLogitself throws onexecute()'s failure arm, the result still carriesstatus: '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/mainhas moved 3 commits since the base (3b47a693c,0be898499,f379f57f4). None touchespackages/services/service-automationorpackages/spec/src/contracts, so the branch is not merged forward. CI and the merge queue validate the merge ref.Generated by Claude Code