refactor(trigger-record-change): drop the ctx.__previous stash fallback, dead since #6656 (#6978 Item 1) - #7081
Merged
os-project-manager merged 1 commit intoAug 9, 2026
Conversation
…back, dead since #6656 (#6978) `plugin-audit`'s `captureBefore` was the only writer of `ctx.__previous` in the repo; #6656 retired it, leaving `buildContext`'s `ctx.previous ?? ctx.__previous` with a second operand nothing can bind. The engine is the single producer of the pre-image and binds the declared key ahead of every dispatch (engine.ts:7010 for by-id update, bindPreImage at :7869 for by-id delete, :1746/:1825 for the per-row contexts of a predicate write — #5272/#5574/#5846). Removed under ADR-0049 enforce-or-remove rather than kept "for safety": a future producer of `__previous` is now ignored by design, which is the intended post-state (declared = enforced, PD #12). The test that fed the limb synthesised the key in its own body — the #4984 shape that kept it looking live. Replaced with the inverted negative pin the deletion needs, the same treatment #5671 gave the `doc` alias in this file: restoring the limb turns it red on both assertions. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01USNUyHEr7uaU6MoEWXitei
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 1 package(s): ⛔ 1 release-owned page(s) reference the affected code. These are read-only:
|
os-project-manager
marked this pull request as ready for review
August 9, 2026 16:33
os-project-manager
deleted the
claude/issue-6978-previous-stash-dead-limb
branch
August 9, 2026 16:47
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of #6978
Item 1 only. The card carries two items; this PR delivers Item 1 (the dead
ctx.__previousfallback limb and the fixture that fed it, both inpackages/triggers/trigger-record-change). Item 2 — the four stalecaptureBeforecomments inpackages/objectql,packages/metadataandpackages/spec— is deliberately untouched here and awaits the triage splitrecorded in the claim comment: those files belong to
domain:engine-core,domain:metadataand the hard-owned spec seat respectively. SoPart of, notFixes— the card stays open for Item 2.What changed
packages/triggers/trigger-record-change/src/record-change-trigger.ts,buildContext:…plus the doc-comment sentence that described the stash, and the test case that
synthesised the key. A changeset is included (
@objectstack/trigger-record-change,patch) — see "Changeset, not skip-changeset" below.
Premise re-measured on today's
origin/mainThe card's census was taken on #6977's branch. Re-run on
origin/mainat2c7e62d5(2026-08-09), the grep pair still answers the same way — no newproducer appeared:
One hit needs disposing of explicitly, because it is the only one that is live
code rather than prose:
quickjs-runner.ts:494issetGlobalJson(vm, '__previous', ctx.previous). That installs a global insidethe QuickJS sandbox VM, read from the canonical
ctx.previous— it is aread-side convenience for script bodies, not a writer of the host
HookContext.__previous, and the VM is isolated by JSON copy so nothing insideit can write back onto the host ctx. It is therefore not a producer. Every other
non-fixture hit is a comment or a changeset line.
So: producers of
ctx.__previouson today's main = the test fixture, andnothing else. Premise holds.
Why the limb is unreachable, not merely unused
The sharper check the card asked for. The engine binds the canonical
ctx.previousbefore it dispatches, on every write shape, so a consumer thatsees an unbound
previousis seeing "no row was read", never "the row wentsomewhere else":
update()engine.ts:7010—if (priorRecord) hookContext.previous = …:7012triggerHooks('beforeUpdate', …)delete()engine.ts:7869bindPreImage, called at:7897:7899triggerHooks('beforeDelete', …)engine.ts:1825— per-row ctx carriespreviousdispatchPerRowBeforeHooksengine.ts:1746— per-row ctx carriespreviousbuildPerRowAfterContexts(#5272 by-id delete, #5574 per-row before phase, #5846 the update-side
consolidation that retired
sys_fetch_previous_updateon exactly this ground.Line numbers as of
origin/main2c7e62d5.)bindPreImagedeliberately leavespreviousUNBOUND rather than fabricating{}when no row is found (#4649/#4775) — so the one residual "previous isabsent" state is a state where a stash would have had nothing to offer either.
Reverse verification — the inverted direction, as the card predicted
This is the inverted family (#5009 shape), and the report says so rather than
manufacturing a before-green/after-red story: the canonical key sits first in
the
??chain, so deleting the limb changes no outcome any engine path canproduce, and all 55 pre-existing cases stay green unchanged. A positive test
can therefore never carry the weight here — it passes with or without the limb.
The weight is carried by an inverted negative pin, the same treatment #5671
gave the
docalias four cases up in this very file. Measured both ways:56 passed (56)Tests 1 failed | 43 passed (44)inrecord-change-trigger.test.ts, the onefailure being
AssertionError: expected { status: 'old' } to be undefinedondoes NOT read the __previous stash — no producer emits that key (#6978).So the deletion is not invisible to the suite any more, which is the whole point
— the #4984 defect was that it was.
Surviving coverage for the canonical
ctx.previouspathThe diff deletes a test, so here is what still holds the canonical path, by name:
record-change-trigger.test.ts— "fires the callback with a record contextbuilt from the hook ctx": asserts
ctx.previousarrives verbatim as theautomation context's
previous.record-change-trigger.test.ts— "does NOT read adocalias off input":the neighbouring negative pin, which also exercises the absent-
previouscase.record-change-trigger.test.ts— "a record-after-write flow fires on both theinsert hook and the update hook": insert leg with
previous: undefined,update leg with
previousbound.bulk-write-per-row-context.test.ts— "fires PER ROW, withpreviousboundto each row's own pre-write state": end-to-end through the real kernel, the
cross-package contract for the predicate-write shape.
record-change-integration.test.ts— "record-after-write start condition usesprevious == nullto discriminate create vs update (record_change flow start node binds to a single lifecycle event — no create-OR-update in one flow #3427)": the canonicalkey consumed by a real flow condition.
The intended post-state, stated plainly
After this change, a hypothetical future producer of
ctx.__previousissilently ignored. That is intended, not a regression: ADR-0049
enforce-or-remove plus PD #12 — declared = enforced, and an undeclared
side-channel key should not work. The declared way to hand this consumer a
pre-image is
ctx.previous, which is what the engine binds.Changeset, not skip-changeset
@objectstack/trigger-record-changeis a published package and this is abehaviour statement worth a release-notes line, so it ships a
patchchangesetrather than the
skip-changesetlabel — following the house precedent for thisexact family,
.changeset/hook-ctx-doc-alias-reads-removed.md(#5906/#5671),which also recorded "behaviour is unchanged" alias removals as a patch.
Verification
pnpm --filter '@objectstack/trigger-record-change^...' build— green (freshworktree, deps built before testing).
pnpm --filter @objectstack/trigger-record-change test—Test Files 5 passed (5) / Tests 56 passed (56).pnpm --filter @objectstack/trigger-record-change typecheck— green.check:*step enumerated from.github/workflows/lint.yml(not frommemory), run one by one: all 37 ESLint-job steps green (
pnpm lintthroughcheck:tenant-chokepoint), and all TypeScript-Type-Check-job steps greenincluding the full
turbo run build+turbo run typecheckover./packages/*./packages/*/*./apps/*(121 tasks),check:type-check-debt,examples typecheck, downstream-contract typecheck, and the three i18n gates
(which needed the workspace build to measure anything at all).
Out of scope, deliberately
bulk-write-per-row-context.test.ts:15also names__previous, in the"What this used to do, measured" header describing the pre-#5038 world. It is
past-tense history and remains accurate, so it is left alone — and it is outside
the file surface this claim fixed.
🤖 Generated with Claude Code
https://claude.ai/code/session_01USNUyHEr7uaU6MoEWXitei
Generated by Claude Code