Skip to content

Commit 8acdae9

Browse files
docs(trigger-record-change): re-anchor the dead tracker citations to the commits that decided them (#20789)
Part of #20596 Clause-②: no ## What changed This is the thirteenth stage of the `domain:services` lane of the dead-citation sweep. It covers `packages/triggers/trigger-record-change/src/**` and nothing else. By the seat's claim (`5904332626`), it is the largest package in the lane that no in-flight work holds, while `service-automation` stays held behind #20726. 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 #19123), by the method of stages 1 to 12 (PR #20609 as `422db788a`, PR #20626 as `b80ab579d`, PR #20634 as `4d04b6be3`, PR #20658 as `9a4b2bb38`, PR #20693 as `0e9ad74fb`, PR #20708 as `9b384f63a`, PR #20717 as `cbaf04c1f`, PR #20729 as `d2820876f`, PR #20737 as `4dfff176b`, PR #20742 as `697845d19`, PR #20757 as `cba417a8f`, PR #20775 as `91e8fa194`). That is **29 sites on 29 lines in 5 files, covering 3 numbers**: - 6 census sites (every census site this package has, all `#14744`); - 23 sites in test comments, which the census defers: 17 more of `#14744`, 1 of `#13657`, and 5 of `#11081`. `#11081` stands only in a test file here, so the census never judged it; it was read on its own and answers 404. 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: **4 distinct shas**. None of the three numbers has an ADR or ruling record of its own, so every anchor is a commit, per ruling C's order (see the per-number table). No number was dropped. Only comments changed. Every touched source file keeps its line count (30 lines out, 30 in, over 5 files), so no line citation into these files moves. 29 of the 30 changed lines carried a dead citation; the thirtieth keeps a referent the rewrite would otherwise have removed (see Wordings). No code token moves (see the guard below). **No citation number is added.** The only tracker numbers on added lines are the live `#15356` (3 times) and `#8738` (once), each on the line it already stood on. Added minus removed is negative for the three dead numbers and zero for every other number, and no number is new to the diff. No PR number is the citation on an added line. 4 dead sites are left on purpose, all test titles (see the list below). One more file: a `patch` changeset for `@objectstack/trigger-record-change`, because the rewritten prose ships (see Changeset below). ## Census: `trigger-record-change`, 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/triggers/trigger-record-change/`. Each run counts as a reading only because its board frontier equals the newest issue or pull-request number, read by a separate request just before and just after the run. In all three runs a new number was opened while the run was enumerating; each frontier equals the newest number at the run's end, which is the criterion (stages 7 and 11 met the same shape). | reading | tree | board | whole-repo `allocated-but-absent` | trigger-record-change sites | lines | files | numbers | |---|---|---|---|---|---|---|---| | before | base `91e8fa194`, run 2026-09-30T04:58:08Z to 05:01:28Z | enumerated, 187 pages, frontier #20779 (newest #20778 before, #20779 after) | 802 | **6** | 6 | 2 | 1 | | after | `bb9d39a87` (the comments commit), run 05:07:54Z to 05:11:48Z | enumerated, 187 pages, frontier #20780 (newest #20779 before, #20780 after) | 796 | **0** | 0 | 0 | 0 | | after, final head | head `bbfe7cb24`, run 05:39:10Z to 05:42:26Z | enumerated, 187 pages, frontier #20784 (newest #20783 before, #20784 after) | 796 | **0** | 0 | 0 | 0 | The before count matches the seat's census and A1 (6 sites, all `#14744`: `decouple-flow-record.ts` ×1 and `record-change-trigger.ts` ×5). The whole-repo drop is 6, exactly this diff's census sites. The `resolves` tally is 33,055 in all three runs, and `resolves-as-pull-request` (1,984) and `cross-repo-unjudged` (995) did not move either. No run was truncated or discarded: all three enumerations read 187 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 `trigger-record-change/src` (14 files). It takes its verdicts from the before census's own board reading rather than from a second enumeration: a number is dead when that census reported it `allocated-but-absent`, and alive when the gate's own census-scope extraction (36,836 citations over 2,617 files) judged it and the census did not report it. Five numbers are covered by neither, because they stand only in test files: each was read on its own. `#11081` answers 404; `#5715` and `#17982` answer 200 as pull requests; `#5785` and `#17985` answer 200 as issues. The three dead numbers were also read one by one, and each answers 404. | reading | citations | dead | src comment | test comment | src string | test string | |---|---|---|---|---|---|---| | before, `91e8fa194` | 186 | **32** | 6 | 23 | 0 | 3 | | after, `bbfe7cb24` | 157 | **3** | 0 | 0 | 0 | 3 | Its src-comment column equals the census's 6, which is the control on the second instrument. The 154 live citations are the same in both readings, and the drop of 29 citations is exactly the rewritten sites. A third, raw reading (every `#` followed by 2 to 6 digits, whatever surrounds it) finds 195 occurrences before and 166 after. Beyond the gate's grammar it sees 9 tokens, the same at base and head: the second number of five `#A/#B` pairs (only one is dead, the kept title at `before-update-flow-payload-reach.test.ts:872`), two `/#3457/` regex literals in assertions (live), and two `PD #12` ordinals. ## 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. | number | sites / files | rewritten / left | anchor: what it decided | |---|---|---|---| | `#14744` | 27/4 | 22/4 | `4f85e4d11` (PR #15475): the flow-facing `record` (and its `params` alias) and `previous` are decoupled from the engine's own objects before a flow runs (`decoupleFromEngineState`: arrays, plain objects, `Date`, `RegExp`, `Map` and `Set` are copied, primitives, functions and other class instances shared), so a flow mutating a nested value in place no longer writes the batch payload that ADR-0058 Addendum II D3 shares across every row of a `multi: true` update. A COPY rather than a FREEZE, because `expandDeclaredLookups` writes into the record it is handed. The engine's write shape is unchanged, and the same-key per-row-value residue is deliberately left unguarded. Its changeset records the maintainer's option-A ruling on `#14744` in its own words, its diff names `#14744` on 29 added lines, and it created `decouple-flow-record.ts` and both of this package's pin files. `git blame` at the base puts every one of the 22 lines in this commit. New to the sweep | | `#14744` (the census line) | (in the row above) | 1/0 | `03c1b0f6f` (PR #15301): the census of same-key / per-row-VALUE `beforeUpdate` rewrites, which found ZERO across 23 production registration sites and recorded the `buildContext` overlay conclusion as a source reading, not a measurement. Its message names `#14744` four times and states that result word for word. `before-update-flow-payload-reach.test.ts:29` describes this census, not the fix, so it cites the census commit, by the per-arm precedent of stages 5 and 9. The line was written by `4f85e4d11`, which descends from `03c1b0f6f` (`merge-base --is-ancestor` exit 0). New to the sweep | | `#13657` | 1/1 | 1/0 | `b003cf2e8` (PR #13864): the post-hook half of the declared-field door, which refuses an undeclared field a before-hook writes, with one envelope on every driver. Its message names `#13657` seven times. The runtime and lint stages' anchor for the same number. The line was written by `4f85e4d11`, which descends from it (exit 0) | | `#11081` | 5/1 | 5/0 | `c28e4cfae` (PR #11570): the two SqlDriver-backed fixtures stop blanket-silencing their kernel and carry `@objectstack/runtime`'s shared expected-noise capture, which withholds only a declared table's own `no such table` line, forwards every other driver fault, and lets `afterAll` assert each channel fired. Its message names `#11081`, and its diff writes the five `[#11081]` tags in this very file; `git blame` at the base puts all five lines in it. Stage 7's anchor for the same number | Every cited sha matches exactly one commit (`git rev-parse --disambiguate`, count 1 for each of the 4), and all 4 are ancestors of the base (`merge-base --is-ancestor`, exit 0 for each; reverse leg, base against each anchor, exit 1 for each; control legs exit 0: stage 1's landing `422db788a`, and the repository's root commit, which lies deeper than every anchor; the history is complete, `--is-shallow-repository` false, 15,167 commits; the anchors lie 2,516, 2,585, 3,082 and 4,207 commits behind the base). Each of the 3 numbers answers 404 on the issues endpoint, which serves pull requests too. No ADR, `scripts/adr-anchors/` file or other `docs/` page records any of the three as its decision. `docs/audits/2026-09-multi-update-per-row-value-census.md` names `#14744`, but it states that it is "measurement only — ships nothing … implements no guard", the input to a decision rather than its record, so the census line cites the commit that landed it. ## Wordings to check - **Tag swaps in brackets or parentheses.** 「[#14744]」 became 「[commit 4f85e4d]」 at `decouple-flow-record.test.ts:4` and `before-update-flow-payload-reach.test.ts:805`. 「[#11081]」 became 「[commit c28e4cf]」 on 5 lines. 「(#14744, measured by #15356)」 became 「(commit 4f85e4d, measured by #15356)」 at `decouple-flow-record.ts:5`. 「(#14744)」 became 「(commit 4f85e4d)」 at `record-change-trigger.ts:340`. 「(#8738 pre-hook / #13657 post-hook)」 became 「(#8738 pre-hook / commit b003cf2 post-hook)」. - **Headings `:4` and `:859`.** 「[#15356 measured, #14744 closed]」 and 「[#15356 measured it, #14744 closed it]」 keep the live `#15356` and put the sha where the dead number stood. - **`before-update-flow-payload-reach.test.ts:10`.** 「#14744 then ruled the door closed」 became 「The option-A ruling (commit 4f85e4d) then closed the door」: the ruling is named in words beside the commit that carried it, whose changeset records it, the form stages 2, 6 and 7 used for a ruling. - **`:22` and `:87`.** 「the #14744 residue shape」 and 「the #14744 pinned residue shape」 became 「the residue shape commit 4f85e4d pins」: the positive control that pins it is in that commit's diff. - **`:23`.** 「because #14744's fix is about aliasing」 became 「because commit 4f85e4d fixes aliasing」: a commit fixes something, it does not have a fix. - **`:29` and `:34`, the census paragraph.** 「#14744's census found」 became 「The census in commit 03c1b0f found」. That removed the referent of 「The conclusion recorded on that card」 five lines down, so `:34` became 「The conclusion recorded in that census」. This is the one changed line that carried no dead number. It is true as written: the census record `03c1b0f6f` landed carries that very conclusion, "On a source reading, `buildContext` materialises a *new* record object by overlay … a reading, not a measurement" (`docs/audits/2026-09-multi-update-per-row-value-census.md:308-311`). - **`:455`.** 「that is precisely the blind spot #14744 is weighing」 became 「… the blind spot commit 4f85e4d left unguarded」. The present tense described a card still being weighed; that commit's changeset says the key-set refusal "is untouched and is not widened — a hook that assigns the same key with per-row values still passes it". - **「Before #14744」 / 「before #14744」** at `:686`, `:705`, `:738`, `:924` (the word 「Before」 sits at the end of the line above at `:685` and `:704`) became 「before commit 4f85e4d」: before that commit the flow-facing record shared its nested values with the payload, which is the reading each sentence quotes. - **「#14744 made」, 「#14744 carries the fix」, 「#14744 closed the door」** at `:47`, `:95`, `:642`, 「Until #14744」 at `record-change-trigger.ts:341`, 「and #14744.」 at `:124`, 「#14744 — DECOUPLE」 at `:453`, 「(unchanged by #14744 —」 at `:496`: the number became the commit, and each sentence already states what the commit did. ## The 4 sites left - **Test strings, 4 sites on 4 lines**, all `describe` / `it` titles carrying `#14744`, left as stages 1 to 12 left theirs: `before-update-flow-payload-reach.test.ts:825` and `:872` (the second number of `[#15356/#14744]`, a spelling the gate's grammar cannot see), `decouple-flow-record.test.ts:78` and `:136`. - No source string, operator log string, assertion message, quoted maintainer ruling or generated file in this package carries a dead number. - Outside `src`, the package's `CHANGELOG.md` names `#14744` on 2 lines (467, 478). It is release-owned and deliberately not edited here (see Acceptance notes). The package `README.md`, which also ships, names none of the three. ## Mechanical guard: no code token moves The guard compares, base `91e8fa194` against head, over all 5 touched `.ts` files: - **Reading 1**, the TypeScript parser's leaf nodes (a `forEachChild` walk, so comments are trivia and JSDoc nodes are never visited). String and template literals are therefore read in full. - **Reading 2**, the full token stream in parser context (a `getChildren` walk, so punctuation and keywords are included; JSDoc nodes skipped). Results: - Real run at the final head `bbfe7cb24`: 6,110 base leaf tokens, **0 files with a token change** on either reading (exit 0). - Comment control in `record-change-trigger.ts` (「reach nothing outside its own run.」 to 「reach nothing beyond its own run.」): 0 files changed, as expected (exit 0). - Positive control, a code token added in `record-change-trigger.ts` (`params: isolatedRecord,` given `as typeof isolatedRecord`): DIFFER, 953 to 954 leaf tokens and 2,130 to 2,133 full tokens (exit 1). - Positive control, one digit changed inside a kept test title (`decouple-flow-record.test.ts:78`, `#14744` to `#14745`): DIFFER on the string literal (exit 1). Every mutation went through `scripts/ablation-replace.mjs` (wrap mode) under a shell trap that restores by absolute path, and each landed (anchor 1 to 0, blob changed). Each restore was proven byte-identical to the HEAD blob (`f3235a962fc5`, `9a8bf70abbcc`), with `git diff HEAD` empty and a clean tree afterwards. ## Changeset This change ships bytes, so a `patch` changeset for `@objectstack/trigger-record-change` (`.changeset/20596-trigger-record-change-provenance-anchors.md`) is included. Its body is stage 12's, word for word, with the package name changed. Measured on the built package (A3), after a full workspace build in which this package was a cache miss: `files[]` is `dist`, `README.md` and `CHANGELOG.md`, and the package is not private. - `4f85e4d11` appears 3 times in each of `dist/index.js` and `dist/index.mjs`: the `buildContext` docblock (`record-change-trigger.ts:340` and `:341`) and the inline comment at `:496`, which the bundle keeps. - It appears twice in each of `dist/index.d.ts` and `dist/index.d.mts`: the same `buildContext` docblock. - The other three anchors appear nowhere in `dist`: their lines are in test files. The rewrites at `record-change-trigger.ts:124` and `:453` and `decouple-flow-record.ts:5` are stripped by the bundle. - Positive controls, one unchanged line beside each rewrite, land exactly where their neighbours do: the line after `:341` once in all four files, the line before `:496` once in each JS file and 0 in the declaration files, and the neighbours of the three stripped rewrites 0 everywhere. - A never-written negative phrase appears nowhere in `dist`. - None of the three dead numbers is left in `dist`. ## Gates (final head `bbfe7cb24`) - **Citation judging, as CI runs it:** `pnpm check:issue-citations` exits 0 (self-test, 114 cases, 8 batteries). `node scripts/check-issue-citations.mjs` exits 0: the diff-scoped run judged 1 added citation across 2 files, the live `#15356` at `decouple-flow-record.ts:5`, and it resolves. - **Doc authoring:** `pnpm check:doc-authoring` exits 0 (the sibling-package prose-id baseline holds, no growth). - **Derived gates:** `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` at `bbfe7cb24` (after a fresh fetch) derived 59 commands. They are all 53 derived at dispatch, plus `check:engine-double-contract`, `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 59 exit 0; none exited 3. - `--ran`, fed each command with its exit code, reports 59 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, at `bbfe7cb24`:** - `pnpm --filter @objectstack/trigger-record-change test`: 10 files pass and 101 tests pass. `vitest list --filesOnly` names 10 files, all the tracked test files, the 3 touched ones included. - `pnpm --filter @objectstack/trigger-record-change typecheck` exits 0. `tsc --listFiles` on `tsconfig.test.json` holds all 14 files under `src/`, and on `tsconfig.json` the 4 non-test files, so all 5 touched files are compiled. - **Lint, as a proven narrowing:** eslint with inline config disabled, over the 5 touched `.ts` files, gives 5 files, 0 errors and 0 warnings (its `--format json` output). All 5 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 6 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 `#`, `NON_CITATION_HEADS` excuses a number after the word 「option」, and a URL-spelled link carries no `#` at all (#20636). In this package, at the base and at the head: `#N-word` none, `#A/#B` 5 lines, `option #N` none, URL-spelled none, which is the claim's 0 / 5 / 0 / 0. Of the five `#A/#B` second numbers (`#4251` twice, `#5038`, `#4649`, `#14744`), only `#14744` is dead, and it stands in a kept test title. - **`CHANGELOG.md` is left.** `packages/triggers/trigger-record-change/CHANGELOG.md` names `#14744` on 2 lines. It is release-owned (AGENTS.md, Documentation Guardrails), a deferred surface of the citation gate, and ⛔ not part of this stage. - **A live number in a runtime string, left for its lane.** `record-change-trigger.ts:239`'s operator `warn` for an array-form trigger event ends with the live `#3457`, and two tests assert the message carries it. That is form D, not this card's comment-only form C, and the shrink-only `doc-authoring-prose-id` baseline already holds it (`record-change-trigger.ts`: `#3457: 1`), so `check:doc-authoring` sees no growth. - **「The card」 phrases are left.** 3 other comment lines in 2 files of this package speak of 「the card」. They carry no number, neither instrument sees them, and none of them lost a referent in this diff. They are unchanged, as in stages 8 to 12. - **The census instrument did not truncate in this stage.** All three enumerations read 187 pages at the newest frontier. - **Anchors the next stages can reuse**, each checked here: `#14744` → `4f85e4d11` (the decoupling) or `03c1b0f6f` (its census), both new to the sweep; `#13657` → `b003cf2e8` and `#11081` → `c28e4cfae` reuse the runtime and lint stages' anchor and stage 7's. - **Base.** The branch is on `main` at `91e8fa194`. `main` has since moved six commits (`cd6d8a5ff`, `1bcba27d2`, `a3d7588b5`, `9ad654487`, `274e16271`, `085ca6bc1`). Their 50 files touch nothing under `trigger-record-change`, nor `scripts/check-issue-citations.mjs`, `.changeset/config.json` or the `doc-authoring-prose-id` baseline, and none is a path in this diff. Three of them are gate inputs (`scripts/engine-double-contract.pinned.json`, `scripts/objectql-double-limit.baseline.json`, `scripts/sdui-manifest.record.json`), so those families ran here against the base's copies; this diff moves no code token, so nothing here can interact with them. No merge was taken; the merge queue rebuilds on the merged generation. --- _Generated by [Claude Code](https://claude.ai/code/session_01XY5uCwTjZj7884yYtyur4H)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent c8111a5 commit 8acdae9

6 files changed

Lines changed: 40 additions & 30 deletions

File tree

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
'@objectstack/trigger-record-change': patch
3+
---
4+
5+
Provenance comments in `trigger-record-change` were re-anchored
6+
7+
Comment and docblock lines under `src/` that cited tracker numbers which no
8+
longer resolve on GitHub now cite the commit in this repository's history that
9+
decided the matter, and say in their own words what was decided. Comments
10+
only: no type, schema, export, log or refusal text, or runtime behaviour changes.

‎packages/triggers/trigger-record-change/src/before-update-flow-payload-reach.test.ts‎

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
22

33
/**
4-
* [#15356 measured, #14744 closed] PIN — a `record-before-update` flow reaches
4+
* [#15356 measured, commit 4f85e4d11 closed] PIN — a `record-before-update` flow reaches
55
* NO write shape into the BATCH PAYLOAD of a `multi: true` update.
66
*
77
* ## What changed about this file, and what did not
88
*
99
* It was written for #15356 as a MEASUREMENT and it answered NOT BOUNDED: one
10-
* shape (`S5`) reached the payload. #14744 then ruled the door closed —
10+
* shape (`S5`) reached the payload. The option-A ruling (commit 4f85e4d11) then closed the door —
1111
* `buildContext` decouples the flow-facing roots from the engine's own objects
1212
* (`decoupleFromEngineState`) — and this file was adopted whole as the pin.
1313
* `S5`, `S5b` and the SQL replica were FLIPPED to their opposites in that same
@@ -19,19 +19,19 @@
1919
*
2020
* The two controls are why the negatives are readable, and BOTH must keep
2121
* firing: the positive control (a script hook that ASSIGNS the payload — the
22-
* #14744 residue shape — still lands the LAST dispatch's value on every row,
23-
* because #14744's fix is about aliasing and deliberately does not touch that
22+
* residue shape commit 4f85e4d11 pins — still lands the LAST dispatch's value on every row,
23+
* because commit 4f85e4d11 fixes aliasing and deliberately does not touch that
2424
* residue) and the #14099 armed control (divergent key sets are still refused
2525
* whole). If either stops firing, this file has stopped measuring.
2626
*
2727
* ## Why this file exists
2828
*
29-
* #14744's census found the in-repo population of same-key / per-row-VALUE
29+
* The census in commit 03c1b0f6f found the in-repo population of same-key / per-row-VALUE
3030
* `beforeUpdate` payload rewrites is ZERO across 23 production registration
3131
* sites. One door that zero does not bound was named there but never driven:
3232
* `record-change-trigger.ts`'s `start()` binds `beforeUpdate` for the
3333
* `record-before-update` / `record-before-write` trigger types and hands the
34-
* write to USER-AUTHORED FLOW METADATA. The conclusion recorded on that card —
34+
* write to USER-AUTHORED FLOW METADATA. The conclusion recorded in that census —
3535
* `buildContext` materialises a NEW record object by overlay rather than
3636
* handing the flow `ctx.input.data` by reference, so a flow cannot reach the
3737
* batch payload — was labelled by its own author a SOURCE READING, explicitly
@@ -44,7 +44,7 @@
4444
* write shape that mutated a nested value IN PLACE therefore wrote the batch
4545
* payload without ever assigning a top-level key. See `S5` below.
4646
*
47-
* ⭐ #14744 made the reading's sentence TRUE AS STATED rather than deleting it:
47+
* ⭐ Commit 4f85e4d11 made the reading's sentence TRUE AS STATED rather than deleting it:
4848
* the overlay still materialises a new object, and `decoupleFromEngineState`
4949
* now makes that true of the object's CONTENTS too. The distinction is worth
5050
* keeping in front of the next reader — "a new object" and "reaches nothing"
@@ -84,15 +84,15 @@
8484
* ## Two controls, because a negative needs them
8585
*
8686
* - `positive control` — a script `beforeUpdate` hook that DOES assign the
87-
* payload (the #14744 pinned residue shape), in this same harness, showing
87+
* payload (the residue shape commit 4f85e4d11 pins), in this same harness, showing
8888
* the last dispatch's value on every row. Without it firing, every "does not
8989
* reach" below would be a claim about this harness, not about flows.
9090
* - `#14099 armed control` — a hook writing DIVERGENT KEY SETS per row must
9191
* be refused whole, so "the refusal did not fire for the nested shape" is a
9292
* measurement rather than an unarmed check.
9393
*
9494
* ⚠️ #15356 was a MEASUREMENT card: no guard, no write-shape change, no ADR.
95-
* #14744 carries the fix, and it is still not a write-shape change: ADR-0058
95+
* Commit 4f85e4d11 carries the fix, and it is still not a write-shape change: ADR-0058
9696
* Addendum II D3 stands untouched — the engine does not split its own write,
9797
* one payload still serves N rows, and every per-row context is still handed
9898
* that one object (asserted in `S5`). What changed is only that the object a
@@ -203,7 +203,7 @@ function makeDriver(): any {
203203

204204
/**
205205
* `residue` and `tags` are DECLARED fields on purpose: the engine's
206-
* declared-field door (#8738 pre-hook / #13657 post-hook) refuses a payload
206+
* declared-field door (#8738 pre-hook / commit b003cf2e8 post-hook) refuses a payload
207207
* carrying an undeclared key, so a probe writing an undeclared name would be
208208
* measuring that refusal instead of the reach question.
209209
*
@@ -452,7 +452,7 @@ describe('[#15356] can a record-before-update flow reach a multi:true batch payl
452452

453453
// The residue shape: a per-row-VALUE write of the SAME key on every row.
454454
// The key SET is identical across rows, so #14099's divergence refusal does
455-
// not fire — that is precisely the blind spot #14744 is weighing.
455+
// not fire — that is precisely the blind spot commit 4f85e4d11 left unguarded.
456456
const dispatched: string[] = [];
457457
stack.objectql.registerHook(
458458
'beforeUpdate',
@@ -639,7 +639,7 @@ describe('[#15356] can a record-before-update flow reach a multi:true batch payl
639639
/**
640640
* ⭐ S5 — THE PIN. This case was written on 2026-09-04 as a CHARACTERISATION
641641
* of the defect (the nested in-place mutation REACHED the payload, and both
642-
* rows carried both dispatches' contributions). #14744 closed the door on the
642+
* rows carried both dispatches' contributions). Commit 4f85e4d11 closed the door on the
643643
* same day by decoupling the flow-facing roots from the engine's own objects
644644
* (`decoupleFromEngineState`, called at the end of `buildContext`), and the
645645
* case was flipped in the same PR — the assertions below are the OPPOSITE of
@@ -683,7 +683,7 @@ describe('[#15356] can a record-before-update flow reach a multi:true batch payl
683683
expect(observed, 'the script function must have RUN, once per row').toHaveLength(2);
684684
// Row 2 does NOT see row 1's mutation: each dispatch is handed its own copy
685685
// of the nested value, so neither run can observe the other's write. Before
686-
// #14744 the second reading was `["seed","REACHED-alpha"]`.
686+
// commit 4f85e4d11 the second reading was `["seed","REACHED-alpha"]`.
687687
expect(observed[0]?.tagsAsSeen).toBe('["seed"]');
688688
expect(observed[1]?.tagsAsSeen).toBe('["seed"]');
689689
// Reference identity, measured across the same boundary the defect was
@@ -702,7 +702,7 @@ describe('[#15356] can a record-before-update flow reach a multi:true batch payl
702702

703703
// ⭐ The persisted rows: the SET clause carries what the CALLER wrote, and
704704
// no row carries a value derived from the other row's pre-image. Before
705-
// #14744 both rows read `['seed','REACHED-alpha','REACHED-beta']`.
705+
// commit 4f85e4d11 both rows read `['seed','REACHED-alpha','REACHED-beta']`.
706706
expect(wrote, 'and the write still succeeds — this is not a refusal').toMatchObject({ ok: true });
707707
const rows = await rowsByTitle(stack.data, object);
708708
expect(rows.get('alpha')?.tags).toEqual(['seed']);
@@ -735,7 +735,7 @@ describe('[#15356] can a record-before-update flow reach a multi:true batch payl
735735
// ⚠️ THE BREAKING HALF, pinned deliberately. The aliasing was never
736736
// multi-specific: on a by-id write the same in-place mutation reached this
737737
// write's own payload and PERSISTED correctly (`['seed','REACHED']` before
738-
// #14744), so it read as a working per-row write path rather than as
738+
// commit 4f85e4d11), so it read as a working per-row write path rather than as
739739
// corruption. It is the same alias, so closing the door closes it here too,
740740
// and a stack author using it loses a write that used to land. That is why
741741
// the changeset carries a BREAKING banner: the alternative is `update_record`
@@ -802,7 +802,7 @@ describe('[#15356] can a record-before-update flow reach a multi:true batch payl
802802
}, 20000);
803803

804804
/**
805-
* ⭐ [#14744] THE CONTROL ON THE FIX'S SHAPE — why a COPY and not a FREEZE.
805+
* ⭐ [commit 4f85e4d11] THE CONTROL ON THE FIX'S SHAPE — why a COPY and not a FREEZE.
806806
*
807807
* The ruling named deep-copy and freeze as alternatives. They are not
808808
* equivalent, and this case is the measurement that chose between them:
@@ -856,7 +856,7 @@ describe('[#15356] can a record-before-update flow reach a multi:true batch payl
856856
});
857857

858858
/**
859-
* [#15356 measured it, #14744 closed it] S5 again, on the REAL SQL backend —
859+
* [#15356 measured it, commit 4f85e4d11 closed it] S5 again, on the REAL SQL backend —
860860
* `@objectstack/driver-sql` over better-sqlite3 `:memory:`, built the canonical
861861
* way this package's `record-change-integration.test.ts` boots it.
862862
*
@@ -921,7 +921,7 @@ describe('[#15356/#14744] S5 on the real SQL driver — the mutation reaches no
921921
console.log('[#15356] SQL rows:', JSON.stringify([...rows.values()].map((r) => ({ title: r.title, tags: r.tags }))));
922922
const alpha = rows.get('alpha')?.tags;
923923
const beta = rows.get('beta')?.tags;
924-
// Before #14744 both read `['seed','REACHED-alpha','REACHED-beta']` — one
924+
// Before commit 4f85e4d11 both read `['seed','REACHED-alpha','REACHED-beta']` — one
925925
// SET clause carrying both dispatches, including the value derived from the
926926
// other row's pre-image.
927927
expect(alpha).toEqual(['seed']);

‎packages/triggers/trigger-record-change/src/decouple-flow-record.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
22

33
/**
4-
* [#14744] The flow-facing `record` / `previous` roots share no mutable object
4+
* [commit 4f85e4d11] The flow-facing `record` / `previous` roots share no mutable object
55
* with the engine's own state.
66
*
77
* `before-update-flow-payload-reach.test.ts` is the END-TO-END pin: it boots a

‎packages/triggers/trigger-record-change/src/decouple-flow-record.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
/**
44
* Decouple the flow-facing `record` / `previous` roots from the ENGINE-OWNED
5-
* objects they were overlaid from (#14744, measured by #15356).
5+
* objects they were overlaid from (commit 4f85e4d11, measured by #15356).
66
*
77
* ## The leak this closes
88
*

‎packages/triggers/trigger-record-change/src/record-change-integration.test.ts‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ import { SqlDriver } from '@objectstack/driver-sql';
3030
import { AutomationServicePlugin, type AutomationEngine } from '@objectstack/service-automation';
3131
import type { IDataEngine, IObjectQLEngine } from '@objectstack/spec/contracts';
3232
import { RecordChangeTriggerPlugin } from './plugin.js';
33-
// [#11081] `@objectstack/runtime`'s shared expected-noise capture. This import
33+
// [commit c28e4cfae] `@objectstack/runtime`'s shared expected-noise capture. This import
3434
// escapes the package on PURPOSE, so it is DECLARED rather than left for CI to
3535
// discover: `CROSS_PACKAGE_TEST_INPUTS` in
3636
// `scripts/check-cross-package-test-inputs.mjs` names the one file, and
@@ -65,7 +65,7 @@ type TestObjectQLEngine = IObjectQLEngine & {
6565
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
6666

6767
/**
68-
* [#11081] The tables this file deliberately never provisions — and therefore
68+
* [commit c28e4cfae] The tables this file deliberately never provisions — and therefore
6969
* the ONLY read refusals whose log frames may be withheld here.
7070
*
7171
* Every `it` below boots a kernel with no datasource, attaches sqlite late, and
@@ -196,11 +196,11 @@ const authzResolverObjects = [
196196
},
197197
] as const;
198198

199-
/** [#11081] Shared by every kernel this file boots; asserted once in `afterAll`. */
199+
/** [commit c28e4cfae] Shared by every kernel this file boots; asserted once in `afterAll`. */
200200
const noise = captureExpectedReadRefusals([...EXPECTED_ABSENT_PROBE_TABLES]);
201201

202202
/**
203-
* [#11081] The PIN half. ⛔ Repairing a failure here means re-deriving the list
203+
* [commit c28e4cfae] The PIN half. ⛔ Repairing a failure here means re-deriving the list
204204
* above or finding out why a probe stopped firing — NEVER deleting the channel:
205205
* a runtime read that silently stopped happening is exactly the finding this
206206
* assertion exists to make loud.
@@ -243,7 +243,7 @@ afterEach(async () => {
243243
*/
244244
async function attachSqlite(objectql: any): Promise<any> {
245245
const driver = makeSqliteDriver();
246-
// [#11081] Before `connect()` — i.e. before the driver runs any statement, the
246+
// [commit c28e4cfae] Before `connect()` — i.e. before the driver runs any statement, the
247247
// discipline `captureExpectedReadRefusals` documents. `logger` is a protected
248248
// field with a `console` default, so the sink also RESTORES a loud channel:
249249
// an unexpected driver fault reaches the real console from here even though

‎packages/triggers/trigger-record-change/src/record-change-trigger.ts‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ export interface TriggerLogger {
121121
* `ctx.previous`, observed by every OTHER binding sharing the same
122122
* HookContext — pass a copy in. ⚠️ A SHALLOW copy is enough for THIS function
123123
* (it only ever assigns top-level keys), and it is NOT enough for the object
124-
* that reaches a flow: see {@link decoupleFromEngineState} and #14744.
124+
* that reaches a flow: see {@link decoupleFromEngineState} and commit 4f85e4d11.
125125
*
126126
* Exported (module-scope only — NOT re-exported from `index.ts`, so this
127127
* stays off the package's published API) so
@@ -337,8 +337,8 @@ export class RecordChangeTrigger implements FlowTrigger {
337337
* declared fields (see the `materializeDeclaredFields` call below).
338338
*
339339
* ⭐ Both roots it returns are a SNAPSHOT and are DECOUPLED from the
340-
* engine's own state (#14744): a flow can mutate them however it likes and
341-
* reach nothing outside its own run. Until #14744 that was true only of the
340+
* engine's own state (commit 4f85e4d11): a flow can mutate them however it likes and
341+
* reach nothing outside its own run. Until commit 4f85e4d11 that was true only of the
342342
* TOP LEVEL — every overlay here is a shallow spread, so each nested value
343343
* was still the engine's own object, and `inputData` is the batch payload
344344
* ADR-0058 Addendum II D3 shares across every row of a `multi: true` write.
@@ -450,7 +450,7 @@ export class RecordChangeTrigger implements FlowTrigger {
450450
const materializedPrevious =
451451
priorBase && fields ? materializeDeclaredFields({ ...priorBase }, fields) : previous;
452452

453-
// #14744 — DECOUPLE the flow-facing roots from the engine's own objects.
453+
// Commit 4f85e4d11 — DECOUPLE the flow-facing roots from the engine's own objects.
454454
// Every overlay above is a SHALLOW spread, so until this point each
455455
// nested value in `record` is still the engine's: `inputData` is
456456
// `ctx.input.data`, which ADR-0058 Addendum II D3 shares across every
@@ -493,7 +493,7 @@ export class RecordChangeTrigger implements FlowTrigger {
493493
...(session.organizationId ? { tenantId: session.organizationId } : {}),
494494
// Expose the record as params too, so flows with named `isInput`
495495
// variables matching record fields get them seeded. Deliberately the
496-
// SAME object as `record` (unchanged by #14744 — `params` was never a
496+
// SAME object as `record` (unchanged by commit 4f85e4d11 — `params` was never a
497497
// second snapshot, and making it one here would be an observable
498498
// change on top of the aliasing fix).
499499
params: isolatedRecord,

0 commit comments

Comments
 (0)