Repository navigation
Commit 6981246
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)
Fixes #22636
Clause-②: yes (widening: a flow whose `record-*` trigger names no start
`config.objectName` opens, so `validate` no longer reports an unbound
flow CEL root there)
Executes ruling `6104584601` (letter B) on #22636: ruling `5791822697`
item 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 in `packages/lint`, plus (patch
round 1, seat order `6105130863`) the correction of two pending release
notes that still called the stand-down temporary.
## What changes
1. **S is reworded from a hold-out to the ruled boundary.** The
stand-down itself (`perWriteSnapshotEntrance`) is unchanged in code. Its
wording changes from "until #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 to
`objectstack validate` (ADR-0109 Decision §3's boundary). The text cites
ruling `5791822697` (item 3) and ruling `6104584601`. It changes in:
- `flow-cel-root-scope.ts`: the module header section (now "The runtime
publish gate stands down, by design") and `perWriteSnapshotEntrance`'s
docblock;
- `validate-expressions.ts`: the comment above `runtimeGateWrite`
(comment only);
- `validate-expressions.flow-cel-root.test.ts`: the S block's docblock
and describe title (now `the runtime publish gate stands down by design
(S)`), and the `record` block's docblock. The S pins themselves are
unchanged: the gate stands down, and the build door refuses the same
root on the same flow.
2. **A `record-*` trigger with no start `config.objectName` opens the
flow.** `flowCelEntrances` gains one open reason. It applies when the
trigger kind is `record_change` (`resolveFlowTriggerKind`: start
`config.triggerType` is a `record-*` string) and the start node names no
`config.objectName`. Measured on the runtime: `deriveTriggerBinding`
(`engine.ts`) binds `object: undefined`, and `record-change-trigger.ts`
registers the hook with `object: 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.
3. **The owed `map`-child pin** (carry note `6096005997`, from contract
review `6095756455` ③). A `map` child 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 same `map` node in a parent no entrance
opens leaves the child judged). Each `map` node's `itemObject` names 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), and `map-node.ts` spreads the parent context and
overrides `record` only for an id-bearing item.
4. **Two pending release notes say "by design" (patch round 1,
`2c06d027e8`).** Under ruling B, two `@objectstack/lint` changesets 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 the `Clause-②` lines are byte-identical.
- `.changeset/22565-flow-cel-unbound-root-refused.md:34`: "a flow write
there is not judged for unbound roots ~~yet~~, by design (rulings
`5791822697` item 3 and `6104584601`)."
- `.changeset/22677-flow-cel-record-entrance.md:20`: "…and a flow write
there is not refused for `record` ~~until that snapshot carries them~~,
by design (rulings `5791822697` item 3 and `6104584601`)."
`.changeset/22636-flow-cel-record-trigger-no-object-opens.md`:
`@objectstack/lint` `minor`, carrying the `Clause-②` line above.
`check-changeset-no-major`'s level axis requires at least `minor` for a
`yes (widening)` declaration.
## Measured before writing
- **Today's behaviour for the objectName-less record trigger: closed and
judged.** `flowCelEntrances` hands the flow `record` and opens nothing.
Its root is judged against every declared object's fields, so `user.id`
is refused there. The new pin was committed first (`49c47a4e51`) and run
on the unfixed source: `Tests 1 failed | 78 passed (79)`, the failure
being `a record trigger that names no start config.objectName stands
down…` with `AssertionError: expected [ { …(4) } ] to deeply equal []`.
The `map`-child pin was green in the same run.
- **The corpus has no such trigger.** Loaded configs (`tsx` import of
each `objectstack.config.ts`), at `e7e6544ab9`: `app-crm` 1 flow and 0
record triggers; `app-multi-package` 0 flows; `app-showcase` 30 flows,
19 record triggers, 19 naming `objectName`; `app-todo` 4 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 commits `337de825af` and
`2c06d027e8` edit 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 --noEmit` plus
`check:test-typecheck`, whose `tsconfig.test.json` includes `src/**/*`,
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.ts`
- The mutation landed on disk: `anchor x1 -> x0`, `replace x0 -> x1`,
blob `274a60f90d10 -> 392c0b3b88f6`.
- Run with the mutation in place: `Tests 1 failed | 78 passed (79)`, the
one red being the new record-trigger pin.
- Restored and proven: `blob == HEAD (274a60f) and git diff HEAD is
empty`, and `git status --porcelain` was empty afterwards.
- The test imports the module by relative path
(`./flow-cel-root-scope.js`), so the run reads `src/`, not `dist/`. No
rebuild leg was needed.
## `objectstack validate` on the four example apps, before and after
`node packages/cli/bin/run.js validate --json` in each example
directory. Before: lint `dist/index.js` built at `49c47a4e51`, the
unfixed source, sha256 `5a2bfdb33967b164…`. After: rebuilt at
`bd22088e0b`, sha256 `3a0412ddb92568a7…`. Every app exits 0 with `valid:
true` both times, and the error and warning lists are byte-identical
before and after (`diff` of the sorted finding arrays: 0 lines).
| app | errors | warnings by id (before = after) |
|---|---|---|
| `app-crm` | 0 | `field-no-consumers` 9 |
| `app-multi-package` | 0 | `field-no-consumers` 3 |
| `app-showcase` | 0 | `approval-approvers-may-resolve-empty` 11,
`field-no-consumers` 51, `flow-draft-status-ambiguous` 1,
`flow-loop-body-uncontained` 2, `liveness-planned-property` 2,
`sdui/jsx-parse-level-only` 1, `security-private-no-readscope` 2,
`title-unresolvable` 1 |
| `app-todo` | 0 | `field-no-consumers` 5, `flow-draft-status-ambiguous`
2 |
The widening removes nothing here because no shipped record trigger
lacks `objectName` (above). It adds nothing anywhere: the change only
sets an `openedBy`, and an open flow is judged for no root.
## No text still promises the snapshot will widen
```text
$ git grep -n "22636" 337de82 -- packages content/docs skills
packages/lint/src/flow-cel-root-scope.ts:156: * this judgment by ruling `6104584601` on #22636 (letter B).
packages/lint/src/validate-expressions.flow-cel-root.test.ts:419: * #22636, letter B). These pins hold the asymmetry: the gate stands down, the
packages/plugins/plugin-security/src/authored-row-write-verdict.test.ts:20:// Probe E-A (#5493 comment 5226364929) is where that costs something real: a
packages/plugins/plugin-sharing/src/authored-row-write-deferral.test.ts:23:// issue comment 5226364929): `checkEdit` abstains — and `canEdit` therefore
(exit 0)
$ git grep -n -i -E "widens? (that |the )?snapshot|until #22636|#22636 (is the carrier|widens)" 337de82 -- packages content/docs skills
(exit 1)
$ git grep -n -i -E "widens? (that |the )?snapshot|until #22636|#22636 (is the carrier|widens)" e7e6544 -- packages content/docs skills # control: the base
packages/lint/src/flow-cel-root-scope.ts:133: * ## The runtime publish gate stands down (#22636 is the carrier)
packages/lint/src/flow-cel-root-scope.ts:143: * widens that snapshot to carry flows and actions; when it lands, this
packages/lint/src/flow-cel-root-scope.ts:469: * there until #22636 widens the snapshot.
packages/lint/src/validate-expressions.flow-cel-root.test.ts:380: * #22636 widens the snapshot; when it lands, this stand-down goes.
packages/lint/src/validate-expressions.flow-cel-root.test.ts:418: * refused. The runtime publish gate's stand-down (S) covers it until #22636.
packages/lint/src/validate-expressions.ts:2172: // (`perWriteSnapshotEntrance`) until #22636 widens that snapshot; every other
(exit 0)
$ git grep -n -i -E "snapshot carries them|unbound roots yet" HEAD -- .changeset # HEAD = 2c06d02 (patch round 1)
(exit 1)
$ git grep -n -i -E "snapshot carries them|unbound roots yet" 337de82 -- .changeset # control: the previous head
.changeset/22565-flow-cel-unbound-root-refused.md:34:- **The runtime publish gate:** a flow write there is not judged for unbound roots yet. …
.changeset/22677-flow-cel-record-entrance.md:20:**Not judged.** The stand-downs of the unbound-root rule are unchanged. At the runtime publi…
(exit 0)
```
The two remaining `#22636` hits in `packages/lint` cite this card's
ruling and promise nothing. The other two hits are a comment id that
contains the digits. A broader `until … widen` grep 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/objectstack` at the final head `337de825af`: 62 commands
(30 pnpm, 32 direct node), the same list as at `bd22088e0b`. 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 at `337de825af` after 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)`.
- **61 run, all exit 0.** Among them:
- `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 `--event` over this body, reads `LEVEL
AXIS: this PR declares clause-② yes (widening), and no package whose
packages/**/src/** it moves is graded patch` and 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-alias` and `check:cross-package-test-inputs`: OK.
- `check:dual-build-cjs-loads` first answered `PREREQUISITE NOT MET`
(exit 3: six unrelated packages had no `dist/`). Those six were built
under the verify lock, and it then exited 0.
- **NOT MEASURED: `pnpm check:type-check-debt`.** The script is
`check-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.
- Also run, outside this derivation: the three roster gates that keep
their roster under `packages/` (`check:authz-resolver`,
`check:error-code-casing`, `check:filter-alias-parity`), all exit 0.
- **NOT MEASURED, CI's own shell** (no local invocation): the five
path-scheduled CI jobs (Test Core, Temporal Conformance, Dogfood
Regression Gate, Dogfood Verify CLI, Build Core) and the workspace
type-check lanes. The lint package's own `typecheck` ran (above).
**Patch round 1, at `2c06d027e8`.** `dispatch-gates --commands --repo
objectstack-ai/objectstack` on 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-files` and `check:pm-changeset-deadline-census`.
**One is red by design:** `check-empty-changeset --base origin/main`
exits 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) is `success` at `337de825af`
and `failure` at `2c06d027e8`. Its two failure annotations are those two
files.
## Acceptance notes
- **Confirmation requested — DELIBERATE CORRECTION of two pending
release notes (`check-empty-changeset`'s foreign-changeset rule).** This
PR changes `.changeset/22565-flow-cel-unbound-root-refused.md` (from PR
#22609) and `.changeset/22677-flow-cel-record-entrance.md` (from PR
#22730). Both are notes this PR did not add. What changed under them:
ruling `6104584601` made 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 into `packages/lint/CHANGELOG.md`.
Ordered by the `domain:spec` seat in `6105130863`; ⛔ do not restore
either file from the base, which would republish the false sentence. Per
the workflow's route for this class, `skip-changeset` is not applied and
the red is left standing for the approver to merge over.
- `RULE_EXPLANATIONS` carries no S wording, so nothing is routed behind
the `domain:spec` seat's #22161 slice 8.
- The ruling names one contract-review-tier review for this PR (its
`Clause-②` is `yes (widening)`).
- The advisory Docs Drift Check listed 13 pages through the
`record_change` string 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.ts` and `authoring-rules.ts` cite ruling `5791822697`
and contain no "until #22636". They are untouched.
- A `triggerType` array (`['record-after-create', …]`) resolves to no
kind in `resolveFlowTriggerKind`, 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 as `flow-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/spec` or the `RuntimeAuthoringIssueSchema.path`
contract.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01S3aAf11JjbW1mSGL1EhfFj)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 95a8435 commit 6981246
6 files changed
Lines changed: 86 additions & 12 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
31 | 31 | | |
32 | 32 | | |
33 | 33 | | |
34 | | - | |
| 34 | + | |
35 | 35 | | |
36 | 36 | | |
37 | 37 | | |
| |||
Lines changed: 13 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
17 | 17 | | |
18 | 18 | | |
19 | 19 | | |
20 | | - | |
| 20 | + | |
21 | 21 | | |
22 | 22 | | |
23 | 23 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
82 | 82 | | |
83 | 83 | | |
84 | 84 | | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
85 | 90 | | |
86 | 91 | | |
87 | 92 | | |
| |||
130 | 135 | | |
131 | 136 | | |
132 | 137 | | |
133 | | - | |
| 138 | + | |
134 | 139 | | |
135 | 140 | | |
136 | 141 | | |
| |||
139 | 144 | | |
140 | 145 | | |
141 | 146 | | |
142 | | - | |
143 | | - | |
144 | | - | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
145 | 157 | | |
146 | 158 | | |
147 | 159 | | |
| |||
380 | 392 | | |
381 | 393 | | |
382 | 394 | | |
| 395 | + | |
| 396 | + | |
| 397 | + | |
| 398 | + | |
383 | 399 | | |
384 | 400 | | |
385 | 401 | | |
| |||
466 | 482 | | |
467 | 483 | | |
468 | 484 | | |
469 | | - | |
| 485 | + | |
| 486 | + | |
| 487 | + | |
470 | 488 | | |
471 | 489 | | |
472 | 490 | | |
| |||
Lines changed: 44 additions & 3 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
294 | 294 | | |
295 | 295 | | |
296 | 296 | | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
| 302 | + | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
| 308 | + | |
| 309 | + | |
| 310 | + | |
| 311 | + | |
| 312 | + | |
297 | 313 | | |
298 | 314 | | |
299 | 315 | | |
| |||
361 | 377 | | |
362 | 378 | | |
363 | 379 | | |
| 380 | + | |
| 381 | + | |
| 382 | + | |
| 383 | + | |
| 384 | + | |
| 385 | + | |
| 386 | + | |
| 387 | + | |
| 388 | + | |
| 389 | + | |
| 390 | + | |
| 391 | + | |
| 392 | + | |
| 393 | + | |
| 394 | + | |
| 395 | + | |
| 396 | + | |
| 397 | + | |
| 398 | + | |
364 | 399 | | |
365 | 400 | | |
366 | 401 | | |
| |||
377 | 412 | | |
378 | 413 | | |
379 | 414 | | |
380 | | - | |
| 415 | + | |
| 416 | + | |
| 417 | + | |
| 418 | + | |
| 419 | + | |
| 420 | + | |
381 | 421 | | |
382 | | - | |
| 422 | + | |
383 | 423 | | |
384 | 424 | | |
385 | 425 | | |
| |||
415 | 455 | | |
416 | 456 | | |
417 | 457 | | |
418 | | - | |
| 458 | + | |
| 459 | + | |
419 | 460 | | |
420 | 461 | | |
421 | 462 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2169 | 2169 | | |
2170 | 2170 | | |
2171 | 2171 | | |
2172 | | - | |
2173 | | - | |
| 2172 | + | |
| 2173 | + | |
| 2174 | + | |
| 2175 | + | |
2174 | 2176 | | |
2175 | 2177 | | |
2176 | 2178 | | |
| |||
0 commit comments