Repository navigation
fix(lint)!: the save door refuses a flow written into a package whose trigger object is outside that package's closure (#24669) - #24839
Conversation
…me save door Under a package closure the runtime publish gate states, the flow rule's snapshot is that closure, so flow-trigger-unknown-object is an error there (both the config.objectName and config.timeRelative.object positions). Without a stated closure (the CLI commands, a write that names no package) it stays the advisory it was. WIP: pins and changeset follow. Claude-Session: https://claude.ai/code/session_019oFY6Hi35J7atvgVFnmoaK Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019oFY6Hi35J7atvgVFnmoaK Co-authored-by: Claude <noreply@anthropic.com>
…the real saveMetaItem Claude-Session: https://claude.ai/code/session_019oFY6Hi35J7atvgVFnmoaK Co-authored-by: Claude <noreply@anthropic.com>
…r's closure refusal Claude-Session: https://claude.ai/code/session_019oFY6Hi35J7atvgVFnmoaK Co-authored-by: Claude <noreply@anthropic.com>
…r object into its package Claude-Session: https://claude.ai/code/session_019oFY6Hi35J7atvgVFnmoaK Co-authored-by: Claude <noreply@anthropic.com>
…ow door's refusal Claude-Session: https://claude.ai/code/session_019oFY6Hi35J7atvgVFnmoaK Co-authored-by: Claude <noreply@anthropic.com>
…ow-trigger-out-of-closure
…oor's refusal Also corrects the reference-integrity suite comment that called the flow door's trigger-object verdict advisory. Claude-Session: https://claude.ai/code/session_019oFY6Hi35J7atvgVFnmoaK Co-authored-by: Claude <noreply@anthropic.com>
… advisory Claude-Session: https://claude.ai/code/session_019oFY6Hi35J7atvgVFnmoaK Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 3 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 3b03a61f85f96c71cb262f8cdcc73d49bbce3a7b && git checkout 3b03a61f85f96c71cb262f8cdcc73d49bbce3a7b
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 1366740c6c80be43b18b6b72a68736032245e3e9 3d4226ca4f62b6bead9bcfe71a832950f8103092 && git checkout -B drift-repro 1366740c6c80be43b18b6b72a68736032245e3e9 && git merge --no-ff 3d4226ca4f62b6bead9bcfe71a832950f8103092
node scripts/docs-audit/affected-docs.mjs --json 1366740c6c80be43b18b6b72a68736032245e3e9
|
|
Seat confirmation · deliberate correction of a pending release note · This PR modifies
Confirmed: option A. The corrected line stays. Restoring the base line would publish a false sentence beside its correction.
⛔ No Generated by Claude Code |
Contract reviewServed-tier: Read on the narrowing face, adversarially: the net diff against the merge base ① Derived judgmentsThe census gate was honoured; I found no writer it missed. Triage The narrowing is exactly the ruled one. No CLI caller can pass The doors and the closure.
Same id with closure-keyed severity (dev deviation c) is sound. Precedent: the dataset door's Message and hint under a closure are true, and
The re-pins and re-trues outside the claim are each the ruled consequence, nothing loosened. (1) The file-surface breach (dev deviation a): each edit minimal and true. Ablation (8/36 lint, 6/20 protocol) is consistent with the pins' control flow. With the closure branch at Check-runs on this head, read at 2026-10-20T09:51Z: success: Auto Label, Build Core, Build Docs, Check Documentation Links, Check PR Size, Dogfood Regression Gate, Flag docs affected by code changes, Governed Surface Queue Guard, Lint & Repo Gates, Lint · CI release & engine gates, Lint · ESLint, Lint · PM dispatch-gates, Lint · docs & code guards, Lint · repo & tooling gates, Lint · scoped · query-options erasure, Lint · scoped · slot lookup & corpus, Lint · scoped · tooling self-tests, No other open PR may claim the same issue, No other open PR may claim the same single-writer path, Part-of PR must not also close its card, Spec property liveness, Test Core (1/6), The card this PR closes must claim this branch, Type Check · consumer gates, Type Check · debt ledger, Type Check · source gates, Type Check · workspace, TypeScript Type Check, filter. in_progress: Test Core (2/6) through (6/6). skipped: 18.x upgrade harness, Console Pin Gate, Dogfood Regression Gate shards, Dogfood Verify CLI, Packed-tarball smoke, Temporal Conformance. failure: Check Changeset, and the job log names the cause exactly: ② Semver levelWhat The deliberate correction of ③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
|
Contract review adopted ·
Generated by Claude Code |
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, oneflow-trigger-unknown-objectissue aterror, at either position the rule reads (config.objectNameon arecord-*start node, orconfig.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). Asys_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_metadatasentinel, 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/.mtsfile in this repository ata43405d596(this branch's base) that contains atype: 'start'literal: 379 files and 930 start nodes. I read each start node'sconfig.objectName,config.triggerTypeandconfig.timeRelative.object. 136 start nodes carry a record trigger (arecord-*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, andshowcase_field_zoo,showcase_announcement,showcase_invoiceandtodo_task×1 each.a43405d596)examples/app-showcase/src/automation/flows/approver-bindings.flow.ts:63showcase_approver_bindingsshowcase_field_zoo(objectName)examples/app-showcase/src/automation/flows/dynamic-approval.flow.ts:22showcase_dynamic_approvalshowcase_announcement(objectName)examples/app-showcase/src/automation/flows/index.ts:33showcase_task_completedshowcase_task(objectName)examples/app-showcase/src/automation/flows/index.ts:161showcase_task_assigned_notifyshowcase_task(objectName)examples/app-showcase/src/automation/flows/index.ts:216showcase_budget_approvalshowcase_project(objectName)examples/app-showcase/src/automation/flows/index.ts:333showcase_task_completed_slackshowcase_task(objectName)examples/app-showcase/src/automation/flows/index.ts:471showcase_task_completed_rest_pingshowcase_task(objectName)examples/app-showcase/src/automation/flows/index.ts:530showcase_declarative_connector_pingshowcase_task(objectName)examples/app-showcase/src/automation/flows/index.ts:583showcase_mcp_connector_echoshowcase_task(objectName)examples/app-showcase/src/automation/flows/index.ts:632showcase_task_follow_upshowcase_task(objectName)examples/app-showcase/src/automation/flows/index.ts:724showcase_task_done_notify_ownershowcase_task(objectName)examples/app-showcase/src/automation/flows/index.ts:845showcase_project_closureshowcase_project(objectName)examples/app-showcase/src/automation/flows/index.ts:1003showcase_fan_out_notifyshowcase_task(objectName)examples/app-showcase/src/automation/flows/index.ts:1210showcase_resilient_syncshowcase_task(objectName)examples/app-showcase/src/automation/flows/index.ts:1309showcase_invoice_signoffshowcase_invoice(objectName)examples/app-showcase/src/automation/flows/index.ts:1411showcase_project_escalationshowcase_project(objectName)examples/app-showcase/src/automation/flows/index.ts:1758showcase_expense_signoffshowcase_expense_report(objectName)examples/app-showcase/src/automation/flows/index.ts:1808showcase_committee_quorumshowcase_expense_report(objectName)examples/app-showcase/src/automation/flows/index.ts:1878showcase_task_due_remindershowcase_task(timeRelative.object)examples/app-showcase/src/automation/flows/index.ts:1937showcase_urgent_task_alertshowcase_task(objectName)examples/app-todo/src/flows/task.flow.ts:324task_completiontodo_task(objectName)Zero flows in:
packages/apps/**,packages/platform-objects/**,packages/plugins/**,packages/services/**;create-objectstackblank template (itssrc/flows/index.tsisexport {};);examples/app-multi-package(the one multi-package example),examples/app-crm(one screen flow, no trigger),examples/embed-objectql;The
os g flowscaffold binds--objector 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 onshowcase_task.Tests. 115 trigger-bearing start nodes:
validate-flow-trigger-readiness.test.ts(other_package_object→billing_invoice), which is unchanged;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.tsholds 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
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.authoring-rules.ts,validateFlowTriggerReadiness,surfaces: CLI_AND_RUNTIME,runtimeTypes: ['flow']). It is not a reference-integrity-suite member, and itsruncallback did not read the rule context.buildRuntimeWriteSnapshotSetnarrowsobjectsfor every write type, flow included (runtime-gate.ts:721,narrowObjectsToPackageClosure).runRuntimeAuthoringRulesputspackageScopeon the rule context exactly whenpackageScopeInForceholds (:1121). So under a package write,objectNamesalready IS the closure.ctx.packageScopeto the rule and pickserrorwhen it is stated.runtime-gate.tsis unchanged.reference-integrity-suite.tsis touched only for one comment, after fix(lint)!: the overlay save door refuses a list view bound to an object that exists nowhere (#24668) #24789 landed.saveMetaItem.origin/maina43405d596with lint built from it,protocol.package-closure-gate.test.tsandprotocol-publish-drafts-closure.test.tswere 15/15 green. That included "reports a reference into a package the written one never declared": a flow written intocom.acme.crmtriggering on acom.other.analyticsobject answeredsuccess: truewithadvisories = ['flow-trigger-unknown-object']. The same body with no package drew nothing, because the stranger's object is in the whole collection.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 inprotocol-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 iserror, with a message that names the package and its closure, and a hint that names the remedy (declare the dependency in the manifestdependencies, 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 runsvalidateFlowTriggerReadiness(stack, ctx.packageScope ? { packageScope: ctx.packageScope } : {}). TheAuthoringRuleContext.packageScopedoc names its two readers.packages/lint/src/rule-explanations.ts.os explain flow-trigger-unknown-objectsaid "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.validate-list-view-field-refs.ts(two lines) andreference-integrity-suite.ts(one line, after fix(lint)!: the overlay save door refuses a list view bound to an object that exists nowhere (#24668) #24789 landed), plus one sentence incontent/docs/deployment/validating-metadata.mdx..changeset/24669-flow-trigger-out-of-closure.md:@objectstack/lintminor, a!title,Clause-②: yes (narrowing), FROM / TO / Fix, the stored-row sentence, and the ADR-0087 dispositionnot-required (no-migration-prescription)..changeset/pre.jsonreadsmode: 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, throughrunRuntimeAuthoringRules). The new block, for both positions:dependencies;isSystemobjects (one of them stranger-owned), a carriedsys_object, asys_name the snapshot does not carry, and a provenance-less overlay row;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 realsaveMetaItem/publishMetaItem):422 INVALID_METADATA, asserting the ADR-0112code+status, the issue'sseverity/pathand the hint, with nothing stored. This holds forconfig.objectName,config.timeRelative.object, and an object that exists nowhere;isSystemplatform object andsys_user;warningadvisory at both positions;Fixtures the consumer sweep found (outside
packages/lint/packages/spec), for the seat's #6367 addendumAll four are test files only. There is no
protocol.tsorobjectqlsource change, and no assertion was weakened.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.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 isINVALID_METADATAwith the issue aterror, and the healthy sibling isBATCH_ABORTED.packages/metadata-protocol/src/protocol.publish-item-rebind-announce.test.ts: the fixture is re-trued. Its stub registry now lists the flows' trigger objectticketunder the packageapp.ops. The batch CONTROL case had gone red becauseticketwas outside the closure. Assertions are unchanged.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 theticketcopy refused, the copieddupsrc_on_ticketkeeps the source'sticketas its trigger object, which is outside the copy package's closure, so the flow door refuses it. This is the same shape as fix(lint)!: the save door refuses a list view written into a package and bound outside its closure #24630's view re-pin (the seat's answer A,6277565064). The changeset's door list namesduplicatePackage.Ablation (G5): run once, recorded here, no permanent file
The fix was committed first.
scripts/ablation-replace.mjs --hold, armed with atraprestore, replaced the closure branch'sseverity: 'error',withseverity: 'warning',plus a dist-visible marker.48415eb218→255f3f2863.ablation-dist-preflight.mjs @objectstack/lint ABLATION_24669found the marker in 4 built files.git diff HEADempty, whole tree clean. After a rebuild,--absentfound the marker in none of the 20 built files. Both files went green again: 36/36 and 20/20.Verification
--maxWorkers=2, underos-verify-lock.sh), onaa20228392. Results:lint6953/6953;service-automation2605;runtime7587 (19 skipped);rest6404 (344 skipped). The two first-run reds were fixtures 3 and 4 above, re-run green after re-truing:metadata-protocolwas 1 red of 20834, andobjectql1 red of 8495.cli --project unitwas 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.origin/main(fix(lint)!: the overlay save door refuses a list view bound to an object that exists nowhere (#24668) #24789 landed), on009a2b2b09. Results:lintfull suite andtypecheckgreen;metadata-protocolfull suite green; the objectql fixture green; the two cli suites green.metadata-protocoltypecheckwas green on the pre-merge head.3d4226ca4f.node scripts/pm/dispatch-gates.mjs --commands(no paths) derives 96 families. All 96 ran with their exit codes recorded, and--ranreconciles 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 the009a2b2b09pass,check:skill-examplesandcheck:dual-build-cjs-loadsfirst 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'sTest Coreruns it.A pending release note corrected: please confirm (
Check Changesetstays red by design).changeset/24563-list-view-out-of-closure.md(#24563's, unreleased) said under Unchanged: "The flow door:flow-trigger-unknown-objectstays an advisory." This PR makes that sentence false, so the line now reads: "The flow door: under the same closure,flow-trigger-unknown-objectrefuses 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 Changesetis 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: theos explaintext;content/docs/deployment/validating-metadata.mdx: one sentence. Its lines are disjoint from fix(lint)!: the overlay save door refuses a list view bound to an object that exists nowhere (#24668) #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 fix(lint)!: the overlay save door refuses a list view bound to an object that exists nowhere (#24668) #24789 landed;.changeset/24563-list-view-out-of-closure.md: one line, after fix(lint)!: the overlay save door refuses a list view bound to an object that exists nowhere (#24668) #24789 landed.Before touching these, I read the file lists of all open PRs. Only #24789 overlapped, and it has since landed.
Acceptance notes
Generated by Claude Code