Skip to content

Commit 096ac40

Browse files
fix(lint)!: the save door refuses a flow written into a package whose trigger object is outside that package's closure (#24669) (#24839)
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

‎.changeset/24563-list-view-out-of-closure.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ Clause-②: yes (narrowing)
2222

2323
- A list view bound to an object inside the closure (the package's own, a declared dependency's, a platform/system object, an overlay row): same field verdicts, same refusals.
2424
- A write that names no package (an overlay write) is judged against the whole collection, exactly as before. An object that exists nowhere gets no closure verdict there; #24668 refuses it as `list-view-object-unknown` (see that changeset).
25-
- The flow door: `flow-trigger-unknown-object` stays an advisory.
25+
- The flow door: under the same closure, `flow-trigger-unknown-object` refuses an out-of-closure trigger object too (#24669, see that changeset).
2626
- `os validate`, `os build` and `os lint`: no closure is stated, so the rule skips an unresolvable binding as before.
2727
- `OS_ALLOW_UNLINTED_METADATA_WRITES=1` still turns a refusal into a logged write.
2828
- Measured before crossing: the 11 views shipped in this repository carry 39 list-view bindings, in `examples/app-crm`, `app-todo`, `app-showcase` and `app-multi-package`, the `create-objectstack` blank template, and `qa/downstream-contract`. None is outside its package's closure, and the door's `view` rule set refuses none of them, with the package's closure or without it. None of the 97 platform built-in list views retargets `data.object`. Deployed rows were not measured.
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
---
2+
'@objectstack/lint': minor
3+
---
4+
5+
fix(lint)!: the save door refuses a flow written into a package whose trigger object is outside that package's closure (#24669)
6+
7+
Clause-②: yes (narrowing)
8+
9+
**Why.** The runtime save door judges a write into a package against that package's closure (ruling #9612): the package's own objects, the objects of the packages its manifest declares in `dependencies`, platform/system objects, and provenance-less overlay rows. Every other object is dropped from the snapshot before any rule runs. The dataset door already refused a reference outside that closure (`object-reference-unknown`), and the view door refuses one too (`list-view-object-outside-closure`). The flow door only advised: `flow-trigger-unknown-object` stayed a `warning` there, with the hint "ignore this if another installed package declares it". Under a closure that hedge is false, because the snapshot is the closure: an object outside it is one the package never declared it may reference. So the same mistake was refused at two doors and stored with an advisory at the third.
10+
11+
**What is refused.** A flow written into a package whose record trigger (`config.objectName` on a `record-*` start node) or time-relative sweep (`config.timeRelative.object`) names an object outside the package's closure is refused with one `flow-trigger-unknown-object` issue at `error`, located at that key. The hint names the remedy and the package. An object that exists nowhere is refused the same way, because the closure is all the snapshot sees. A `sys_`-prefixed name is still not judged, exactly as without a closure.
12+
13+
**BREAKING — what moves for consumers.** This is an accept-set narrowing on the metadata save door, shipped as `minor` under the launch-window convention for accept-set narrowings (`.changeset/pre.json` is in `exit` mode, so the next version pass cuts 18.0.0 and this change ships in it).
14+
15+
- FROM: a publish-mode write of a flow into a package, triggered on or sweeping an object outside the package's closure, answered `200` and was stored, with a `flow-trigger-unknown-object` save advisory. This covers `PUT /api/v1/meta/flow/:name?package=…` (and `saveMetaItem` in publish mode), the promotion of a draft (`POST /api/v1/meta/flow/:name/publish`, `publishMetaItem`), a package draft publish (`publishPackageDrafts`), and a package copy (`duplicatePackage`), which writes its copies through the same door.
16+
- TO: `saveMetaItem` in publish mode, the draft promotion and `publishPackageDrafts` answer `422 INVALID_METADATA`, with the `flow-trigger-unknown-object` issue at `error` at the trigger key, and nothing is stored. A package draft publish fails the item, and the batch with it. In `duplicatePackage`, a copied flow whose trigger object stays outside the copy package's closure is refused with the same rule and listed in its `failed[]`. One way that happens is when that object's own copy was refused.
17+
- Fix: declare a dependency on the package that defines the trigger object in the `dependencies` of the writing package's manifest, or trigger the flow on an object the package may reach (its own, a declared dependency's, or a platform object). Then save again. Saving the flow as a draft (`mode: 'draft'`) is still allowed, because drafts are never gated; publishing that draft is judged.
18+
19+
**The next save of a stored flow whose trigger object is outside its package's closure is refused with the fix; stored flows keep loading and firing until then.** The door judges writes only. A stored row is not re-judged at boot.
20+
21+
**Unchanged.**
22+
23+
- A flow triggered on an object inside the closure (the package's own, a declared dependency's, a platform/system object, an overlay row): no finding, as before.
24+
- A write that names no package (an overlay write), a write naming a package the registry cannot produce, and the `sys_metadata` sentinel state no closure: an object the whole collection does not carry is still a `flow-trigger-unknown-object` save advisory, byte-identical to before.
25+
- `os validate`, `os build` and `os lint`: no closure is stated, so the rule's verdict, severity and wording are unchanged.
26+
- `OS_ALLOW_UNLINTED_METADATA_WRITES=1` still turns a refusal into a logged write.
27+
- Measured before the change: every record-triggered or time-relative start node shipped in this repository (21, all in `examples/app-showcase` and `examples/app-todo`; no flow in `packages/apps`, `packages/platform-objects` or the `create-objectstack` template) triggers on an object its own stack defines, and objectui ships no such flow. Deployed rows were not measured.
28+
29+
**API.**
30+
31+
- `validateFlowTriggerReadiness(stack, options?)` accepts an optional `{ packageScope }` (`{ packageId, dependencies }`). Stated, an unknown trigger object is an `error`; omitted, the rule answers as before.
32+
- The runtime publish gate hands `AuthoringRuleContext.packageScope` to this rule, which now reads it beside `validateListViewFieldRefs`.
33+
34+
<!-- adr-0087: not-required (no-migration-prescription) a refusal at the metadata save door of a flow written into a package whose trigger object is outside that package's dependency closure: no authorable key, spelling, export or stored shape moves, and no stored row is read, rewritten or converted. A stored flow with such a trigger keeps loading and firing until it is next saved, and the repair is the author's choice of which package to declare as a dependency (or which object to trigger on), which no ledger entry can derive. The other categories are closed on facts: the package publishes (not unpublished); no ADR-0087 id covers this door (not already-registered); and the change is a door verdict, not a declaration or an export (not runtime-interface-only, type-surface-only or runtime-export-retired). -->

‎content/docs/deployment/validating-metadata.mdx‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -710,7 +710,9 @@ the written item's **package closure** — the written package, its declared
710710
dependencies, and unconditionally both platform/system objects and rows carrying
711711
no package provenance (#9612); a provenance it cannot read narrows nothing at all.
712712
That changes what a rule can **resolve**, never which rules run, and nothing at
713-
this door walks `packages[]`. One rule is told the closure is in force: a list
713+
this door walks `packages[]`. Two rules are told the closure is in force. A flow
714+
whose trigger object is outside it is refused with `flow-trigger-unknown-object`
715+
instead of advised (#24669), and a list
714716
view bound to an object outside it is refused with
715717
`list-view-object-outside-closure` instead of having its field references
716718
skipped (#24563). With no closure in force — a write that names no package —

‎packages/lint/src/authoring-rules.ts‎

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -384,9 +384,10 @@ export interface AuthoringRuleContext {
384384
* rule tell "outside what this package declared it may reference" from
385385
* "not in this stack": an object missing from a narrowed snapshot is the
386386
* first, one missing from a whole one may come from another package. Read
387-
* by `validateListViewFieldRefs` only (through the reference-integrity
387+
* by two rules: `validateListViewFieldRefs` (through the reference-integrity
388388
* suite), which refuses a list view bound outside the closure instead of
389-
* skipping it.
389+
* skipping it, and [#24669] `validateFlowTriggerReadiness`, which refuses a
390+
* flow whose trigger object is outside it instead of advising.
390391
*/
391392
packageScope?: { readonly packageId: string; readonly dependencies: readonly string[] };
392393
}
@@ -1359,7 +1360,12 @@ export const AUTHORING_RULES: readonly AuthoringRule[] = [
13591360
// the flow fires. `flow-trigger-unknown-object` deliberately stayed `warning`
13601361
// (the object may come from another installed package — a hedge this rule
13611362
// cannot decide), as did `flow-draft-status-ambiguous` (draft flows DO fire;
1362-
// that one is ambiguity of intent, not a dead flow).
1363+
// that one is ambiguity of intent, not a dead flow). [#24669] The hedge is
1364+
// gone where the runtime publish gate states a package closure: the snapshot
1365+
// then IS that closure, so an unknown trigger object is one the package never
1366+
// declared it may reference, and the id is an `error` there — the dataset and
1367+
// view doors' verdict for the same reference. Without a stated closure (the
1368+
// three CLI commands, a write that names no package) it is still `warning`.
13631369
//
13641370
// Commit ecdfc9411 added a sixth id, `flow-schedule-organization-missing`, at
13651371
// `warning`; #17396 RETIRED it. The criterion above is what retired it: this
@@ -1379,10 +1385,13 @@ export const AUTHORING_RULES: readonly AuthoringRule[] = [
13791385
// stayed `warning` keep being logged as advisories. The gate judges a
13801386
// snapshot whose `flows` holds only the written item and subtracts the
13811387
// baseline's findings, so this refuses the dead flow's own publish — never
1382-
// another flow's save on account of a stored one.
1388+
// another flow's save on account of a stored one. [#24669] The gate's
1389+
// stated package closure is handed on, so an out-of-closure trigger object
1390+
// refuses the publish too; the CLI never states one.
13831391
surfaces: CLI_AND_RUNTIME,
13841392
runtimeTypes: ['flow'],
1385-
run: (stack) => validateFlowTriggerReadiness(stack),
1393+
run: (stack, ctx) =>
1394+
validateFlowTriggerReadiness(stack, ctx.packageScope ? { packageScope: ctx.packageScope } : {}),
13861395
},
13871396
// #20553 — `flow-api-trigger-secret-missing`, split out of the entry above as
13881397
// its own exported rule. Same family, same `error`, all three commands.

‎packages/lint/src/reference-integrity-suite.ts‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -313,8 +313,9 @@ export const REFERENCE_INTEGRITY_RULES: readonly ReferenceIntegrityRule[] = [
313313
// reference, or nowhere at all — and the member's skip 1 used to read that as
314314
// "another package's object" and judge nothing. It now refuses the binding
315315
// instead (`list-view-object-outside-closure`, `error`: the verdict the
316-
// dataset door gives an out-of-closure base object under the same closure;
317-
// the flow door's trigger-object advisory is a separate question). The
316+
// dataset door gives an out-of-closure base object under the same closure,
317+
// and [#24669] the one the flow door gives an out-of-closure trigger object,
318+
// from its own rule outside this suite). The
318319
// missing-collection argument above is unchanged: the binding is judged
319320
// against `stack.objects`, the collection every per-write snapshot carries,
320321
// and the gate states the scope only when it narrowed that collection.

‎packages/lint/src/rule-explanations.ts‎

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1755,11 +1755,13 @@ const FLOW_API_TRIGGER_SECRET_MISSING_EXPLANATION: RuleExplanation = {
17551755
};
17561756

17571757
// `flow-trigger-unknown-object` lives in the same rule file but is not one of
1758-
// the five: its verdict depends on packages a build cannot see, so it warns.
1758+
// the five: its verdict depends on packages a build cannot see, so it warns —
1759+
// except under a package closure the runtime publish gate states, where the
1760+
// snapshot is that closure and the id refuses.
17591761

17601762
const FLOW_TRIGGER_UNKNOWN_OBJECT_EXPLANATION: RuleExplanation = {
17611763
rule: 'flow-trigger-unknown-object',
1762-
covers: 'why an unknown trigger object only warns',
1764+
covers: 'why an unknown trigger object warns, and when it refuses',
17631765
paragraphs: [
17641766
'A record-triggered flow binds an ObjectQL hook filtered to the exact name in its start node\'s ' +
17651767
'`config.objectName`, and a time-relative sweep queries the exact name in `config.timeRelative.object`. ' +
@@ -1769,8 +1771,16 @@ const FLOW_TRIGGER_UNKNOWN_OBJECT_EXPLANATION: RuleExplanation = {
17691771
'another installed package, which a build of this stack cannot see, so the finding is a warning and ' +
17701772
'can be ignored when such a package declares the object; a `sys_` name is a platform object and is ' +
17711773
'not judged. Of the findings that say a flow never fires, it is the only one that stays advisory: ' +
1772-
'the others are settled by contracts this repository ships. At the runtime publish gate it is a save advisory, ' +
1773-
'judged against the live object registry, never a refusal.',
1774+
'the others are settled by contracts this repository ships. At the runtime publish gate a write that ' +
1775+
'names no package gets it as a save advisory, judged against the live object registry.',
1776+
'A write into a package is judged against that package\'s closure instead: its own objects, the objects ' +
1777+
'of the packages its manifest declares in `dependencies`, and platform objects. An object outside it ' +
1778+
'is one the package never declared it may reference, and nothing it installs puts the object in ' +
1779+
'reach until its manifest declares the dependency, so there the finding is an error and the publish ' +
1780+
'is refused, the verdict the dataset and view doors give the same reference. Declare a dependency on ' +
1781+
'the package that defines the object, or trigger the flow on an object inside the closure. A `sys_` ' +
1782+
'name is not judged there either, a draft save is not gated, and a stored flow is judged at its next ' +
1783+
'publish.',
17741784
],
17751785
};
17761786

0 commit comments

Comments
 (0)