Repository navigation
feat(lint): a record trigger that names no object opens its flow to the CEL root judge; the runtime gate stand-down is by design - #22762
Conversation
…ap child of an open parent; reword S as by design The record-trigger pin is red until flowCelEntrances gains the open reason in the next commit; the map-child pin holds behaviour already on main. Claude-Session: https://claude.ai/code/session_01S3aAf11JjbW1mSGL1EhfFj Co-authored-by: Claude <noreply@anthropic.com>
…he CEL root judge; the runtime gate's stand-down is by design flowCelEntrances gains one open reason: a flow whose trigger kind is record_change and whose start node names no config.objectName. The record-change trigger then registers its hook with no object filter, fires on a write to any object and hands that row, so the build door cannot know the record's keys. The trigger shape is not refused. The runtime publish gate's stand-down of this judgment (S) is reworded from a hold-out awaiting a wider per-write snapshot to the ruled boundary: a judgment that reads other items of the stack is objectstack validate's, and the snapshot is not widened for it. Claude-Session: https://claude.ai/code/session_01S3aAf11JjbW1mSGL1EhfFj Co-authored-by: Claude <noreply@anthropic.com>
…boundary, without pointing at other entries Claude-Session: https://claude.ai/code/session_01S3aAf11JjbW1mSGL1EhfFj Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 13 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 2 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 4 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 1e251073cbf8a62cddfd10cb1ff005ddaadc1839 && git checkout 1e251073cbf8a62cddfd10cb1ff005ddaadc1839
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin e86530088a137275d803db29cb7c3afbc0f9f10e 2c06d027e8eb501d1f6c053f45d1f4f1a15a0b8b && git checkout -B drift-repro e86530088a137275d803db29cb7c3afbc0f9f10e && git merge --no-ff 2c06d027e8eb501d1f6c053f45d1f4f1a15a0b8b
node scripts/docs-audit/affected-docs.mjs --json e86530088a137275d803db29cb7c3afbc0f9f10e
|
… gate's stand-down by design Each note's "yet" / "until that snapshot carries them" clause becomes "by design", citing rulings 5791822697 item 3 and 6104584601; the rest of each sentence, the front matter, the levels and the Clause-② lines are unchanged. Claude-Session: https://claude.ai/code/session_01S3aAf11JjbW1mSGL1EhfFj Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Read-only, at 2026-10-11T04:10Z: card #22636 (body and all 15 comments, including ruling ① Derived judgments1. The one accept-set change — a flow whose start
2. The widening is exactly the 3. Nothing moves at the runtime publish door — right; S stands. 4. S re-worded; its code and pins unchanged — right. The three 5. The owed 6. The record-trigger pin is a true pin of the change. 7. Docs — no false shipped sentence. The drift comment's 13 pages are listed through the generic Check-runs on the head (the read above; 40 runs): 31 success, 5 skipped (Auto Label and Check PR Size duplicates from the body-edit re-fire, Build Docs, Console Pin Gate, Packed-tarball smoke opt-in), 2 failure (both DELIBERATE CORRECTION — written confirmation (
Both notes are still PENDING: present on ② Semver level
③ Boundary flagsEvery flag in
Implemented-by: VERDICT: PASS |
|
Designed red, recorded before queueing ·
|
Fixes #22636
Clause-②: yes (widening: a flow whose
record-*trigger names no startconfig.objectNameopens, sovalidateno longer reports an unbound flow CEL root there)Executes ruling
6104584601(letter B) on #22636: ruling5791822697item 3 stands, so the runtime publish gate keeps its per-write snapshot and its stand-down on the flow CEL unbound-root judgment is by design. W (the snapshot carrying flows and actions) is not built. This PR carries the three items the ruling names, all inpackages/lint, plus (patch round 1, seat order6105130863) the correction of two pending release notes that still called the stand-down temporary.What changes
perWriteSnapshotEntrance) is unchanged in code. Its wording changes from "until lint(runtime gate): a flow write's per-write snapshot carries no actions and no other flows, so a flow CEL root rule cannot see the entrances that hand a flow its record (W, from #22565) #22636 widens the snapshot" to "by design": who launches a flow and what record it is handed are read from other items of the stack, so this is a whole-stack judgment and belongs toobjectstack validate(ADR-0109 Decision §3's boundary). The text cites ruling5791822697(item 3) and ruling6104584601. It changes in:flow-cel-root-scope.ts: the module header section (now "The runtime publish gate stands down, by design") andperWriteSnapshotEntrance's docblock;validate-expressions.ts: the comment aboveruntimeGateWrite(comment only);validate-expressions.flow-cel-root.test.ts: the S block's docblock and describe title (nowthe runtime publish gate stands down by design (S)), and therecordblock's docblock. The S pins themselves are unchanged: the gate stands down, and the build door refuses the same root on the same flow.record-*trigger with no startconfig.objectNameopens the flow.flowCelEntrancesgains one open reason. It applies when the trigger kind isrecord_change(resolveFlowTriggerKind: startconfig.triggerTypeis arecord-*string) and the start node names noconfig.objectName. Measured on the runtime:deriveTriggerBinding(engine.ts) bindsobject: undefined, andrecord-change-trigger.tsregisters the hook withobject: binding.object, so the hook has no object filter. It fires on a write to any object, including one the stack does not declare, and hands that row (binding.object ?? ctx.object). The build door cannot know the record's keys, so it leaves the flow unjudged, as it already does for a record trigger on an undeclared object. ⛔ The trigger shape is not refused (no new gate, no narrowing). The module header's entrance list gains the bullet.map-child pin (carry note6096005997, from contract review6095756455③). Amapchild of an open parent is opened by the fixpoint. The pin has three parts: one level, two levels (g → map → h → map → f), and a red control (the samemapnode in a parent no entrance opens leaves the child judged). Eachmapnode'sitemObjectnames a declared object, so the node cannot open the child by itself; only the parent's openness can. No behaviour change: the pin was green before the fix commit (below), andmap-node.tsspreads the parent context and overridesrecordonly for an id-bearing item.2c06d027e8). Under ruling B, two@objectstack/lintchangesets that have not been released yet were stating something false. In each, only the "yet" / "until …" clause changes; the rest of the sentence, the front matter, the levels and theClause-②lines are byte-identical..changeset/22565-flow-cel-unbound-root-refused.md:34: "a flow write there is not judged for unbound rootsyet, by design (rulings5791822697item 3 and6104584601).".changeset/22677-flow-cel-record-entrance.md:20: "…and a flow write there is not refused forrecorduntil that snapshot carries them, by design (rulings5791822697item 3 and6104584601).".changeset/22636-flow-cel-record-trigger-no-object-opens.md:@objectstack/lintminor, carrying theClause-②line above.check-changeset-no-major's level axis requires at leastminorfor ayes (widening)declaration.Measured before writing
flowCelEntranceshands the flowrecordand opens nothing. Its root is judged against every declared object's fields, souser.idis refused there. The new pin was committed first (49c47a4e51) and run on the unfixed source:Tests 1 failed | 78 passed (79), the failure beinga record trigger that names no start config.objectName stands down…withAssertionError: expected [ { …(4) } ] to deeply equal []. Themap-child pin was green in the same run.tsximport of eachobjectstack.config.ts), ate7e6544ab9:app-crm1 flow and 0 record triggers;app-multi-package0 flows;app-showcase30 flows, 19 record triggers, 19 namingobjectName;app-todo4 flows, 1 record trigger, 1 naming it.RULE_EXPLANATIONS(rule-explanations.ts) carries no S wording: no "until", "snapshot" or stand-down sentence for the flow CEL root judgment. Not touched.Tests (at
bd22088e0b; the later commits337de825afand2c06d027e8edit only changeset text)pnpm --filter @objectstack/lint exec vitest run --maxWorkers=2(the whole package):Test Files 136 passed (136),Tests 6353 passed (6353).pnpm --filter @objectstack/lint typecheck(tsc --noEmitpluscheck:test-typecheck, whosetsconfig.test.jsonincludessrc/**/*, so it compiles the test file): exit 0,check:test-typecheck: OK … 2 file(s) / 6 error(s) / 2 pinned signature(s) held(pre-existing, unchanged; the edited test file is not in the ledger, so it compiles with zero errors).pnpm --filter @objectstack/lint build: exit 0,check-dts-emitted: @objectstack/lint - 6/6 declared declaration file(s) present.Ablation of the new open reason.
node scripts/ablation-replace.mjs --file packages/lint/src/flow-cel-root-scope.ts --anchor "if (kind === 'record_change' && !triggerObject) {" --replacement "if (false && kind === 'record_change' && !triggerObject) {" -- pnpm --filter @objectstack/lint exec vitest run --maxWorkers=2 src/validate-expressions.flow-cel-root.test.tsanchor x1 -> x0,replace x0 -> x1, blob274a60f90d10 -> 392c0b3b88f6.Tests 1 failed | 78 passed (79), the one red being the new record-trigger pin.blob == HEAD (274a60f90d10) and git diff HEAD is empty, andgit status --porcelainwas empty afterwards../flow-cel-root-scope.js), so the run readssrc/, notdist/. No rebuild leg was needed.objectstack validateon the four example apps, before and afternode packages/cli/bin/run.js validate --jsonin each example directory. Before: lintdist/index.jsbuilt at49c47a4e51, the unfixed source, sha2565a2bfdb33967b164…. After: rebuilt atbd22088e0b, sha2563a0412ddb92568a7…. Every app exits 0 withvalid: trueboth times, and the error and warning lists are byte-identical before and after (diffof the sorted finding arrays: 0 lines).app-crmfield-no-consumers9app-multi-packagefield-no-consumers3app-showcaseapproval-approvers-may-resolve-empty11,field-no-consumers51,flow-draft-status-ambiguous1,flow-loop-body-uncontained2,liveness-planned-property2,sdui/jsx-parse-level-only1,security-private-no-readscope2,title-unresolvable1app-todofield-no-consumers5,flow-draft-status-ambiguous2The widening removes nothing here because no shipped record trigger lacks
objectName(above). It adds nothing anywhere: the change only sets anopenedBy, and an open flow is judged for no root.No text still promises the snapshot will widen
The two remaining
#22636hits inpackages/lintcite this card's ruling and promise nothing. The other two hits are a comment id that contains the digits. A broaderuntil … widengrep over the same roots finds only unrelated sentences (sharing grants, driver-memory, the spec changelog).Gates
Derived with
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackat the final head337de825af: 62 commands (30 pnpm, 32 direct node), the same list as atbd22088e0b. Each was run with its exit code captured before any pipe. The diff-reading ones (the changeset, citation, docs-audit, doc-authoring and nul-byte gates) were re-run at337de825afafter the last commit. Reconciled with--ran:✓ dispatch-gates --ran: 62 derived famil(ies) accounted for — 61 run, 1 NOT-MEASURED (0 DERIVED from a recorded exit 3, 1 claimed).check-adr-0087-registration:this PR adds no declared-breaking changeset (1 non-breaking changeset(s) seen);check-changeset-no-major:This diff introduces no major bump; its level axis, driven offline with--eventover this body, readsLEVEL AXIS: this PR declares clause-② yes (widening), and no package whose packages/**/src/** it moves is graded patchand exits 0;check-empty-changeset:No changeset from the merge base modified or deleted by this diff;check-issue-citations:every citation this change adds resolves;check:nul-bytes:scanned 10782 text file(s) … no raw ASCII control bytes;check:dts-closure:161/161 declared declaration file(s) present across 62 package(s);check:test-source-aliasandcheck:cross-package-test-inputs: OK.check:dual-build-cjs-loadsfirst answeredPREREQUISITE NOT MET(exit 3: six unrelated packages had nodist/). Those six were built under the verify lock, and it then exited 0.pnpm check:type-check-debt. The script ischeck-type-check-coverage.mjs --re-measure, which the dispatch forbids running locally; CI runs it.pnpm check:type-check-coverage(no re-measure) ran: exit 0.packages/(check:authz-resolver,check:error-code-casing,check:filter-alias-parity), all exit 0.typecheckran (above).Patch round 1, at
2c06d027e8.dispatch-gates --commands --repo objectstack-ai/objectstackon the two changeset paths derived 20 commands. 19 exited 0, among them:check-adr-0087-registration --base origin/main:this PR adds no declared-breaking changeset (3 non-breaking changeset(s) seen);check-changeset-no-major --base origin/main:This diff introduces no major bump;check:nul-bytes,check:changeset-gate-self-tests,check:published-filesandcheck:pm-changeset-deadline-census.One is red by design:
check-empty-changeset --base origin/mainexits 1 on the foreign-changeset rule, naming both files. This is its DELIBERATE CORRECTION class, so the confirmation request is below.Check Changeset(not a required context) issuccessat337de825afandfailureat2c06d027e8. Its two failure annotations are those two files.Acceptance notes
check-empty-changeset's foreign-changeset rule). This PR changes.changeset/22565-flow-cel-unbound-root-refused.md(from PR feat(lint)!: refuse a flow CEL root the flow does not bind, naming current_user for the run-user aliases #22609) and.changeset/22677-flow-cel-record-entrance.md(from PR fix(lint)!: flow CELrecordis bound only where an entrance hands the flow a record (#22677) #22730). Both are notes this PR did not add. What changed under them: ruling6104584601made the runtime gate's stand-down permanent, so "not judged … yet" and "not refused … until that snapshot carries them" became false. Each now says "by design", and nothing else in either file moved. Neither note has been consumed intopackages/lint/CHANGELOG.md. Ordered by thedomain:specseat in6105130863; ⛔ do not restore either file from the base, which would republish the false sentence. Per the workflow's route for this class,skip-changesetis not applied and the red is left standing for the approver to merge over.RULE_EXPLANATIONScarries no S wording, so nothing is routed behind thedomain:specseat's [maintainer] validate: thefield-no-consumerswarning is one 856-character line, printed by validate, build and dev alike — one-line verdict +rule:id + a pointer to the full reasoning (os explain, which today takes only schema names) #22161 slice 8.Clause-②isyes (widening)).record_changestring literal. None of them describes the build door's unbound-root judgment:git grep -n -i -E "unbound root|Unknown variable: (user|record)|is not bound in|flowCelEntrances|objectName.{0,40}(omit|without|no )"over the 13 exits 1.runtime-gate.tsandauthoring-rules.tscite ruling5791822697and contain no "until lint(runtime gate): a flow write's per-write snapshot carries no actions and no other flows, so a flow CEL root rule cannot see the entrances that hand a flow its record (W, from #22565) #22636". They are untouched.triggerTypearray (['record-after-create', …]) resolves to no kind inresolveFlowTriggerKind, so it does not reach the new open reason. The engine routes that shape only so it can refuse it at bind, and lint already reports it asflow-trigger-unknown-event. Unchanged, by that resolver's documented decision.Not in this PR
W as filed: no change to
RuntimeStackContext,CONTEXT_STACK_KEYS,protocol.ts,packages/specor theRuntimeAuthoringIssueSchema.pathcontract.Generated by Claude Code