Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .changeset/24563-list-view-out-of-closure.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
---
'@objectstack/lint': minor
---
Expand All @@ -22,7 +22,7 @@

- 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.
- 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).
- The flow door: `flow-trigger-unknown-object` stays an advisory.
- The flow door: under the same closure, `flow-trigger-unknown-object` refuses an out-of-closure trigger object too (#24669, see that changeset).
- `os validate`, `os build` and `os lint`: no closure is stated, so the rule skips an unresolvable binding as before.
- `OS_ALLOW_UNLINTED_METADATA_WRITES=1` still turns a refusal into a logged write.
- 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.
Expand Down
34 changes: 34 additions & 0 deletions .changeset/24669-flow-trigger-out-of-closure.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
---
'@objectstack/lint': minor
---

fix(lint)!: the save door refuses a flow written into a package whose trigger object is outside that package's closure (#24669)

Clause-②: yes (narrowing)

**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.

**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.

**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).

- 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.
- 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.
- 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.

**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.

**Unchanged.**

- 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.
- 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.
- `os validate`, `os build` and `os lint`: no closure is stated, so the rule's verdict, severity and wording are unchanged.
- `OS_ALLOW_UNLINTED_METADATA_WRITES=1` still turns a refusal into a logged write.
- 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.

**API.**

- `validateFlowTriggerReadiness(stack, options?)` accepts an optional `{ packageScope }` (`{ packageId, dependencies }`). Stated, an unknown trigger object is an `error`; omitted, the rule answers as before.
- The runtime publish gate hands `AuthoringRuleContext.packageScope` to this rule, which now reads it beside `validateListViewFieldRefs`.

<!-- 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). -->
4 changes: 3 additions & 1 deletion content/docs/deployment/validating-metadata.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -710,7 +710,9 @@ the written item's **package closure** — the written package, its declared
dependencies, and unconditionally both platform/system objects and rows carrying
no package provenance (#9612); a provenance it cannot read narrows nothing at all.
That changes what a rule can **resolve**, never which rules run, and nothing at
this door walks `packages[]`. One rule is told the closure is in force: a list
this door walks `packages[]`. Two rules are told the closure is in force. A flow
whose trigger object is outside it is refused with `flow-trigger-unknown-object`
instead of advised (#24669), and a list
view bound to an object outside it is refused with
`list-view-object-outside-closure` instead of having its field references
skipped (#24563). With no closure in force — a write that names no package —
Expand Down
19 changes: 14 additions & 5 deletions packages/lint/src/authoring-rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -384,9 +384,10 @@ export interface AuthoringRuleContext {
* rule tell "outside what this package declared it may reference" from
* "not in this stack": an object missing from a narrowed snapshot is the
* first, one missing from a whole one may come from another package. Read
* by `validateListViewFieldRefs` only (through the reference-integrity
* by two rules: `validateListViewFieldRefs` (through the reference-integrity
* suite), which refuses a list view bound outside the closure instead of
* skipping it.
* skipping it, and [#24669] `validateFlowTriggerReadiness`, which refuses a
* flow whose trigger object is outside it instead of advising.
*/
packageScope?: { readonly packageId: string; readonly dependencies: readonly string[] };
}
Expand Down Expand Up @@ -1352,7 +1353,12 @@ export const AUTHORING_RULES: readonly AuthoringRule[] = [
// the flow fires. `flow-trigger-unknown-object` deliberately stayed `warning`
// (the object may come from another installed package — a hedge this rule
// cannot decide), as did `flow-draft-status-ambiguous` (draft flows DO fire;
// that one is ambiguity of intent, not a dead flow).
// that one is ambiguity of intent, not a dead flow). [#24669] The hedge is
// gone where the runtime publish gate states a package closure: the snapshot
// then IS that closure, so an unknown trigger object is one the package never
// declared it may reference, and the id is an `error` there — the dataset and
// view doors' verdict for the same reference. Without a stated closure (the
// three CLI commands, a write that names no package) it is still `warning`.
//
// Commit ecdfc9411 added a sixth id, `flow-schedule-organization-missing`, at
// `warning`; #17396 RETIRED it. The criterion above is what retired it: this
Expand All @@ -1372,10 +1378,13 @@ export const AUTHORING_RULES: readonly AuthoringRule[] = [
// stayed `warning` keep being logged as advisories. The gate judges a
// snapshot whose `flows` holds only the written item and subtracts the
// baseline's findings, so this refuses the dead flow's own publish — never
// another flow's save on account of a stored one.
// another flow's save on account of a stored one. [#24669] The gate's
// stated package closure is handed on, so an out-of-closure trigger object
// refuses the publish too; the CLI never states one.
surfaces: CLI_AND_RUNTIME,
runtimeTypes: ['flow'],
run: (stack) => validateFlowTriggerReadiness(stack),
run: (stack, ctx) =>
validateFlowTriggerReadiness(stack, ctx.packageScope ? { packageScope: ctx.packageScope } : {}),
},
// #20553 — `flow-api-trigger-secret-missing`, split out of the entry above as
// its own exported rule. Same family, same `error`, all three commands.
Expand Down
5 changes: 3 additions & 2 deletions packages/lint/src/reference-integrity-suite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -313,8 +313,9 @@ export const REFERENCE_INTEGRITY_RULES: readonly ReferenceIntegrityRule[] = [
// reference, or nowhere at all — and the member's skip 1 used to read that as
// "another package's object" and judge nothing. It now refuses the binding
// instead (`list-view-object-outside-closure`, `error`: the verdict the
// dataset door gives an out-of-closure base object under the same closure;
// the flow door's trigger-object advisory is a separate question). The
// dataset door gives an out-of-closure base object under the same closure,
// and [#24669] the one the flow door gives an out-of-closure trigger object,
// from its own rule outside this suite). The
// missing-collection argument above is unchanged: the binding is judged
// against `stack.objects`, the collection every per-write snapshot carries,
// and the gate states the scope only when it narrowed that collection.
Expand Down
18 changes: 14 additions & 4 deletions packages/lint/src/rule-explanations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1755,11 +1755,13 @@ const FLOW_API_TRIGGER_SECRET_MISSING_EXPLANATION: RuleExplanation = {
};

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

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

Expand Down
Loading
Loading