Repository navigation
Commit 096ac40
Fixes #24669
Clause-②: yes (narrowing)
Under a package closure the runtime save door states, a flow whose
trigger object is outside that closure is now **refused**: `422
INVALID_METADATA`, one `flow-trigger-unknown-object` issue at `error`,
at either position the rule reads (`config.objectName` on a `record-*`
start node, or `config.timeRelative.object`). The dataset door
(`object-reference-unknown`) and the view door
(`list-view-object-outside-closure`) already give this verdict for the
same reference under the same closure, so one mistake now gets one
answer at all three doors (ruling #9612). A `sys_` name is still not
judged.
With no closure stated, the advisory is byte-identical to before. That
covers the three CLI commands, a write that names no package, the
`sys_metadata` sentinel, and a package the registry cannot produce.
Drafts stay ungated, and a stored row is judged at its next publish.
## Step 1: the census (triage `6279216806`'s first act, measured before
any code)
**Method.** I parsed, with the TypeScript compiler API, every `.ts` /
`.tsx` / `.mts` file in this repository at `a43405d596` (this branch's
base) that contains a `type: 'start'` literal: 379 files and 930 start
nodes. I read each start node's `config.objectName`,
`config.triggerType` and `config.timeRelative.object`. 136 start nodes
carry a record trigger (a `record-*` token, or a non-literal one) or a
time-relative descriptor.
**Outside tests: 21 hits, 0 outside their package's closure.** Each
trigger object is defined by the same single-package example stack
(`com.example.showcase` / `com.example.todo`): `showcase_task` ×12 (one
of them a sweep), `showcase_project` ×3, `showcase_expense_report` ×2,
and `showcase_field_zoo`, `showcase_announcement`, `showcase_invoice`
and `todo_task` ×1 each.
| file:line (at `a43405d596`) | flow | trigger object (position) |
|:--|:--|:--|
|
`examples/app-showcase/src/automation/flows/approver-bindings.flow.ts:63`
| `showcase_approver_bindings` | `showcase_field_zoo` (`objectName`) |
|
`examples/app-showcase/src/automation/flows/dynamic-approval.flow.ts:22`
| `showcase_dynamic_approval` | `showcase_announcement` (`objectName`) |
| `examples/app-showcase/src/automation/flows/index.ts:33` |
`showcase_task_completed` | `showcase_task` (`objectName`) |
| `examples/app-showcase/src/automation/flows/index.ts:161` |
`showcase_task_assigned_notify` | `showcase_task` (`objectName`) |
| `examples/app-showcase/src/automation/flows/index.ts:216` |
`showcase_budget_approval` | `showcase_project` (`objectName`) |
| `examples/app-showcase/src/automation/flows/index.ts:333` |
`showcase_task_completed_slack` | `showcase_task` (`objectName`) |
| `examples/app-showcase/src/automation/flows/index.ts:471` |
`showcase_task_completed_rest_ping` | `showcase_task` (`objectName`) |
| `examples/app-showcase/src/automation/flows/index.ts:530` |
`showcase_declarative_connector_ping` | `showcase_task` (`objectName`) |
| `examples/app-showcase/src/automation/flows/index.ts:583` |
`showcase_mcp_connector_echo` | `showcase_task` (`objectName`) |
| `examples/app-showcase/src/automation/flows/index.ts:632` |
`showcase_task_follow_up` | `showcase_task` (`objectName`) |
| `examples/app-showcase/src/automation/flows/index.ts:724` |
`showcase_task_done_notify_owner` | `showcase_task` (`objectName`) |
| `examples/app-showcase/src/automation/flows/index.ts:845` |
`showcase_project_closure` | `showcase_project` (`objectName`) |
| `examples/app-showcase/src/automation/flows/index.ts:1003` |
`showcase_fan_out_notify` | `showcase_task` (`objectName`) |
| `examples/app-showcase/src/automation/flows/index.ts:1210` |
`showcase_resilient_sync` | `showcase_task` (`objectName`) |
| `examples/app-showcase/src/automation/flows/index.ts:1309` |
`showcase_invoice_signoff` | `showcase_invoice` (`objectName`) |
| `examples/app-showcase/src/automation/flows/index.ts:1411` |
`showcase_project_escalation` | `showcase_project` (`objectName`) |
| `examples/app-showcase/src/automation/flows/index.ts:1758` |
`showcase_expense_signoff` | `showcase_expense_report` (`objectName`) |
| `examples/app-showcase/src/automation/flows/index.ts:1808` |
`showcase_committee_quorum` | `showcase_expense_report` (`objectName`) |
| `examples/app-showcase/src/automation/flows/index.ts:1878` |
`showcase_task_due_reminder` | `showcase_task` (`timeRelative.object`) |
| `examples/app-showcase/src/automation/flows/index.ts:1937` |
`showcase_urgent_task_alert` | `showcase_task` (`objectName`) |
| `examples/app-todo/src/flows/task.flow.ts:324` | `task_completion` |
`todo_task` (`objectName`) |
**Zero flows** in:
- `packages/apps/**`, `packages/platform-objects/**`,
`packages/plugins/**`, `packages/services/**`;
- the `create-objectstack` blank template (its `src/flows/index.ts` is
`export {};`);
- `examples/app-multi-package` (the one multi-package example),
`examples/app-crm` (one screen flow, no trigger),
`examples/embed-objectql`;
- any YAML/JSON flow file.
The `os g flow` scaffold binds `--object` or the stack's only object, so
it stays in-stack by construction.
**`packages/qa/**`.** Every dogfood fixture's trigger object is declared
by the same fixture stack. I resolved the 13 constant-named ones by
hand. The upgrade-harness SQL holds only run records of showcase flows
on `showcase_task`.
**Tests.** 115 trigger-bearing start nodes:
- 47 bind an object the same file declares, and 36 bind a computed name;
- 30 bind a name the file does not declare: unit tests that build engine
stacks with no package, plus the deliberate CLI advisory pin
`validate-flow-trigger-readiness.test.ts` (`other_package_object` →
`billing_invoice`), which is unchanged;
- 1 binds `sys_user`, and 1 has no object.
The fixtures that write a flow into a package through the real door are
the ones the consumer sweep surfaced (below).
**objectui** (read-only, at `32b492b67`): zero record-triggered or
time-relative flow metadata outside tests.
`apps/console/src/preview-samples.ts` holds one schedule-start preview
sample, and nothing in it is written to a server.
**Verdict: no real writer**, so step 2's first branch applies and the
code was written. Deployed rows were not measured.
## Premises
- **P1. Holds** at `a43405d596`.
`validate-flow-trigger-readiness.ts:446` / `:448` (record trigger) and
`:498` / `:500` (time-relative): `severity: 'warning'`, keyed on
`!objectNames.has(objectName) && !objectName.startsWith('sys_')`, not on
the closure.
- **P2. Holds: a severity choice, not a new lookup.**
- The flow rule is its own registry entry (`authoring-rules.ts`,
`validateFlowTriggerReadiness`, `surfaces: CLI_AND_RUNTIME`,
`runtimeTypes: ['flow']`). It is not a reference-integrity-suite member,
and its `run` callback did not read the rule context.
- `buildRuntimeWriteSnapshotSet` narrows `objects` for every write type,
flow included (`runtime-gate.ts:721`, `narrowObjectsToPackageClosure`).
`runRuntimeAuthoringRules` puts `packageScope` on the rule context
exactly when `packageScopeInForce` holds (`:1121`). So under a package
write, `objectNames` already IS the closure.
- The change hands `ctx.packageScope` to the rule and picks `error` when
it is stated. `runtime-gate.ts` is unchanged.
`reference-integrity-suite.ts` is touched only for one comment, after
#24789 landed.
- **P3. Holds: an advisory at the runtime door, through the real
`saveMetaItem`.**
- Before, on `origin/main` `a43405d596` with lint built from it,
`protocol.package-closure-gate.test.ts` and
`protocol-publish-drafts-closure.test.ts` were 15/15 green. That
included "reports a reference into a package the written one never
declared": a flow written into `com.acme.crm` triggering on a
`com.other.analytics` object answered `success: true` with `advisories =
['flow-trigger-unknown-object']`. The same body with no package drew
nothing, because the stranger's object is in the whole collection.
- With the rule changed and those pins untouched: 3 red. Two read
`flow/crm_on_stranger failed author-time validation: 1 issue —
flows[0].nodes[0].config.objectName [flow-trigger-unknown-object]`, and
the third is the batch case in
`protocol-publish-drafts-closure.test.ts`.
## What changes
- **`packages/lint/src/validate-flow-trigger-readiness.ts`.**
`validateFlowTriggerReadiness(stack, options = {})` takes an optional `{
packageScope }`. Both unknown-object sites go through one helper. With
no closure, the finding is the old advisory (same keys, same order, same
strings). Under a closure, the same id is `error`, with a message that
names the package and its closure, and a hint that names the remedy
(declare the dependency in the manifest `dependencies`, or set the named
key to an in-closure object). The header's Severity section says why the
hedge is gone under a closure.
- **`packages/lint/src/authoring-rules.ts`.** The flow entry runs
`validateFlowTriggerReadiness(stack, ctx.packageScope ? { packageScope:
ctx.packageScope } : {})`. The `AuthoringRuleContext.packageScope` doc
names its two readers.
- **`packages/lint/src/rule-explanations.ts`.** `os explain
flow-trigger-unknown-object` said "at the runtime publish gate it is a
save advisory … never a refusal", which this change makes false. It now
gives the closure case, its remedy, and the draft and stored-row rules.
- **Comments made false by this change:**
`validate-list-view-field-refs.ts` (two lines) and
`reference-integrity-suite.ts` (one line, after #24789 landed), plus one
sentence in `content/docs/deployment/validating-metadata.mdx`.
- **`.changeset/24669-flow-trigger-out-of-closure.md`:**
`@objectstack/lint` `minor`, a `!` title, `Clause-②: yes (narrowing)`,
FROM / TO / Fix, the stored-row sentence, and the ADR-0087 disposition
`not-required (no-migration-prescription)`. `.changeset/pre.json` reads
`mode: exit`, `tag: next`, so the next version pass cuts 18.0.0 and this
ships in it. The changeset says so.
## Pins
- **Lint** (`runtime-gate.package-closure.test.ts`, through
`runRuntimeAuthoringRules`). The new block, for both positions:
- an undeclared package's object is refused at the key, with a hint
naming the package and `dependencies`;
- an object that exists nowhere is refused;
- admitted: the own package, a declared dependency, `isSystem` objects
(one of them stranger-owned), a carried `sys_` object, a `sys_` name the
snapshot does not carry, and a provenance-less overlay row;
- with no package, or an empty package id: the stranger's object is
clean, and the ghost is advised. The door's severity, message and hint
equal the CLI's byte for byte;
- the CLI reading is unchanged.
The three existing flow pins in that file now assert severity: "a REAL
finding survives narrowing" (same rule and path, advisory on the whole
collection, refused under the closure), the undeclared case, and the
stored-sibling case.
- **Protocol**
(`packages/metadata-protocol/src/protocol.package-closure-gate.test.ts`,
through the real `saveMetaItem` / `publishMetaItem`):
- refused `422 INVALID_METADATA`, asserting the ADR-0112 `code` +
`status`, the issue's `severity` / `path` and the hint, with nothing
stored. This holds for `config.objectName`,
`config.timeRelative.object`, and an object that exists nowhere;
- admitted at both positions: the own package, a declared dependency, an
`isSystem` platform object and `sys_user`;
- the draft save of the refused body succeeds and stores no active row,
and its promotion is refused;
- with no package, the ghost keeps its `warning` advisory at both
positions;
- the uninstalled-package case narrows nothing and resolves.
## Fixtures the consumer sweep found (outside `packages/lint` /
`packages/spec`), for the seat's #6367 addendum
All four are test files only. There is no `protocol.ts` or `objectql`
source change, and no assertion was weakened.
1.
`packages/metadata-protocol/src/protocol.package-closure-gate.test.ts`:
the protocol-layer pin block above. Its advisory pins are re-pinned to
the refusal.
2.
`packages/metadata-protocol/src/protocol-publish-drafts-closure.test.ts`:
"STILL reports a flow bound to an object that is in neither the batch
nor the registry" becomes "STILL refuses". The batch fails, the
ghost-triggered flow is `INVALID_METADATA` with the issue at `error`,
and the healthy sibling is `BATCH_ABORTED`.
3.
`packages/metadata-protocol/src/protocol.publish-item-rebind-announce.test.ts`:
the fixture is re-trued. Its stub registry now lists the flows' trigger
object `ticket` under the package `app.ops`. The batch CONTROL case had
gone red because `ticket` was outside the closure. Assertions are
unchanged.
4.
`packages/objectql/src/protocol-duplicate-package-unprefixed-object.test.ts`:
"a name already claimed is refused up front" is re-pinned to three
refusals. With the `ticket` copy refused, the copied `dupsrc_on_ticket`
keeps the source's `ticket` as its trigger object, which is outside the
copy package's closure, so the flow door refuses it. This is the same
shape as #24630's view re-pin (the seat's answer A, `6277565064`). The
changeset's door list names `duplicatePackage`.
## Ablation (G5): run once, recorded here, no permanent file
The fix was committed first. `scripts/ablation-replace.mjs --hold`,
armed with a `trap` restore, replaced the closure branch's `severity:
'error',` with `severity: 'warning',` plus a dist-visible marker.
- Mutate leg: anchor 1 → 0, blob `48415eb218` → `255f3f2863`.
`ablation-dist-preflight.mjs @objectstack/lint ABLATION_24669` found the
marker in 4 built files.
- The lint pins went red: 8 of 36. So did the protocol pins: 6 of 20,
every refusal case (objectName, timeRelative, ghost, draft promotion,
uninstalled control, batch). The direction was red, as expected.
- Restore leg: blob == HEAD, `git diff HEAD` empty, whole tree clean.
After a rebuild, `--absent` found the marker in none of the 20 built
files. Both files went green again: 36/36 and 20/20.
## Verification
- **Consumer sweep** (`--maxWorkers=2`, under `os-verify-lock.sh`), on
`aa20228392`. Results: `lint` 6953/6953; `service-automation` 2605;
`runtime` 7587 (19 skipped); `rest` 6404 (344 skipped). The two
first-run reds were fixtures 3 and 4 above, re-run green after
re-truing: `metadata-protocol` was 1 red of 20834, and `objectql` 1 red
of 8495. `cli --project unit` was 4612 passed, with 2 suites refused for
a missing prerequisite (`packages/cli is not built`), not a measurement.
Both re-ran green after a cli build, below.
- **After merging `origin/main` (#24789 landed), on `009a2b2b09`.**
Results: `lint` full suite and `typecheck` green; `metadata-protocol`
full suite green; the objectql fixture green; the two cli suites green.
`metadata-protocol` `typecheck` was green on the pre-merge head.
- **Gates, on the final head `3d4226ca4f`.** `node
scripts/pm/dispatch-gates.mjs --commands` (no paths) derives 96
families. All 96 ran with their exit codes recorded, and `--ran`
reconciles them: "96 derived famil(ies) accounted for — 96 run, 0
NOT-MEASURED (a DERIVED zero …)". 95 exit 0. One is red **by design**:
`check-empty-changeset --base origin/main` (exit 1, the next section).
On the `009a2b2b09` pass, `check:skill-examples` and
`check:dual-build-cjs-loads` first exited 3 (missing prerequisite: dist
absent). They re-ran green after the missing packages were built, and
both are exit 0 in the final-head pass.
- **`test:repo`:** not run locally. Nothing in its project changed, and
CI's `Test Core` runs it.
## A pending release note corrected: please confirm (`Check Changeset`
stays red by design)
`.changeset/24563-list-view-out-of-closure.md` (#24563's, unreleased)
said under **Unchanged**: "The flow door: `flow-trigger-unknown-object`
stays an advisory." This PR makes that sentence false, so the line now
reads: "The flow door: under the same closure,
`flow-trigger-unknown-object` refuses an out-of-closure trigger object
too (#24669, see that changeset)." This is check-empty-changeset's
DELIBERATE CORRECTION class. Per ruling D, the gate stays red, and the
correction needs written confirmation here. `Check Changeset` is not a
required context.
## File surface beyond the claim
The claim named the rule file, the closure plumbing, the lint pins, one
changeset, and the riders. I also touched the following, each because
this change made a sentence in it false:
- `packages/lint/src/rule-explanations.ts`: the `os explain` text;
- `content/docs/deployment/validating-metadata.mdx`: one sentence. Its
lines are disjoint from #24789's hunk, and the merge was clean;
- `packages/lint/src/validate-list-view-field-refs.ts`: two comment
lines;
- `packages/lint/src/reference-integrity-suite.ts`: one comment line,
after #24789 landed;
- `.changeset/24563-list-view-out-of-closure.md`: one line, after #24789
landed.
Before touching these, I read the file lists of all open PRs. Only
#24789 overlapped, and it has since landed.
## Acceptance notes
- **Not measured: Studio's flow designer object picker** (objectui). I
did not check whether it offers objects outside the package's closure
when you author inside a package. If it does, an author can now pick an
object the publish refuses. That refusal is loud and names the fix, and
the view door has the same shape. This is an observation only; no
carrier is named.
- **Deployed stored rows were not measured.** A stored flow with an
out-of-closure trigger keeps loading and firing. It is refused only at
its next publish, and the refusal names the fix.
---
_Generated by [Claude
Code](https://claude.ai/code/session_019oFY6Hi35J7atvgVFnmoaK)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent 41646a5 commit 096ac40
13 files changed
Lines changed: 524 additions & 82 deletions
File tree
- .changeset
- content/docs/deployment
- packages
- lint/src
- metadata-protocol/src
- objectql/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
22 | 22 | | |
23 | 23 | | |
24 | 24 | | |
25 | | - | |
| 25 | + | |
26 | 26 | | |
27 | 27 | | |
28 | 28 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
710 | 710 | | |
711 | 711 | | |
712 | 712 | | |
713 | | - | |
| 713 | + | |
| 714 | + | |
| 715 | + | |
714 | 716 | | |
715 | 717 | | |
716 | 718 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
384 | 384 | | |
385 | 385 | | |
386 | 386 | | |
387 | | - | |
| 387 | + | |
388 | 388 | | |
389 | | - | |
| 389 | + | |
| 390 | + | |
390 | 391 | | |
391 | 392 | | |
392 | 393 | | |
| |||
1359 | 1360 | | |
1360 | 1361 | | |
1361 | 1362 | | |
1362 | | - | |
| 1363 | + | |
| 1364 | + | |
| 1365 | + | |
| 1366 | + | |
| 1367 | + | |
| 1368 | + | |
1363 | 1369 | | |
1364 | 1370 | | |
1365 | 1371 | | |
| |||
1379 | 1385 | | |
1380 | 1386 | | |
1381 | 1387 | | |
1382 | | - | |
| 1388 | + | |
| 1389 | + | |
| 1390 | + | |
1383 | 1391 | | |
1384 | 1392 | | |
1385 | | - | |
| 1393 | + | |
| 1394 | + | |
1386 | 1395 | | |
1387 | 1396 | | |
1388 | 1397 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
313 | 313 | | |
314 | 314 | | |
315 | 315 | | |
316 | | - | |
317 | | - | |
| 316 | + | |
| 317 | + | |
| 318 | + | |
318 | 319 | | |
319 | 320 | | |
320 | 321 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1755 | 1755 | | |
1756 | 1756 | | |
1757 | 1757 | | |
1758 | | - | |
| 1758 | + | |
| 1759 | + | |
| 1760 | + | |
1759 | 1761 | | |
1760 | 1762 | | |
1761 | 1763 | | |
1762 | | - | |
| 1764 | + | |
1763 | 1765 | | |
1764 | 1766 | | |
1765 | 1767 | | |
| |||
1769 | 1771 | | |
1770 | 1772 | | |
1771 | 1773 | | |
1772 | | - | |
1773 | | - | |
| 1774 | + | |
| 1775 | + | |
| 1776 | + | |
| 1777 | + | |
| 1778 | + | |
| 1779 | + | |
| 1780 | + | |
| 1781 | + | |
| 1782 | + | |
| 1783 | + | |
1774 | 1784 | | |
1775 | 1785 | | |
1776 | 1786 | | |
| |||
0 commit comments