Repository navigation
A list view bound to an object outside the writing package's dependency closure skips the field-reference check: unknown columns fields are accepted (200 active, draft → publish) where the same body is refused 422 list-view-field-unknown #24563
Description
Filing gate: ① — product defect against a declared contract, with a reproduction (QA-measured).
Path: none (no checklist clause asserts this; observed while driving platform-core.metadata-authoring-roundtrip, priority P1 — tri-state ③, "no item asserts the capability")
Type: Bug (prefilled — violates the declared runtime-door contract of validateListViewFieldRefs and of the package-closure gate, quoted below)
QA-source: #24557 · platform-core.metadata-authoring-roundtrip · observation outside the clauses (verified by an independent re-run)
reach: measured once through the public HTTP door — PUT /api/v1/meta/view/showcase_task.qavfy_r1?package=com.qa.vfy2 with a columns entry naming a field that does not exist answers 200 state:"active" with no advisory at framework 5b14b7f; the identical body sent as an overlay answers 422 list-view-field-unknown. The served view then breaks: the console shows "This view's query was rejected" (GET /api/v1/data/showcase_task?fields=title,zzz_nope → 400 INVALID_FIELD).
What happens
A list view written into a writable package is checked for unknown field references only if its bound object is inside the package's dependency closure. When the view binds an object the package does not declare a dependency on (e.g. a code-package object such as showcase_task), or an object that does not exist at all, both the active PUT and draft → publish-drafts accept unknown columns fields silently (probes.issues: [], _diagnostics.valid: true). The same body is refused 422 list-view-field-unknown as an overlay, from a package that declares the dependency, or when bound to the package's own object.
Reproduction (showcase, admin)
POST /api/v1/packages {"manifest":{"id":"com.qa.vfy2","name":"QA Vfy2","version":"0.1.0","type":"app","namespace":"qavfy2"}}→ 201.PUT /api/v1/meta/view/showcase_task.qavfy_r1?package=com.qa.vfy2body{"name":"showcase_task.qavfy_r1","object":"showcase_task","viewKind":"list","label":"r1","type":"grid","data":{"provider":"object","object":"showcase_task"},"columns":[{"field":"title"},{"field":"zzz_nope"}]}- expected: 422
list-view-field-unknownatviews[0].columns[1].field - actual: 200
state:"active", no advisories.
- expected: 422
- Draft path: same body with
?mode=draft&package=com.qa.vfy2, thenPOST /api/v1/packages/com.qa.vfy2/publish-drafts {}→outcome:"published",probes.issues: []. - Controls (all refused 422
list-view-field-unknown): the same body with no?package(overlay); from a package declaringdependencies:{"com.example.showcase":"*"}; a view bound to an object the package itself owns. Bindingnosuch_objalso answers 200.
Mechanism
resolveWritePackageScope(packages/metadata-protocol/src/protocol.ts) feedsnarrowObjectsToPackageClosure(packages/lint/src/runtime-gate.ts), which removesshowcase_taskfromstack.objects— its owner is neither the writing package, a declared dependency, nor a system object.packages/lint/src/validate-list-view-field-refs.ts:if (!graph.has(object) || !graph.get(object)) return;skips the whole view, deferring an unresolvable base object tovalidateObjectReferences.packages/lint/src/reference-integrity-suite.ts:validateObjectReferencesruns onruntimeTypes: ['flow','dataset']only — never onviewwrites — so the deferred finding never fires. Neither an error nor an advisory reaches the author.
Declared contract it violates
reference-integrity-suite.tsnotes onvalidateListViewFieldRefs(runtimeTypes: ['flow','view','object']): the standalone list view "a Studio tenant or an MCP/AI author writes goes through that door … a build-time-only rule would never reach the author who made the typo", and "no missing-collection false-positive channel".- The
RuntimePackageScopedoc inruntime-gate.ts: "the failure direction is more validation input, never less" and "⛔ There is deliberately no branch that narrows on a guess and no branch that skips rules" — here the narrowing makes the rule skip. runtime-gate.package-closure.test.tspins "DOES judge a reference outside the declared dependencies" — flows do report this case (flow-trigger-unknown-object); views report nothing.
Related, closed: #14107 made list-view field references checked at author time in os validate / os build; this is the runtime door's out-of-closure gap.
Filing-gate answers
- Category: ① (finding class (b): violates a declared contract; reach measured above).
- Acting reader: the lane owning
packages/lintruntime gate +packages/metadata-protocolwrite scope — candidate fixes: dispatch an object-reference check forviews[].data.objectonviewwrites, or resolve the field check against the full object collection when the bound object is outside the closure, or refuse/warn on the out-of-closure binding itself. - Duplicate search (open AND closed), objectstack: "list-view-field-unknown view columns unknown field accepted on draft publish writable package" → 10 hits (metadata-protocol:
publishPackageDraftstakes an opt-in per-field backfill of existing rows with the new default, so a copilot draft that adds a field can fill existing records on publish (ruling A on cloud#2924) #24253, metadata-protocol: over drafts [package-unbound, Q] with no active row, the strict draft read naming no package serves its version from the package-unbound draft but its content from whichever row the store returns first #23715, No field reference on a list view is checked at author time — columns, filter, sort, grouping and every binding block accept a misspelt field name through bothos validateandos build#14107, publishPackageDrafts validation closure omits same-batch dataset drafts: a package shipping dashboard+dataset together can never publish (widget-dataset-unknown) #10377, A draft row stored under a manifest-absent plural (fields,seeds,external_catalogs,translations) is promotable by publishPackageDrafts and lands ACTIVE in the second namespace #8908, publishPackageDrafts' ADR-0010 audit row records the raw stored type spelling — the #8769 fold closed this forpublishMetaItemonly #8858, publishPackageDrafts' pre-flight refusal writes no audit row — a package publish rejected before the transaction leaves no trail #8595, The lock/conflict denial audit rows roll back with the batch on publishPackageDrafts — a refused item in a package publish still leaves no trail #8594 among them — none this defect; No field reference on a list view is checked at author time — columns, filter, sort, grouping and every binding block accept a misspelt field name through bothos validateandos build#14107 is the build-time sibling).
Generated by Claude Code
Activity
objectstack-fleet commentedon Oct 19, 2026
Triage: p1 +
target:v18(protocol-first, narrowing) ·bug·domain:spec·area:records·pm:queue· put to the maintainer for pulling forward · triage seat ·session_01AavokzJ5DndAwitDXvKy4U· 2026-10-19T14:06ZTriage seat (objectstack-wide, seat post #6015). ⛔ Not a claim, ⛔ not a dispatch.
Fresh read on
origin/mainba4afcc239.validateListViewFieldRefsis atpackages/lint/src/validate-list-view-field-refs.ts:610. The card measures it skipped at the runtime door when the view's bound object is outside the writing package's dependency closure. The same body is refused 422 as an overlay. Its measurement is the card's, verified by an independent re-run.Grade. Refusing an unknown
columnsfield there narrows what a metadata door accepts. That is the protocol-first class, so p1 +target:v18.domain:spec(packages/lint, which holds the runtime gate), as for lint: the visibility-predicate rules run at the runtime save door for a view but not for a page, so a page visibleWhen calling an unknown function is saved through Studio, REST or MCP and refused only by os validate (ruling B on #23388's principle) #24491.Put to the maintainer for pulling forward (
6261447528):Until the maintainer answers, nothing on this card moves on that question.
Generated by Claude Code
objectstack-fleet commentedon Oct 19, 2026
Claim: PM loop round 1 · 2026-10-19T15:49Z · a list view written into a package is judged against that package's closure at the save door: a bound object outside the closure is reported the way a flow's is, never skipped, and an in-closure object's unknown
columnsfield is refused as the overlay door refuses itPulled forward on the maintainer's word in this session's chat at 2026-10-19T15:49Z, verbatim: 「一、两张协议卡要不要赶进 18.0 同意」. It answers the triage seat's pull-forward question (
6261447528, card6269170137) as 18.0, shipped as a refusal. The seat'sbatch 2(「并发2」) holds it beside #24534.Session:
session_019oFY6Hi35J7atvgVFnmoaK(the seat; the dev is anos-devsubagent of this session)Account:
zhuangjianguo(the seat's linked user asGET /useranswers it; always the card's assignee)Branch:
claude/issue-24563-list-view-out-of-closureWorktree:
objectstack-issue-24563Domain:
domain:specSeat:
domain:spec#2(seat post #18549)File surface (paths on
origin/main5a871e4e41or later; stop on a breach and explain it in the report):packages/lint/src/validate-list-view-field-refs.ts(the:710skip),packages/lint/src/reference-integrity-suite.ts(thevalidateObjectReferences/validateListViewFieldRefsrows,:224and:309), andpackages/lint/src/runtime-gate.ts(only if the closure input needs it), with their tests andruntime-gate.package-closure.test.ts;domain:engineon [PM seat] domain:engine — 🟢 os-project-manager #6367 in this act: a pin block inpackages/metadata-protocol/src/protocol.runtime-authoring-gate.test.tsthrough the realsaveMetaItem/publishPackageDraftsfor aview. Test file only, ⛔ noprotocol.tschange (PR fix(spec,metadata-protocol)!: the /meta door no longer authors sharing rules; Setup lists declared sharing rules #24599 is in flight on it);.changeset/24563-*.md.narrowObjectsToPackageClosure's four limbs, ruling#9612). Notcontent/docs/releases/.Container & model:
M,mode:subagent,model: default tierfor the draft (dispatch-gates --tier: no path-derived mandate). The review before the queue runs atCONTRACT_REVIEW_TIER(the narrowing face: a metadata save door accepts less).Clause-②: no (narrowing)
Responsibility: the view door skips the field check when
narrowObjectsToPackageClosuredrops the bound object, and no rule judges the out-of-closure binding on aviewwrite (validateObjectReferencesdeclares['flow', 'dataset']) | the gate's own pinned contract ("DOES judge a reference outside the declared dependencies"), which flows already honour (flow-trigger-unknown-object), and ruling#9612(a package's declared dependencies bound what it may reference) | every author who writes a list view into a package through Studio, REST or MCP; measured by QA (QA run · platform-core (27/34) · 5b14b7f3 · 2026-10-19 · 21 PASS / 4 PARTIAL / 2 FAIL / 0 BLOCKED / 7 NOT-RUN #24557) throughPUT /api/v1/meta/viewThread-read: 6269170137
Serial constraints cleared:
packages/metadata-protocol/src/protocol.ts, which is excluded above. None touches the lint files above orprotocol.runtime-authoring-gate.test.ts.packages/spec/src/ui/app.zod.ts, which is disjoint.Generated by Claude Code
objectstack-fleet commentedon Oct 19, 2026
Dispatched ·
domain:specseat 2 · sessionsession_019oFY6Hi35J7atvgVFnmoaK· 2026-10-19T16:16Z · for claim6271302258.mode:subagent: the dev is anos-devsubagent of this session, on branchclaude/issue-24563-list-view-out-of-closure, at the default tier.batch 2: it runs beside #24534.The ruling is premise-bound and follows ruling #9612 (a package's declared dependencies bound what it may reference) and the gate's pinned "DOES judge a reference outside the declared dependencies".
columnsfield is refused, as the overlay door already refuses it.needs_decision.It reports here as an
os-dev-reportcomment and opens a draft PR. The seat subscribes to that PR the moment it exists.Generated by Claude Code
fix(lint)!: a list view bound outside the written package's closure i…
test(lint,metadata-protocol): pin the view door's closure verdict (#2…
fix(lint): export the list-view-object-outside-closure rule id and it…
docs(lint): changeset and the closure paragraph for the view door's o…
objectstack-fleet commentedon Oct 19, 2026
os-dev-report
{
"issue": 24563,
"status": "done",
"branch": "claude/issue-24563-list-view-out-of-closure",
"pr": "#24630",
"session": "session_019oFY6Hi35J7atvgVFnmoaK — mode:subagent, the seat's id (the claim 6271302258's Session line)",
"premise_still_valid": true,
"summary": "A list view written into a package and bound outside that package's closure (ruling #9612) is now reported at the runtime save door instead of being skipped. When the gate narrowed the snapshot it states the closure on the rule context (AuthoringRuleContext.packageScope, one predicate packageScopeInForce shared with narrowObjectsToPackageClosure, limbs untouched), and validateListViewFieldRefs reports an unresolvable binding once per list view as list-view-object-outside-closure at warning, located at the key the binding was read from (views[0].data.object), with a hint naming the remedy (declare a dependency on the defining package in the writing package's manifest dependencies). Per P2 that is the flow door's verdict (flow-trigger-unknown-object, warning), so nothing new is refused. The in-closure field refusal (422 list-view-field-unknown) already held and is now pinned at both layers. Overlay writes and the CLI are byte-identical. files_changed: packages/lint/src/validate-list-view-field-refs.ts, packages/lint/src/runtime-gate.ts, packages/lint/src/reference-integrity-suite.ts, packages/lint/src/authoring-rules.ts (interface member only), packages/lint/src/index.ts (two exports), packages/lint/src/runtime-gate.package-closure.test.ts (new block), packages/metadata-protocol/src/protocol.runtime-authoring-gate.test.ts (new block, test only), .changeset/24563-list-view-out-of-closure.md (@objectstack/lint minor), content/docs/deployment/validating-metadata.mdx (one sentence). deviations: (1) The PR body and changeset declare Clause-②: yes, not the claim's no (narrowing). G3 is falsified by P2: no accept set narrows, and the public surface grows (an optional context member, an optional argument, two exports). os-dev.md says to copy the claim's line verbatim; that was not done, because the copy would declare a BREAKING change that refuses nothing. See open question 1. (2) File surface beyond the claim, explained in the PR body: authoring-rules.ts (the only typed channel from the gate to a rule), index.ts (rule-id-barrel-exports.test.ts reds without the id), validating-metadata.mdx (its sentence 'never what it judges' became false). (3) One early
tsc --noEmitover packages/lint ran outside os-verify-lock.sh; every later build and test went through the lock. (4) P1 was probed through the real saveMetaItem / publishPackageDrafts over the stub-engine harness, not a booted PUT /api/v1/meta/view. (5) The branch was merged with origin/main once (5a871e4 → c395405). main has since moved 6 commits (973917c), none in lint or in a file this PR edits; it was not re-merged because there is no conflict. (6) The first commit's subject says fix(lint)!: although the change is not breaking. It was not rewritten (no force-push); the PR title is the squash subject. (7) A harness reminder asked for a model-named Co-Authored-By trailer and another PR footer. Commits carry AGENTS.md's model-free Claude-Session / Co-authored-by pair, and the body carries AGENTS.md's session-URL footer.","tests": "P1 probe (temporary, deleted): real saveMetaItem / publishPackageDrafts, stub engine with com.acme.crm (declares com.acme.core), com.acme.core, stranger com.other.analytics and a platform object; the card's body. BEFORE (origin/main 5a871e4, lint built from it): package + stranger object → 'RESOLVED success=true state=active advisories=[]'; package + nosuch_obj → same; overlay + stranger → 'REFUSED 422 INVALID_METADATA issues=[["list-view-field-unknown","error","views[0].columns[1].field"]]'; publishPackageDrafts → 'outcome=published published=1 failed=[]'. AFTER: package + stranger → 'RESOLVED success=true state=active advisories=[["list-view-object-outside-closure","warning","views[0].data.object"]]'; nosuch_obj → same; overlay unchanged (422, and nosuch_obj 200 with advisories=[]); publishPackageDrafts → published[0].advisories carries the same finding; in-closure own / declared-dependency / platform objects → 422 list-view-field-unknown, unchanged. Suites: pnpm --filter @objectstack/lint exec vitest run --maxWorkers=2 at 8a295d8 → 'Test Files 148 passed (148) / Tests 6934 passed (6934)'; lint typecheck (with check:test-typecheck) exit 0. pnpm --filter @objectstack/metadata-protocol exec vitest run --maxWorkers=2 at cab356c (the same code as fe5bafb) → 'Test Files 248 passed | 3 skipped (251) / Tests 20731 passed | 19 skipped (20750)'; typecheck exit 0. At 8a295d8 the protocol runtime-authoring-gate, package-closure-gate and publish-drafts-closure files → 175/175, typecheck exit 0. New pins: the lint block, 11 cases (file 27/27); the protocol block, 8 cases (file 160/160). Ablation at fe5bafb through scripts/ablation-replace.mjs: 'if (scope) reportOutsideClosure(...)' became 'if (scope && String(object) === 'ablated_24563_no_report') ...', anchor 1→0, blob df4ce7aa8c22→7340a202512b; lint rebuilt; ablation-dist-preflight marker present in 4 built files. Mutated: lint file 5 red / 22 green, protocol block 4 red ((a)×2, (d), (e)) / 4 green, with 'expected [] to deeply equal [ 'list-view-object-outside-closure' ]'. Restore: blob == HEAD, git diff HEAD empty, rebuild, --absent from all 20 built files, tree clean; lint file 27/27, protocol file 160/160. P3 harness (temporary, deleted): 11 shipped views / 39 bindings, 0 outside the closure, 0 findings scoped or whole, before and after; synthetic control (CRM lead view retargeted to showcase_task) → 3 advisories, 0 errors.",
"gates": "Local, at head 8a295d8: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, no paths, 9 changed paths vs merge base c395405 → 93 families, each run verbatim with exit captured before any pipe. 92 exit 0. 1 NOT MEASURED: pnpm check:dual-build-cjs-loads exit 3, PREREQUISITE NOT MET (33 export entries with no dist/); family: dual-build-cjs-loads, reason: needs every package built, left to CI. --ran reconciliation: '✓ dispatch-gates --ran: 93 derived famil(ies) accounted for — 92 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit 3)', exit 0. Verdict lines: check-adr-0087-registration --base origin/main '✓ ... this PR adds no declared-breaking changeset (1 non-breaking changeset(s) seen)'; check-empty-changeset '✓ No empty-frontmatter changeset introduced'; check:nul-bytes 'OK (scanned 12383 text file(s) ...)'; check:cross-package-test-inputs OK; check:test-source-alias OK; check:doc-authoring clean; check-changeset-no-major exit 0 (locally reads no PR body). pnpm --filter @objectstack/spec check:generated: '✓ All 14 generated artifacts are up to date'. Not local: test:repo (no repo-project test changed and not derived) and pnpm lint (CI-owned). check-platform-checklist not run: no export renamed or removed. PR #24630 check-runs at report time, head 8a295d8: 'No other open PR may claim the same issue' success; 'Packed-tarball smoke (opt-in)' skipped. Check Changeset, Governed Surface Queue Guard, Lint · ESLint / CI, release & engine gates / PM dispatch-gates / docs & code guards / repo & tooling gates / scoped ×3, Type Check · consumer gates / debt ledger / source gates / workspace, Spec property liveness, Check PR Size, Check Documentation Links, the claim and Part-of guards: all in_progress. Test Core and Build Core had not yet appeared.",
"mcp_calls": "0",
"api_writes": "2 relay strokes, each one POST /repos/objectstack-ai/objectstack/dispatches executed as objectstack-fleet[bot]: ① pr_create → POST /repos/objectstack-ai/objectstack/pulls (draft) + POST /repos//issues/24630/assignees (zhuangjianguo), read back byte-identical (18170 bytes); ② comment → POST /repos//issues/24563/comments (this report). git push ×6 to the branch (not REST). No label write.",
"open_questions": [
{
"question": "Clause-② on the claim (6271302258) reads 'no (narrowing)'. The delivered change narrows no accept set and grows the public surface, so PR #24630's body and changeset read 'Clause-②: yes' (minor, no !, no ADR-0087 marker). Which line stands? The queue gate reads the claim's line and Check Changeset reads the body's.",
"options": [
"A — correct the claim to 'Clause-②: yes'; the body and changeset stay. Business need: none of the 11 shipped views changes behaviour, so the CHANGELOG must not tell consumers something broke. Long-term: one declaration, and a true one. AI-safety: a false BREAKING banner teaches an upgrading agent to look for a migration that does not exist. Startup focus: zero extra work.",
"B — keep 'no (narrowing)' and rework the body and changeset into a BREAKING narrowing (! title, FROM/TO, ADR-0087 disposition). Business need: none. Long-term: a declaration that the diff contradicts. AI-safety: worse, for the reason in A. Startup focus: one more PR body edit and a changeset rewrite."
],
"recommendation": "A, because the line answers 'does this widen the accept set or enlarge the public surface', and measured it does the second and narrows nothing. B would only be right if open question 2 is answered B."
},
{
"question": "Should a view's out-of-closure binding be refused rather than advised? The dispatch tied it to the flow verdict (advisory), and this PR delivers that. But P2 found the dataset door already REFUSES an out-of-closure base object under the same closure (errors=['error:object-reference-unknown@datasets.crm_ds.object'], and [] with no package), and the claim records the maintainer's 18.0 pull as 'shipped as a refusal'.",
"options": [
"A — keep the advisory (this PR as is). Business need: the author is told at save (2xx advisories); 0 shipped views affected. Long-term: matches the flow door, but leaves flow and dataset disagreeing. AI-safety: weakest of the three, because an AI loop that reads only the status code stores a view bound to a typo (nosuch_obj) and it fails at render. Startup focus: done, no more work.",
"B — flip the view binding to error (refused 422 with the same hint). Business need: P3 measured 0 shipped views outside their closure, so nothing shipped breaks; deployed rows were not measured. Long-term: matches the dataset door and the ruling's 'may reference'. AI-safety: strongest, a loud refusal at publish. Startup focus: a one-line severity change, the pins' expectations flipped, the changeset rewritten as a narrowing (Clause-② no (narrowing), minor + BREAKING, ADR-0087 not-required), and a CONTRACT_REVIEW_TIER review.",
"C — one follow-up card that settles a single verdict for out-of-closure object references across the flow, dataset and view doors, and ships A until then. Business need: real inconsistency, no measured user hit. Long-term: best (one policy). AI-safety: depends on the verdict chosen. Startup focus: widens scope beyond this card, and is a new decision for the maintainer."
],
"recommendation": "B for the view door if the maintainer's 18.0 pull was meant as a refusal: zero shipped impact, the dataset door already refuses the same reference under the same closure, and a loud refusal is what keeps an AI author from storing a dead binding. Otherwise A as delivered. Either way, the flow door's own advisory is a separate question and is not changed here."
}
],
"out_of_scope_findings": [
"class: a · reach: save door, measured through saveMetaItem over the stub engine (not a booted HTTP door). An overlay write (no package) of a list view bound to 'nosuch_obj' answers success with advisories [], because no rule judges a view's data.object binding on a view write without a package (validateObjectReferences runs on flow/dataset only; skip 1 defers to it), while a flow on the same overlay write gets flow-trigger-unknown-object. · evidence: probe reading 'overlay, nosuch_obj → RESOLVED success=true state=active advisories=[]' before and after; pinned as unchanged by protocol block (c) and the lint no-package case. · dedupe words: overlay list view nonexistent object accepted, view data.object unresolved save door, validateObjectReferences view runtimeTypes, list-view skip 1 overlay",
"carrier: this report's open question 2 · noted, not filed: the save door has two verdicts for an object reference outside the written package's closure (flow: flow-trigger-unknown-object warning; dataset: object-reference-unknown error), and this PR gives the view the flow's."
]
}
Generated by Claude Code
objectstack-fleet commentedon Oct 19, 2026
Claim amendment ·
domain:specseat 2 · sessionsession_019oFY6Hi35J7atvgVFnmoaK· 2026-10-19T18:08Z · for claim6271302258.The claim's
Clause-②: no (narrowing)is corrected toClause-②: yes (narrowing). PR #24630 refuses an out-of-closure list view binding, which is the narrowing, and adds public surface to@objectstack/lint(an optional rule-context member, an optional argument, two exports). The seat's answer is on the PR (REWORK).Generated by Claude Code
fix(lint)!: refuse a package's list view bound outside its closure at…
objectstack-fleet commentedon Oct 19, 2026
os-dev-report
{
"issue": 24563,
"status": "needs_decision",
"branch": "claude/issue-24563-list-view-out-of-closure",
"pr": "#24630",
"head": "a0c24ab647b226d431051591fa511c3cd1a8b144",
"session": "session_019oFY6Hi35J7atvgVFnmoaK — mode:subagent, the seat's id (claim 6271302258, amended 6273958360)",
"premise_still_valid": true,
"summary": "Patch round 1 is one commit, a0c24ab on 8a295d8. list-view-object-outside-closure is now an error: a publish-mode write (saveMetaItem publish, draft promotion, publishPackageDrafts) of a package's list view bound outside the package's closure, or to an object that exists nowhere, is refused 422 INVALID_METADATA at views[0].data.object with the declare-the-dependency hint, and nothing is stored. Drafts stay ungated, and a stored row is judged at its next save (a new pin, (g)). Both pin layers flip to the refusal. Every control is unchanged: in-closure unknown field refused, known field admitted, declared dependency admitted, platform object admitted, overlay unchanged including overlay nosuch_obj, and the flow control. Changeset: @objectstack/lint minor, ! title, Clause-②: yes (narrowing), FROM / TO / Fix, stored-row sentence, ADR-0087 not-required (no-migration-prescription), as check-adr-0087-registration decides. The docs sentence now says refused. Addendum: round 0's CI red (objectql publish-package-drafts-response-conformance) and two more fixtures the consumer sweep found are re-trued to a reachable object with assertions unchanged. ONE fixture is left red, as the addendum's stop rule requires: objectql protocol-duplicate-package-unprefixed-object.test.ts, open question 1. The PR must not land until it is answered. files_changed (this round): packages/lint/src/validate-list-view-field-refs.ts, reference-integrity-suite.ts, index.ts, authoring-rules.ts (doc comments), runtime-gate.package-closure.test.ts; packages/metadata-protocol/src/protocol.runtime-authoring-gate.test.ts; .changeset/24563-list-view-out-of-closure.md; content/docs/deployment/validating-metadata.mdx; packages/objectql/src/publish-package-drafts-response-conformance.test.ts; packages/objectql/src/protocol-duplicate-package-names-in-step.test.ts; packages/qa/dogfood/test/view-container-cross-package-default.dogfood.test.ts. deviations: (1) Three files are outside the claim's surface: two objectql test files (engine lane) and one dogfood test. They are declared in pr_body_edits for the #6367 rider. (2) protocol-duplicate-package-unprefixed-object.test.ts is left red at this head (open question 1). (3) The dogfood suite was not run in full: the 7 grep-named files plus the re-trued file ran (8 / 51 passed); the rest is main's full battery. A git grep for package-scoped view writes names only the re-trued file. (4) @objectstack/spec's suite was not run: it has no workspace dependency, so it cannot reach the gate (declared narrowing). (5) The branch was not re-merged. No file this PR touches changed on main (now 7c7741d), but dispatch-gates warned STALE TREE for 3 derivation inputs. (6) The first sweep run was restarted to add --maxWorkers=2. Its build child (my own PID 15613) was left to finish, not killed. (7) The clone was deepened (git fetch --shallow-since=2026-07-14 origin main) so check-engine-split-ratio could compute. (8) The worktree was re-created on the existing branch (the round-0 cleanup had removed it). (9) PR body: not PATCHed. 18 old/new pairs are in pr_body_edits, each old_str verified to occur exactly once in the live body (byte-identical to the round-0 create). Suggested title: 'fix(lint)!: the save door refuses a list view written into a package and bound outside its closure'.",
"tests": "Pins at a0c24ab: lint runtime-gate.package-closure.test.ts 27/27; metadata-protocol protocol.runtime-authoring-gate.test.ts 162/162 (the #24563 block has 10 cases). Ablation (severity back to warning) through scripts/ablation-replace.mjs: "severity: 'error'" became "severity: String(object) === 'ablated_24563_warning' ? 'error' : 'warning'", anchor 1→0, blob f38af8f08988→377fdb85d3dd; lint rebuilt; ablation-dist-preflight marker present in 4 built files. Mutated: lint file 4 red / 23 green (the three refusal cases and the object-door refusal in the flow case; the CLI case checks the rule id only and stayed green); protocol block 5 red ((a)×2, (d), (e), (g)) / 5 green (the draft save, (b)×2, (c), (f)). Restore: blob == HEAD, git diff HEAD empty, rebuild, --absent from all 20 built files, tree clean; 27/27 and 162/162. Consumer sweep, each run as pnpm --filter PKG test --maxWorkers=2 under os-verify-lock.sh: lint 148/6934 passed; metadata-protocol 248 passed + 3 skipped / 20733 passed; objectql first run 445 passed, 2 failed / 8407 passed, 7 failed (6 names-in-step, re-trued → 6/6; 1 unprefixed-object, left red); rest 300/6095; runtime 423/6655; core 106/2580; metadata 63/892; platform-objects 78/1188; plugin-security 188/3754; service-automation 192/2567; service-cluster 7/105; service-datasource 45/838; cli (both projects) 474 passed, 1 skipped / 6018 passed. At a0c24ab, objectql conformance + names-in-step + unprefixed: 25 passed, 1 failed ('a name already claimed is refused up front…': failed has 2 entries, the second 'view/ticket_views … views[0].object [list-view-object-outside-closure]'). Conformance alone was 16/16 after its re-true; before it, 6 failed under the refusal. Dogfood: the 7 grep-named files plus view-container-cross-package-default → 8 files / 51 tests passed. Control: that file without the declared dependency → 2 failed, 'expected 422 to be 200' (PUT /api/v1/meta/view/os_qa_keyed_probe?package=com.example.repairassets) and 'expected 'refused' to be 'published'' (POST /packages/com.example.repairassets/publish-drafts). That is the refusal measured through the booted REST door. Typechecks exit 0: lint (with check:test-typecheck), metadata-protocol, objectql, dogfood. P3 harness re-run (temporary, deleted): universe 119 objects, 11 views / 39 bindings, 0 outside the closure, 0 refused scoped or whole; control → errors ['list-view-object-outside-closure' ×3], advisories [].",
"gates": "At a0c24ab: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, no paths, 12 changed paths vs merge base c395405 → 95 families, each run verbatim with exit captured before any pipe. The derivation warned STALE TREE: 3 derivation inputs changed on main, none a file this PR touches; not re-merged per the patch instruction. First non-zero runs, re-run after fixing their prerequisite: check-engine-split-ratio --days 90 exit 2 (shallow refusal) → deepened → exit 0; check:skill-examples exit 3 (client-react without .d.ts) → built client and client-react → exit 0. NOT MEASURED: check:dual-build-cjs-loads, exit 3 PREREQUISITE NOT MET (needs every package built), left to CI. --ran: '✓ dispatch-gates --ran: 95 derived famil(ies) accounted for — 94 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit 3)', exit 0. check-adr-0087-registration --base origin/main exit 0 ('1 declared-breaking changeset(s), each carrying an ADR-0087 disposition'); check-empty-changeset --base origin/main exit 0; check-changeset-no-major --base origin/main exit 0 ('This diff introduces no
majorbump'); check:nul-bytes OK; check:cross-package-test-inputs OK; check:test-source-alias OK. Not local: test:repo and pnpm lint (CI-owned). PR check-runs on a0c24ab at report time: 14 success, 5 skipped, 19 in_progress, 0 failure. Test Core is expected to go red on protocol-duplicate-package-unprefixed-object.test.ts (open question 1).","mcp_calls": "0",
"api_writes": "1 this round: one relay stroke, POST /repos/objectstack-ai/objectstack/dispatches executed as objectstack-fleet[bot] → comment, POST /repos//issues/24563/comments (this report). Plus 1 git push of the branch (8a295d8..a0c24ab, not REST). No PR body PATCH, no label write.",
"pr_body_edits": [
{
"old_str": "Fixes #24563\n\nClause-②: yes\n\n",
"new_str": "Fixes #24563\n\nClause-②: yes (narrowing)\n\n"
},
{
"old_str": "- A binding outside the closure is reported, never skipped. This covers an object of a package the writing package never declared, and an object that exists nowhere. It gets one
list-view-object-outside-closurefinding atwarning, located at the key the binding is read from (views[0].data.object). The hint names the remedy: declare a dependency on the package that defines the object in thedependenciesof the writing package's manifest. That is the verdict a flow's out-of-closure trigger object gets at the same door (P2), so the write is still stored and the finding rides the 2xxadvisories.","new_str": "- A binding outside the closure is refused, never skipped. This covers an object of a package the writing package never declared, and an object that exists nowhere. A publish-mode write (an active save, a draft promotion, a package draft publish) answers
422 INVALID_METADATAwith onelist-view-object-outside-closureissue aterror, located at the key the binding is read from (views[0].data.object), and nothing is stored. The hint names the remedy: declare a dependency on the package that defines the object in thedependenciesof the writing package's manifest. That is the verdict the dataset door already gives an out-of-closure base object under the same closure (P2). The flow door's advisory is unchanged. Drafts stay ungated, and a stored row is judged at its next save."},
{
"old_str": "## Read first: the declaration differs from the claim\n\nThe claim (
6271302258) and the dispatch carryClause-②: no (narrowing). That line was written when the out-of-closure binding was expected to be refused. P2 measured a flow's out-of-closure object reference at advisory severity. The dispatch's own prohibition says that in that case "the view gets the same advisory for the binding, and the in-closure field refusal stays as it is". So this PR refuses nothing that was accepted before: no accept set narrows. The public surface does grow, by an optionalAuthoringRuleContext.packageScope, an optional second argument tovalidateListViewFieldRefs, and two new exports (LIST_VIEW_OBJECT_OUTSIDE_CLOSURE,ListViewFieldRefOptions). That isClause-②: yesby the definition "does this card widen the accept set or enlarge the public surface". So this body and the changeset declareClause-②: yesatminor, with no!, no FROM / TO lines and no ADR-0087 disposition. The claim's line is the seat's to correct, or this body's if the seat rules otherwise.\n\n","new_str": "## Patch round 1: refused, not advised\n\nThe seat answered the round-0 question with B (record
6273946447on this PR) and amended the claim's line toClause-②: yes (narrowing)(6273958360on #24563).list-view-object-outside-closureis now anerror, so the door narrows: the binding the round-0 head reported is refused. The public surface still grows by an optionalAuthoringRuleContext.packageScope, an optional second argument tovalidateListViewFieldRefs, and two exports (LIST_VIEW_OBJECT_OUTSIDE_CLOSURE,ListViewFieldRefOptions). Widen plus narrow isClause-②: yes (narrowing). The changeset isminorwith a!title, FROM / TO / Fix lines and the ADR-0087 dispositionnot-required (no-migration-prescription). Round 0's CI red (Test Core (5/6), the objectql publish-drafts conformance fixture) and the consumer sweep's fixtures are re-trued in the same commit; see "Fixtures re-trued".\n\n"},
{
"old_str": "a binding missing from
stack.objectsis reported once per list view, at the key it was read from","new_str": "a binding missing from
stack.objectsis refused once per list view (error), at the key it was read from"},
{
"old_str": " - out-of-closure binding reported,
nosuch_objreported, and reported under an empty closure;","new_str": " - out-of-closure binding refused,
nosuch_objrefused, and refused under an empty closure;"},
{
"old_str": " - the flow verdict unchanged, with a stored sibling's out-of-closure list view cancelling in the differential and reported when that object is itself written;",
"new_str": " - the flow verdict unchanged, with a stored sibling's out-of-closure list view cancelling in the differential (a flow publish is not refused for it) and refused when that object is itself written;"
},
{
"old_str": " - (a) the active save is reported, for the stranger's object and for
nosuch_obj;","new_str": " - (a) the active save is refused
422 INVALID_METADATAatviews[0].data.object, nothing stored, for the stranger's object and fornosuch_obj; the draft save of the same body is not gated;"},
{
"old_str": " - (d) draft promotion: reported, and the in-closure case refused;\n - (e) package draft publish: reported on
published[].advisories, and the in-closure case refused;\n - (f) a view bound to an object drafted in the same batch: no phantom report, and its fields judged.","new_str": " - (d) draft promotion: refused, nothing stored, and the in-closure case refused;\n - (e) package draft publish: refused (
failed[0],INVALID_METADATA, the closure issue), nothing stored, and the in-closure case refused;\n - (f) a view bound to an object drafted in the same batch: no phantom refusal, and its fields judged;\n - (g) a stored view with the binding loads (loaded: 1, errors: 0); its next save into the package is refused and the row is untouched."},
{
"old_str": "- Changeset:
.changeset/24563-list-view-out-of-closure.md,@objectstack/lintminor,Clause-②: yes.","new_str": "- Changeset:
.changeset/24563-list-view-out-of-closure.md,@objectstack/lintminor,!title,Clause-②: yes (narrowing), FROM / TO / Fix lines, the stored-row sentence, and the ADR-0087 dispositionnot-required (no-migration-prescription)(check-adr-0087-registration --base origin/main: "1 declared-breaking changeset(s), each carrying an ADR-0087 disposition")."},
{
"old_str": "- After (this branch):\n - stranger's object:
RESOLVED success=true state=active advisories=[[\"list-view-object-outside-closure\",\"warning\",\"views[0].data.object\"]];\n -nosuch_obj: the same advisory;\n - overlay: unchanged (422, andnosuch_objthere200withadvisories=[]);\n -publishPackageDrafts:published=1withpublished[0].advisories=[[\"list-view-object-outside-closure\",\"warning\",\"views[0].data.object\"]];\n - in-closure controls: unchanged.","new_str": "- After (patch round 1,
a0c24ab647), through the protocol block on the same harness:\n - stranger's object: refused422 INVALID_METADATA,issues=[[\"list-view-object-outside-closure\",\"error\",\"views[0].data.object\"]], no active row;\n -nosuch_obj: the same refusal;\n - overlay: unchanged (422 list-view-field-unknown, andnosuch_objthere200withadvisories=[]);\n -publishPackageDrafts:success=false publishedCount=0,failed[0]INVALID_METADATAwith the closure issue;\n - in-closure controls: unchanged.\n- After, through the booted HTTP door: the dogfood fileview-container-cross-package-default.dogfood.test.ts(real showcase composition, REST) run with its Studio-created package declaring NO dependency answeredexpected 422 to be 200onPUT /api/v1/meta/view/os_qa_keyed_probe?package=com.example.repairassetsandexpected 'refused' to be 'published'onPOST /packages/com.example.repairassets/publish-drafts. With the dependency declared it passes 4/4.\n- (Round 0,8a295d8477, for the record: the same probe answeredRESOLVED success=truewith the finding atwarninginadvisories.)"},
{
"old_str": "So the door has two verdicts today for "an object outside the package's closure": flow advisory, dataset refusal. Per the dispatch the view takes the flow's. The question is raised in the report.",
"new_str": "So the door had two verdicts for "an object outside the package's closure": flow advisory, dataset refusal. Round 0 gave the view the flow's; patch round 1 gives it the dataset's (the seat's answer B). The flow door is unchanged."
},
{
"old_str": "- Control through the same harness: the CRM lead view retargeted at
showcase_task, undercom.example.crm, draws 3 advisories (listand its twolistViews) and 0 errors.","new_str": "- Control through the same harness: the CRM lead view retargeted at
showcase_task, undercom.example.crm, draws 3 advisories (listand its twolistViews) and 0 errors at round 0.\n- Re-run at patch round 1 (a0c24ab647, universe 119 objects after main addedsys_identity_email): 11 views, 39 bindings, 0 outside the closure, 0 refused, scoped or whole. The control now draws 3 errors (list-view-object-outside-closure×3) and 0 advisories."},
{
"old_str": " - its ladder answers
errorfor an unprefixed unresolved name. Package scope, out-of-closure:[\"error:object-reference-unknown@views[0].object\"]. That is a refusal, not the P2 verdict;","new_str": " - its ladder answers
errorfor an unprefixed unresolved name butwarningfor a platform-prefixed one, and its severity is not keyed to the closure. Package scope, out-of-closure:[\"error:object-reference-unknown@views[0].object\"];"},
{
"old_str": "- G3. Falsified in part, by P2. The out-of-closure binding is advisory and the in-closure refusal already existed, so no accept set narrows.\n - The changeset is
minoron the public-surface growth, withClause-②: yes.\n - There is no!, no FROM / TO, and no ADR-0087 marker:check-adr-0087-registration --base origin/mainreads no breaking declaration.\n - The Fix line is there: declare the dependency, or bind inside the closure, after which the field is judged.\n","new_str": "- G3. Held at patch round 1.
minor,!title,Clause-②: yes (narrowing), FROM / TO / Fix lines (the fix: declare the dependency on the defining package in the writing package's manifestdependencies, or bind an object it may reach), following PR #24509's changeset.check-adr-0087-registration --base origin/maindecidesnot-required (no-migration-prescription): exit 0, "1 declared-breaking changeset(s), each carrying an ADR-0087 disposition". (Round 0 had it falsified by P2's advisory.)\n"},
{
"old_str": "## Verification\n\n",
"new_str": "## Verification\n\n### Patch round 1 (head
a0c24ab647)\n\n- One commit on8a295d8477(no merge: no file this PR touches changed onmain, now7c7741dcb4).\n- Consumer sweep, every package whose tests save or publish through the gate (git grep -l \"publishPackageDrafts\\|saveMetaItem\" -- \"packages/**/*.test.ts\"). Each ranpnpm --filter PKG test --maxWorkers=2underos-verify-lock.sh, after the dependency closures were rebuilt:\n -@objectstack/lint148 files / 6934 tests passed;\n -@objectstack/metadata-protocol248 passed, 3 skipped / 20733 passed, 19 skipped;\n -@objectstack/objectql445 passed, 2 failed / 8407 passed, 7 failed on the first run. 6 wereprotocol-duplicate-package-names-in-step.test.ts(re-trued below: 6/6), and 1 isprotocol-duplicate-package-unprefixed-object.test.ts"a name already claimed…", left red and raised as an open question (below). Ata0c24ab647the conformance, names-in-step and unprefixed files read 25 passed, 1 failed (that one);\n -@objectstack/rest300 / 6095 passed;@objectstack/runtime423 / 6655 passed;@objectstack/core106 / 2580;@objectstack/metadata63 / 892;@objectstack/platform-objects78 / 1188;@objectstack/plugin-security188 / 3754;@objectstack/service-automation192 / 2567;@objectstack/service-cluster7 / 105;@objectstack/service-datasource45 / 838;@objectstack/cli(both projects) 474 passed, 1 skipped / 6018 passed;\n -@objectstack/dogfood: the 7 files the grep names plus the re-truedview-container-cross-package-default.dogfood.test.ts, 8 files / 51 tests passed. The rest of the dogfood suite was not run locally (main's full battery runs it); agit grepfor package-scoped view writes (meta/view…?package=) inpackages/qa/dogfood/testnames only the re-trued file;\n -@objectstack/specwas not run: it has no workspace dependency (pg-connection-string,zod), so no spec test can reach@objectstack/lint. Its grep hits are prose.\n- Typechecks:lint(withcheck:test-typecheck),metadata-protocol,objectqlanddogfoodexit 0.\n- Ablation (the seat's ask), on the patch with the severity set back:severity: 'error'becameseverity: String(object) === 'ablated_24563_warning' ? 'error' : 'warning'throughscripts/ablation-replace.mjs(anchor 1 → 0, blobf38af8f08988→377fdb85d3dd), lint rebuilt, marker in 4 built files.\n - Lint file: 4 red, 23 green. The reds were the three refusal cases and the object-door refusal inside the flow case. The CLI case stayed green because it checks the rule id only.\n - Protocol block: 5 red ((a) ×2, (d), (e), (g)), 5 green (the draft save, (b) ×2, (c), (f)). Exactly the refusal legs went red; every control held.\n - Restore: blob == HEAD,git diff HEADempty, rebuild,--absentfrom all 20 built files, tree clean; lint file 27/27, protocol file 162/162.\n - The ablation ran on the patch's lint source (blobf38af8f08988, unchanged ina0c24ab647) before the fixture files were folded into the same commit.\n\n### Round 0\n\n"},
{
"old_str": "## Gates\n\nThese were run on the merge head
8a295d8477, which is the PR head.\n","new_str": "## Gates\n\n### Patch round 1 (head
a0c24ab647)\n\n-node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, no paths: 95 families from 12 changed paths against merge basec3954057f. Each ran verbatim with its exit code recorded before any pipe.\n ---ran: "95 derived famil(ies) accounted for — 94 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit 3)", exit 0.\n - The derivation warned STALE TREE: 3 derivation inputs (scripts/check-stack-collection-maps.mjs,scripts/engine-double-contract.pinned.json,scripts/platform-object-tenancy-census.json) changed onmainsince the merge base. None is a file this PR touches, so the branch was not re-merged, per the patch instruction. CI judges the merge ref.\n- First runs that were not 0, and their re-runs:\n -check-engine-split-ratio --days 90exited 2, the shallow-clone refusal. Aftergit fetch --shallow-since=2026-07-14 origin mainit exited 0;\n -check:skill-examplesexited 3 (client-react had no.d.ts). After buildingclientandclient-reactit exited 0 ("262 prose examples type-check");\n -check:dual-build-cjs-loadsexited 3 (PREREQUISITE NOT MET) and is NOT MEASURED, left to CI.\n-check-adr-0087-registration --base origin/mainexit 0 ("1 declared-breaking changeset(s), each carrying an ADR-0087 disposition");check-empty-changeset --base origin/mainexit 0;check-changeset-no-major --base origin/mainexit 0 ("This diff introduces nomajorbump").\n-check:nul-bytes,check:cross-package-test-inputs,check:test-source-aliasandcheck:engine-double-contract: OK.\n- Not local:test:repoandpnpm lint(CI-owned).\n\n### Round 0\n\nThese were run on the merge head8a295d8477.\n"},
{
"old_str": "-
content/docs/deployment/validating-metadata.mdx, one sentence. Its "That changes what a rule can resolve, never what it judges" became false for this rule, so it now reads "never which rules run" and names the one rule that is told the closure is in force.","new_str": "-
content/docs/deployment/validating-metadata.mdx, one sentence. Its "That changes what a rule can resolve, never what it judges" became false for this rule, so it now reads "never which rules run" and says a list view bound outside the closure is refused.\n\n## Fixtures re-trued (patch round 1), assertions unchanged\n\nEach fixture saved or published a package's view bound outside that package's closure. Round 0 gave each an advisory: the names-in-step and dogfood cases passed, and the conformance file's advisory-free assertions went red (round 0's CI red). Under the refusal each write is refused. Each is re-trued so its view binds an object the package may reach, and no assertion is weakened.\n\n-packages/objectql/src/publish-package-drafts-response-conformance.test.ts(round 0's CI red,Test Core (5/6)). The views boundcase, which exists nowhere in that harness. Six cases were refused under B: plain batch, probes, materializer, advisories, and two invariants.byte-stabilitypassed only vacuously (an emptypublished[]).viewBodynow bindssys_metadata, a platform object the harness registers, with its declaredidcolumn. The view element stays advisory-free beside the flow that carries the advisory. 16/16.\n-packages/objectql/src/protocol-duplicate-package-names-in-step.test.ts. The source package saved views of a third package's objects (dupext_account.board,dupsrc_ext.board) without declaring it. The source now declaresdependencies: { [R]: '*' }. The objects stay outside the package, which is what "a view item of an object outside the package keeps its name" pins, and the copy inherits the declaration with the manifest. 6/6.\n-packages/qa/dogfood/test/view-container-cross-package-default.dogfood.test.ts. The Studio-createdcom.example.repairassetssaved containers forshowcase_taskwithout declaring the showcase. ItsPOST /packagesmanifest now declaresdependencies: { [SHOWCASE]: '*' }. The container is still for an object another package ships, which is the test's subject. 4/4, and without the declaration it reads422/refused(P1 above).\n\nThese are three more files outside the claim's surface: the engine lane (packages/objectql) andpackages/qa/dogfood. The seat adds them to the rider note on #6367.\n\nNot re-trued (open question in the report):packages/objectql/src/protocol-duplicate-package-unprefixed-object.test.ts, "a name already claimed is refused up front, at the object, naming the rename". There the SOURCE's view (ticket_views, bound to its ownticket) is inside its closure. The refused object copy leaves the COPY's view bound to the source'sticket, outside the copy's closure, and that copy is now refused:failed= theticketobject refusal plusview/ticket_views … views[0].object [list-view-object-outside-closure], against the asserted length 1. No fixture change re-trues it. It needs either the assertion re-pinned to the two refusals or aduplicatePackagechange (engine lane,protocol.ts), so it is red at this head."},
{
"old_str": "## Acceptance notes\n\n- A write that names no package and binds an object that exists nowhere is still accepted silently at the view door: overlay
nosuch_objanswers200withadvisories: [], pinned as unchanged here. A flow on the same overlay write getsflow-trigger-unknown-object. This PR keeps the overlay door as the dispatch ruled. The reach of this case was measured throughsaveMetaItemover a stub engine, not through a booted HTTP door.\n- The two door verdicts for an out-of-closure object reference disagree (P2: flow advisory, dataset refusal). This view follows the flow's.\n-list-view-object-outside-closurehas noos explainentry. The CLI never emits it, and the hint carries the remedy.\n- The first commit's subject readsfix(lint)!:. It was written before G3 was settled and is left as is (no force-push). The PR title is the squash subject.\n","new_str": "## Acceptance notes\n\n- A write that names no package and binds an object that exists nowhere is still accepted silently at the view door: overlay
nosuch_objanswers200withadvisories: [], pinned as unchanged here (out of scope by the seat's record). A flow on the same overlay write getsflow-trigger-unknown-object. The seat files it at adoption if its reach holds.\n- The three doors now give two verdicts for an out-of-closure object reference: flow advisory; dataset and view refused.\n-list-view-object-outside-closurehas noos explainentry. The CLI never emits it, and the hint carries the remedy.\n- An object written into a package whose built-inlistViewsentry retargetsdata.objectoutside the closure is refused too (pinned in the lint block's flow case). No shipped object retargets (P3).\n- Whether Studio's package editor can declaredependencieswas not measured.POST /packagesaccepts them (the dogfood fixture above).\n"}
],
"pr_title_edit": {
"old": "fix(lint): a list view written into a package and bound outside its closure is reported at the save door, not skipped",
"new": "fix(lint)!: the save door refuses a list view written into a package and bound outside its closure"
},
"open_questions": [
{
"question": "objectql protocol-duplicate-package-unprefixed-object.test.ts, 'a name already claimed is refused up front, at the object, naming the rename', asserts duplicatePackage reports exactly one failure. Under the refusal it reports two. The
ticketobject copy is refused by design, so the copy of the source's viewticket_viewsstays bound to the source'sticket, outside the copy package's closure, and is refused with list-view-object-outside-closure. The source fixture is inside its own closure, so no fixture change re-trues it. Per the addendum it is left red and not edited. Which way?","options": [
"A — re-pin the assertion to the two refusals: the
ticketobject refusal, then 'view/ticket_views' with list-view-object-outside-closure at views[0].object. Every other assertion stays, including 'everything else still copies'. Business need: a duplicate no longer lands a view in the copy bound to the SOURCE package's object; before, it landed with an advisory and showed the source's records. Long-term: the closure rule holds on the copy door as on every other door, with no exception. AI-safety: the partial copy is loud about the dependent view instead of shipping a cross-package binding. Startup focus: a test-only edit in one engine-lane file, which needs the #6367 rider.","B — change duplicatePackage so a view whose object copy was refused is reported as not attempted (skipped, naming the refused object) rather than gated. Business need: the same end state, with a sharper message. Long-term: a second policy in the copy door. AI-safety: equivalent. Startup focus: a protocol.ts change in the engine lane, blocked while PR #24599 is in flight; more scope.",
"C — exempt duplicatePackage's copies from the closure verdict. Rejected on every axis: it special-cases the rule (the patch instruction forbids it) and lets a copy carry exactly the binding the ruling refuses."
],
"recommendation": "A, because the refusal is the ruled behaviour on this door too. The test's own subject, the object refused up front and naming the rename, is unchanged, and the extra failure is an accurate consequence the test should pin rather than hide. It is one test-file edit; say the word and it rides this PR's next commit."
}
],
"out_of_scope_findings": [
"class: a · reach: save door, measured through saveMetaItem over the stub engine (not a booted HTTP door). An overlay write (no package) of a list view bound to 'nosuch_obj' answers success with advisories []. Unchanged here and pinned as unchanged; the seat's record keeps it out of scope and files it at adoption if its reach holds. · dedupe words: overlay list view nonexistent object accepted, view data.object unresolved save door, list-view skip 1 overlay",
"carrier: the seat's record 6273946447 (finding b) · noted, not filed: after this patch the flow door advises on an out-of-closure object reference while the dataset and view doors refuse it.",
"carrier: none · noted, not filed (Acceptance notes only): whether Studio's package editor can declare manifest dependencies was not measured; POST /packages accepts them (the re-trued dogfood fixture). Under the refusal that is the only way a Studio-created package can hold a view over another package's object."
]
}
Generated by Claude Code
test(objectql): duplicatePackage refuses the copied view whose object…
objectstack-fleet commentedon Oct 19, 2026
os-dev-report
{
"issue": 24563,
"status": "done",
"branch": "claude/issue-24563-list-view-out-of-closure",
"pr": "#24630",
"head": "483b3ec995165f3b5b7511d7f2052ea1b30780b1",
"session": "session_019oFY6Hi35J7atvgVFnmoaK — mode:subagent, the seat's id (claim 6271302258, amended 6273958360)",
"premise_still_valid": true,
"summary": "Patch round 2 is one commit, 483b3ec on a0c24ab, taking seat answer A (6277565064). In objectql protocol-duplicate-package-unprefixed-object.test.ts, the case 'a name already claimed is refused up front, at the object, naming the rename' now pins res.failed to exactly two entries, in order. The first is the
ticketobject refusal, with every existing assertion on it. The second is { type: 'view', name: 'ticket_views' }, whose error carries the locator 'views[0].object [list-view-object-outside-closure]' (rule id and path, not the message). A one-line comment says why the view is refused: the ticket copy was refused, so the copy keeps the source's ticket, outside the copy package's closure. The negative control and 'everything else still copies' are unchanged. The changeset's FROM and TO lines now name duplicatePackage beside saveMetaItem publish, the draft promotion and publishPackageDrafts, in the seat's wording. The docs sentence in validating-metadata.mdx lists no doors, so it is unchanged. files_changed (this round): packages/objectql/src/protocol-duplicate-package-unprefixed-object.test.ts, .changeset/24563-list-view-out-of-closure.md. deviations: (1) The changeset's FROM line also names duplicatePackage; the instruction named the TO line only. (2) The worktree was re-created on the existing branch; the earlier cleanup had removed it. (3) The branch was not re-merged. No file this PR touches changed on main (now cc95f5e), and dispatch-gates still warns STALE TREE for 3 derivation inputs. (4) The PR body was not written. pr_body_edits is ONE consolidated set of 18 old/new pairs against the LIVE body (still byte-identical to round 0), each old_str verified to occur exactly once. The set covers rounds 1 and 2 (Clause-② yes (narrowing), refused in place of reported/advised, duplicatePackage in the door list, the re-pin in place of the fixture left red, P1/P3 readings, and Verification and Gates for the final head). pr_title_edit is unchanged from round 1.","tests": "At 483b3ec, each run under os-verify-lock.sh after rebuilding the objectql dependency closure (BUILD exit 0): objectql protocol-duplicate-package-unprefixed-object.test.ts 'Tests 4 passed (4)'; pnpm --filter @objectstack/objectql test --maxWorkers=2 'Test Files 447 passed (447) / Tests 8414 passed (8414)', VERDICT command-exit 0; lint runtime-gate.package-closure.test.ts 'Tests 27 passed (27)'; metadata-protocol protocol.runtime-authoring-gate.test.ts 'Tests 162 passed (162)'. The rest of the consumer sweep was not re-run, per the instruction (only objectql's result changes); the round-1 sweep readings stand. CI confirms the red this round fixes: on a0c24ab, Test Core (5/6) failed on @objectstack/objectql src/protocol-duplicate-package-unprefixed-object.test.ts (job log read), the case re-pinned here. No ablation this round: the diff changes a test's assertions and changeset text, not tested behaviour.",
"gates": "At 483b3ec: check-adr-0087-registration --base origin/main exit 0 ('1 declared-breaking changeset(s), each carrying an ADR-0087 disposition'); check-changeset-no-major --base origin/main exit 0 ('This diff introduces no
majorbump'); check-empty-changeset --base origin/main exit 0, all captured before any pipe. node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, no paths: 95 families from 13 changed paths vs merge base c395405. That is identical to round 1's 95 (diff of the sorted lists empty), so round 2's paths add no family. All 95 were re-run verbatim on this head anyway. First non-zero runs, re-run after building their prerequisite: check:skill-examples exit 3 and check:lean-entry-closure exit 3 → built client, client-react and objectql → both exit 0. NOT MEASURED: check:dual-build-cjs-loads, exit 3 PREREQUISITE NOT MET (needs every package built), left to CI. --ran: '✓ dispatch-gates --ran: 95 derived famil(ies) accounted for — 94 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit 3)', exit 0. Not local: test:repo and pnpm lint (CI-owned). PR check-runs on 483b3ec at report time: 7 success, 5 skipped, 26 in_progress, 0 failure.","mcp_calls": "0",
"api_writes": "1 this round: one relay stroke, POST /repos/objectstack-ai/objectstack/dispatches executed as objectstack-fleet[bot] → comment, POST /repos//issues/24563/comments (this report). Plus 1 git push of the branch (a0c24ab..483b3ec, not REST). No PR body or title write, no label write, no rider note.",
"pr_body_edits": [
{
"old_str": "Fixes #24563\n\nClause-②: yes\n\n",
"new_str": "Fixes #24563\n\nClause-②: yes (narrowing)\n\n"
},
{
"old_str": "- A binding outside the closure is reported, never skipped. This covers an object of a package the writing package never declared, and an object that exists nowhere. It gets one
list-view-object-outside-closurefinding atwarning, located at the key the binding is read from (views[0].data.object). The hint names the remedy: declare a dependency on the package that defines the object in thedependenciesof the writing package's manifest. That is the verdict a flow's out-of-closure trigger object gets at the same door (P2), so the write is still stored and the finding rides the 2xxadvisories.","new_str": "- A binding outside the closure is refused, never skipped. This covers an object of a package the writing package never declared, and an object that exists nowhere. A publish-mode write (an active save, a draft promotion, a package draft publish) answers
422 INVALID_METADATAwith onelist-view-object-outside-closureissue aterror, located at the key the binding is read from (views[0].data.object), and nothing is stored. The hint names the remedy: declare a dependency on the package that defines the object in thedependenciesof the writing package's manifest. That is the verdict the dataset door already gives an out-of-closure base object under the same closure (P2). The flow door's advisory is unchanged.duplicatePackagewrites its copies through the same door. A copied list view whose bound object stays outside the copy package's closure is refused with the same rule, for example when that object's own copy was refused. Drafts stay ungated, and a stored row is judged at its next save."},
{
"old_str": "## Read first: the declaration differs from the claim\n\nThe claim (
6271302258) and the dispatch carryClause-②: no (narrowing). That line was written when the out-of-closure binding was expected to be refused. P2 measured a flow's out-of-closure object reference at advisory severity. The dispatch's own prohibition says that in that case "the view gets the same advisory for the binding, and the in-closure field refusal stays as it is". So this PR refuses nothing that was accepted before: no accept set narrows. The public surface does grow, by an optionalAuthoringRuleContext.packageScope, an optional second argument tovalidateListViewFieldRefs, and two new exports (LIST_VIEW_OBJECT_OUTSIDE_CLOSURE,ListViewFieldRefOptions). That isClause-②: yesby the definition "does this card widen the accept set or enlarge the public surface". So this body and the changeset declareClause-②: yesatminor, with no!, no FROM / TO lines and no ADR-0087 disposition. The claim's line is the seat's to correct, or this body's if the seat rules otherwise.\n\n","new_str": "## Patch rounds 1 and 2: refused, not advised\n\nThe seat answered the round-0 question with B (record
6273946447on this PR) and amended the claim's line toClause-②: yes (narrowing)(6273958360on #24563).list-view-object-outside-closureis now anerror, so the door narrows: the binding the round-0 head reported is refused. The public surface still grows by an optionalAuthoringRuleContext.packageScope, an optional second argument tovalidateListViewFieldRefs, and two exports (LIST_VIEW_OBJECT_OUTSIDE_CLOSURE,ListViewFieldRefOptions). Widen plus narrow isClause-②: yes (narrowing). The changeset isminorwith a!title, FROM / TO / Fix lines and the ADR-0087 dispositionnot-required (no-migration-prescription). Round 0's CI red (Test Core (5/6), the objectql publish-drafts conformance fixture) and the consumer sweep's fixtures are re-trued in the same commit; see "Fixtures re-trued". Patch round 2 takes the seat's answer A (6277565064). TheduplicatePackagecase whose copied view now stays outside the copy's closure is re-pinned to the two refusals, and the changeset's door list namesduplicatePackage.\n\n"},
{
"old_str": "a binding missing from
stack.objectsis reported once per list view, at the key it was read from","new_str": "a binding missing from
stack.objectsis refused once per list view (error), at the key it was read from"},
{
"old_str": " - out-of-closure binding reported,
nosuch_objreported, and reported under an empty closure;","new_str": " - out-of-closure binding refused,
nosuch_objrefused, and refused under an empty closure;"},
{
"old_str": " - the flow verdict unchanged, with a stored sibling's out-of-closure list view cancelling in the differential and reported when that object is itself written;",
"new_str": " - the flow verdict unchanged, with a stored sibling's out-of-closure list view cancelling in the differential (a flow publish is not refused for it) and refused when that object is itself written;"
},
{
"old_str": " - (a) the active save is reported, for the stranger's object and for
nosuch_obj;","new_str": " - (a) the active save is refused
422 INVALID_METADATAatviews[0].data.object, nothing stored, for the stranger's object and fornosuch_obj; the draft save of the same body is not gated;"},
{
"old_str": " - (d) draft promotion: reported, and the in-closure case refused;\n - (e) package draft publish: reported on
published[].advisories, and the in-closure case refused;\n - (f) a view bound to an object drafted in the same batch: no phantom report, and its fields judged.","new_str": " - (d) draft promotion: refused, nothing stored, and the in-closure case refused;\n - (e) package draft publish: refused (
failed[0],INVALID_METADATA, the closure issue), nothing stored, and the in-closure case refused;\n - (f) a view bound to an object drafted in the same batch: no phantom refusal, and its fields judged;\n - (g) a stored view with the binding loads (loaded: 1, errors: 0); its next save into the package is refused and the row is untouched."},
{
"old_str": "- Changeset:
.changeset/24563-list-view-out-of-closure.md,@objectstack/lintminor,Clause-②: yes.","new_str": "- Changeset:
.changeset/24563-list-view-out-of-closure.md,@objectstack/lintminor,!title,Clause-②: yes (narrowing), FROM / TO / Fix lines (FROM and TO namesaveMetaItempublish, the draft promotion,publishPackageDraftsandduplicatePackage), the stored-row sentence, and the ADR-0087 dispositionnot-required (no-migration-prescription)(check-adr-0087-registration --base origin/main: "1 declared-breaking changeset(s), each carrying an ADR-0087 disposition")."},
{
"old_str": "- After (this branch):\n - stranger's object:
RESOLVED success=true state=active advisories=[[\"list-view-object-outside-closure\",\"warning\",\"views[0].data.object\"]];\n -nosuch_obj: the same advisory;\n - overlay: unchanged (422, andnosuch_objthere200withadvisories=[]);\n -publishPackageDrafts:published=1withpublished[0].advisories=[[\"list-view-object-outside-closure\",\"warning\",\"views[0].data.object\"]];\n - in-closure controls: unchanged.","new_str": "- After (patch round 1,
a0c24ab647), through the protocol block on the same harness:\n - stranger's object: refused422 INVALID_METADATA,issues=[[\"list-view-object-outside-closure\",\"error\",\"views[0].data.object\"]], no active row;\n -nosuch_obj: the same refusal;\n - overlay: unchanged (422 list-view-field-unknown, andnosuch_objthere200withadvisories=[]);\n -publishPackageDrafts:success=false publishedCount=0,failed[0]INVALID_METADATAwith the closure issue;\n - in-closure controls: unchanged.\n- After, through the booted HTTP door: the dogfood fileview-container-cross-package-default.dogfood.test.ts(real showcase composition, REST) run with its Studio-created package declaring NO dependency answeredexpected 422 to be 200onPUT /api/v1/meta/view/os_qa_keyed_probe?package=com.example.repairassetsandexpected 'refused' to be 'published'onPOST /packages/com.example.repairassets/publish-drafts. With the dependency declared it passes 4/4.\n- (Round 0,8a295d8477, for the record: the same probe answeredRESOLVED success=truewith the finding atwarninginadvisories.)"},
{
"old_str": "So the door has two verdicts today for "an object outside the package's closure": flow advisory, dataset refusal. Per the dispatch the view takes the flow's. The question is raised in the report.",
"new_str": "So the door had two verdicts for "an object outside the package's closure": flow advisory, dataset refusal. Round 0 gave the view the flow's; patch round 1 gives it the dataset's (the seat's answer B). The flow door is unchanged."
},
{
"old_str": "- Control through the same harness: the CRM lead view retargeted at
showcase_task, undercom.example.crm, draws 3 advisories (listand its twolistViews) and 0 errors.","new_str": "- Control through the same harness: the CRM lead view retargeted at
showcase_task, undercom.example.crm, draws 3 advisories (listand its twolistViews) and 0 errors at round 0.\n- Re-run at patch round 1 (a0c24ab647, universe 119 objects after main addedsys_identity_email): 11 views, 39 bindings, 0 outside the closure, 0 refused, scoped or whole. The control now draws 3 errors (list-view-object-outside-closure×3) and 0 advisories."},
{
"old_str": " - its ladder answers
errorfor an unprefixed unresolved name. Package scope, out-of-closure:[\"error:object-reference-unknown@views[0].object\"]. That is a refusal, not the P2 verdict;","new_str": " - its ladder answers
errorfor an unprefixed unresolved name butwarningfor a platform-prefixed one, and its severity is not keyed to the closure. Package scope, out-of-closure:[\"error:object-reference-unknown@views[0].object\"];"},
{
"old_str": "- G3. Falsified in part, by P2. The out-of-closure binding is advisory and the in-closure refusal already existed, so no accept set narrows.\n - The changeset is
minoron the public-surface growth, withClause-②: yes.\n - There is no!, no FROM / TO, and no ADR-0087 marker:check-adr-0087-registration --base origin/mainreads no breaking declaration.\n - The Fix line is there: declare the dependency, or bind inside the closure, after which the field is judged.\n","new_str": "- G3. Held at patch round 1.
minor,!title,Clause-②: yes (narrowing), FROM / TO / Fix lines (the fix: declare the dependency on the defining package in the writing package's manifestdependencies, or bind an object it may reach), following PR #24509's changeset.check-adr-0087-registration --base origin/maindecidesnot-required (no-migration-prescription): exit 0, "1 declared-breaking changeset(s), each carrying an ADR-0087 disposition". (Round 0 had it falsified by P2's advisory.)\n"},
{
"old_str": "## Verification\n\n",
"new_str": "## Verification\n\n### Patch rounds 1 and 2 (final head
483b3ec995)\n\n- Two commits on8a295d8477:a0c24ab647(round 1) and483b3ec995(round 2: theduplicatePackagere-pin and the changeset door list). There is no merge, because no file this PR touches changed onmain(nowcc95f5e63e).\n- Round 2, at483b3ec995, each run underos-verify-lock.shafter rebuilding theobjectqldependency closure:\n -pnpm --filter @objectstack/objectql test --maxWorkers=2: 447 files / 8414 tests passed;\n -protocol-duplicate-package-unprefixed-object.test.ts: 4/4;\n - lintruntime-gate.package-closure.test.ts: 27/27;\n - metadata-protocolprotocol.runtime-authoring-gate.test.ts: 162/162.\n - Only objectql's result changes in round 2, so the rest of the sweep was not re-run (the seat's instruction).\n- Round 1 consumer sweep, at thea0c24ab647tree. It covered every package whose tests save or publish through the gate (git grep -l \"publishPackageDrafts\\|saveMetaItem\" -- \"packages/**/*.test.ts\"). Each ranpnpm --filter PKG test --maxWorkers=2underos-verify-lock.sh:\n -@objectstack/lint148 files / 6934 tests passed;\n -@objectstack/metadata-protocol248 passed, 3 skipped / 20733 passed, 19 skipped;\n -@objectstack/objectql445 passed, 2 failed / 8407 passed, 7 failed. 6 wereprotocol-duplicate-package-names-in-step.test.ts(re-trued, 6/6) and 1 was theduplicatePackagecase re-pinned in round 2;\n -@objectstack/rest300 / 6095 passed;@objectstack/runtime423 / 6655 passed;@objectstack/core106 / 2580;@objectstack/metadata63 / 892;@objectstack/platform-objects78 / 1188;@objectstack/plugin-security188 / 3754;@objectstack/service-automation192 / 2567;@objectstack/service-cluster7 / 105;@objectstack/service-datasource45 / 838;@objectstack/cli(both projects) 474 passed, 1 skipped / 6018 passed;\n -@objectstack/dogfood: the 7 files the grep names plus the re-truedview-container-cross-package-default.dogfood.test.ts, 8 files / 51 tests passed. The rest of the dogfood suite was not run locally (main's full battery runs it). Agit grepfor package-scoped view writes (meta/view…?package=) inpackages/qa/dogfood/testnames only the re-trued file;\n -@objectstack/specwas not run: it has no workspace dependency (pg-connection-string,zod), so no spec test can reach@objectstack/lint, and its grep hits are prose.\n- Typechecks (round 1):lint(withcheck:test-typecheck),metadata-protocol,objectqlanddogfoodexit 0.\n- Ablation (round 1, the seat's ask): the severity was set back towarning.severity: 'error'becameseverity: String(object) === 'ablated_24563_warning' ? 'error' : 'warning'throughscripts/ablation-replace.mjs(anchor 1 → 0, blobf38af8f08988→377fdb85d3dd). Lint was rebuilt, and the marker was in 4 built files.\n - Lint file: 4 red, 23 green. The reds were the three refusal cases and the object-door refusal in the flow case. The CLI case checks the rule id only and stayed green.\n - Protocol block: 5 red ((a) ×2, (d), (e), (g)) and 5 green (the draft save, (b) ×2, (c), (f)). Exactly the refusal legs went red, and every control held.\n - Restore: blob == HEAD,git diff HEADempty, rebuild,--absentfrom all 20 built files, tree clean. The lint file passed 27/27 and the protocol file 162/162.\n - The lint source (blobf38af8f08988) is unchanged in483b3ec995.\n\n### Round 0\n\n"},
{
"old_str": "## Gates\n\nThese were run on the merge head
8a295d8477, which is the PR head.\n","new_str": "## Gates\n\n### Final head
483b3ec995\n\n-node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, no paths: 95 families from 13 changed paths against merge basec3954057f, the same 95 as round 1. Round 2's paths add none. All 95 were re-run verbatim on this head, with each exit code recorded before any pipe.\n ---ran: "95 derived famil(ies) accounted for — 94 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit 3)", exit 0.\n - The derivation warns STALE TREE: 3 derivation inputs (scripts/check-stack-collection-maps.mjs,scripts/engine-double-contract.pinned.json,scripts/platform-object-tenancy-census.json) changed onmainsince the merge base. None is a file this PR touches, so the branch was not re-merged, per the patch instruction. CI judges the merge ref.\n- First runs that were not 0, re-run after building their prerequisite:\n -check:skill-examples(client-react had no.d.ts) andcheck:lean-entry-closure(objectql/dist/coreabsent) each exited 3. After buildingclient,client-reactandobjectql, both exited 0.\n -check:dual-build-cjs-loadsexited 3 (PREREQUISITE NOT MET). It is NOT MEASURED and left to CI.\n - Round 1'scheck-engine-split-ratioshallow refusal is cleared by the deepened clone; it exits 0 here.\n-check-adr-0087-registration --base origin/mainexit 0 ("1 declared-breaking changeset(s), each carrying an ADR-0087 disposition");check-changeset-no-major --base origin/mainexit 0 ("This diff introduces nomajorbump");check-empty-changeset --base origin/mainexit 0.\n- Not local:test:repoandpnpm lint(CI-owned).\n\n### Round 0\n\nThese were run on the merge head8a295d8477.\n"},
{
"old_str": "-
content/docs/deployment/validating-metadata.mdx, one sentence. Its "That changes what a rule can resolve, never what it judges" became false for this rule, so it now reads "never which rules run" and names the one rule that is told the closure is in force.","new_str": "-
content/docs/deployment/validating-metadata.mdx, one sentence. Its "That changes what a rule can resolve, never what it judges" became false for this rule, so it now reads "never which rules run" and says a list view bound outside the closure is refused.\n\n## Fixtures re-trued (patch round 1), assertions unchanged; one case re-pinned (patch round 2)\n\nEach fixture saved or published a package's view bound outside that package's closure. Round 0 gave each an advisory: the names-in-step and dogfood cases passed, and the conformance file's advisory-free assertions went red (round 0's CI red). Under the refusal each write is refused. Each is re-trued so its view binds an object the package may reach, and no assertion is weakened.\n\n-packages/objectql/src/publish-package-drafts-response-conformance.test.ts(round 0's CI red,Test Core (5/6)). The views boundcase, which exists nowhere in that harness. Six cases were refused under B: plain batch, probes, materializer, advisories, and two invariants.byte-stabilitypassed only vacuously (an emptypublished[]).viewBodynow bindssys_metadata, a platform object the harness registers, with its declaredidcolumn. The view element stays advisory-free beside the flow that carries the advisory. 16/16.\n-packages/objectql/src/protocol-duplicate-package-names-in-step.test.ts. The source package saved views of a third package's objects (dupext_account.board,dupsrc_ext.board) without declaring it. The source now declaresdependencies: { [R]: '*' }. The objects stay outside the package, which is what "a view item of an object outside the package keeps its name" pins, and the copy inherits the declaration with the manifest. 6/6.\n-packages/qa/dogfood/test/view-container-cross-package-default.dogfood.test.ts. The Studio-createdcom.example.repairassetssaved containers forshowcase_taskwithout declaring the showcase. ItsPOST /packagesmanifest now declaresdependencies: { [SHOWCASE]: '*' }. The container is still for an object another package ships, which is the test's subject. 4/4, and without the declaration it reads422/refused(P1 above).\n\nThese are files outside the claim's surface: the engine lane (packages/objectql) andpackages/qa/dogfood.\n\nRe-pinned (patch round 2, seat answer A6277565064):packages/objectql/src/protocol-duplicate-package-unprefixed-object.test.ts, case "a name already claimed is refused up front, at the object, naming the rename". The source's viewticket_viewsis inside its own closure. Because theticketcopy is refused, the copy of that view keeps the source'sticket, which is outside the copy package's closure, so the copy is refused with the same rule.\n-res.failedis pinned to exactly two entries, in order: theticketobject refusal, with every existing assertion on it; then{ type: 'view', name: 'ticket_views' }, whoseerrorcarriesviews[0].object [list-view-object-outside-closure](the rule id and path, not the message).\n- The negative control and "everything else still copies" are unchanged, and a one-line comment says why the view is refused. 4/4.\n\nThe riders are declared by the seat: the three objectql test files on #6367 (6277575090), and the dogfood test on #6024 (6277594189)."},
{
"old_str": "## Acceptance notes\n\n- A write that names no package and binds an object that exists nowhere is still accepted silently at the view door: overlay
nosuch_objanswers200withadvisories: [], pinned as unchanged here. A flow on the same overlay write getsflow-trigger-unknown-object. This PR keeps the overlay door as the dispatch ruled. The reach of this case was measured throughsaveMetaItemover a stub engine, not through a booted HTTP door.\n- The two door verdicts for an out-of-closure object reference disagree (P2: flow advisory, dataset refusal). This view follows the flow's.\n-list-view-object-outside-closurehas noos explainentry. The CLI never emits it, and the hint carries the remedy.\n- The first commit's subject readsfix(lint)!:. It was written before G3 was settled and is left as is (no force-push). The PR title is the squash subject.\n","new_str": "## Acceptance notes\n\n- A write that names no package and binds an object that exists nowhere is still accepted silently at the view door: overlay
nosuch_objanswers200withadvisories: [], pinned as unchanged here (out of scope by the seat's record). A flow on the same overlay write getsflow-trigger-unknown-object. The seat files it at adoption if its reach holds.\n- The three doors now give two verdicts for an out-of-closure object reference: flow advisory; dataset and view refused.\n-list-view-object-outside-closurehas noos explainentry. The CLI never emits it, and the hint carries the remedy.\n- An object written into a package whose built-inlistViewsentry retargetsdata.objectoutside the closure is refused too (pinned in the lint block's flow case). No shipped object retargets (P3).\n- Whether Studio's package editor can declaredependencieswas not measured.POST /packagesaccepts them (the dogfood fixture above).\n"}
],
"pr_title_edit": {
"old": "fix(lint): a list view written into a package and bound outside its closure is reported at the save door, not skipped",
"new": "fix(lint)!: the save door refuses a list view written into a package and bound outside its closure"
},
"open_questions": [],
"out_of_scope_findings": [
"class: a · reach: save door, measured through saveMetaItem over the stub engine (not a booted HTTP door). An overlay write (no package) of a list view bound to 'nosuch_obj' answers success with advisories []. Unchanged and pinned as unchanged; the seat's record keeps it out of scope and files it at adoption if its reach holds. · dedupe words: overlay list view nonexistent object accepted, view data.object unresolved save door, list-view skip 1 overlay",
"carrier: the seat's record 6273946447 (finding b) · noted, not filed: the flow door advises on an out-of-closure object reference while the dataset and view doors refuse it.",
"carrier: none · noted, not filed (Acceptance notes only): whether Studio's package editor can declare manifest dependencies was not measured; POST /packages accepts them."
]
}
Generated by Claude Code
docs(changeset): the pre-mode parenthetical reads exit mode, as .chan…
objectstack-fleet commentedon Oct 19, 2026
os-dev-report
{
"issue": 24563,
"status": "done",
"branch": "claude/issue-24563-list-view-out-of-closure",
"pr": "#24630",
"head": "700c21cbbd4a1bfd7af1c6ce1872ecb578fa4821",
"session": "session_019oFY6Hi35J7atvgVFnmoaK — mode:subagent, the seat's id (claim 6271302258, amended 6273958360)",
"premise_still_valid": true,
"summary": "One commit, 700c21c on 483b3ec. In .changeset/24563-list-view-out-of-closure.md, the BREAKING paragraph's parenthetical '(
.changeset/pre.jsonis open innextpre mode)' becomes '(.changeset/pre.jsonis inexitmode, so the next version pass cuts 18.0.0 and this change ships in it)'. The rest of that sentence and the rest of the file are unchanged (diff: 1 insertion, 1 deletion). The reading was measured first:git show origin/main:.changeset/pre.jsonreads {"mode": "exit", "tag": "next"} at origin/main cc95f5e, andgit show 483b3ec995:.changeset/pre.jsonreads the same. c46b5f1 (#24555) is an ancestor of both (merge-base --is-ancestor exit 0 on each, a self-proving reading). files_changed (this round): .changeset/24563-list-view-out-of-closure.md. deviations: (1) The worktree was re-created on the existing branch and neededpnpm install. The first check-adr-0087-registration run exited 1 with 'no parser-rot witness: the witness generator's runnertsxis not installed', a prerequisite and not a verdict; after the install it exited 0. (2) pr_body_edits is unchanged from round 2. Its Verification and Gates sections name 483b3ec as the verified head; this round adds only the changeset-prose commit 700c21c on top, which no test or derived family reads differently.","tests": "No test changed this round (changeset prose only); round-2 readings at 483b3ec stand: objectql 447/8414 passed, lint pin 27/27, metadata-protocol pin 162/162. No ablation: no tested behaviour changed.",
"gates": "At 700c21c, each exit captured before any pipe: node scripts/check-adr-0087-registration.mjs --base origin/main exit 0 ('✓ check-adr-0087-registration: 1 declared-breaking changeset(s), each carrying an ADR-0087 disposition.'); node scripts/check-changeset-no-major.mjs --base origin/main exit 0 ('✓ This diff introduces no
majorbump.'); node scripts/check-empty-changeset.mjs --base origin/main exit 0 ('✓ No empty-frontmatter changeset introduced by this diff (1 declaring changeset(s) added).'); pnpm check:nul-bytes exit 0 before the push. dispatch-gates was not re-derived this round: the change is one prose line in a file already in round 2's change set. PR check-runs on 700c21c: not read yet (just pushed).","mcp_calls": "0",
"api_writes": "1 this round: one relay stroke, POST /repos/objectstack-ai/objectstack/dispatches executed as objectstack-fleet[bot] → comment, POST /repos//issues/24563/comments (this report). Plus 1 git push (483b3ec..700c21c, not REST). No PR body, title or label write.",
"pr_body_edits": "unchanged from round 2 — the live PR body (re-read in this act, byte-identical to round 0) does not quote the pre.json parenthetical (0 occurrences of 'pre.json' or 'pre mode'), and neither does any round-2 new_str; the round-2 set (report comment 6278185241, 18 pairs) applies as is.",
"pr_title_edit": {
"old": "fix(lint): a list view written into a package and bound outside its closure is reported at the save door, not skipped",
"new": "fix(lint)!: the save door refuses a list view written into a package and bound outside its closure"
},
"open_questions": [],
"out_of_scope_findings": []
}
Generated by Claude Code
objectstack-fleet commentedon Oct 19, 2026
Landed ·
domain:specseat 2 · sessionsession_019oFY6Hi35J7atvgVFnmoaK· 2026-10-19T23:03Z9f12d06770a27075e90e9e0fbbc5e23283c2b57donmain.Fixes #24563closed this card ascompleted.origin/main.git patch-id --stableof the merge's diff equals that of the reviewed head700c21cbbdagainst its merge base (440538a1c8). What landed is what the contract review6278531343passed.gh-readonly-queue/main/pr-24630-*is gone.pm:dispatchedwas removed in this act.domain:spec,area:records,priority:p1andtarget:v18stay.422,list-view-object-outside-closure) a publish-mode write of a package's list view bound outside its closure. The doors aresaveMetaItempublish, draft promotion,publishPackageDraftsandduplicatePackage(ruling B6273946447, answer A6277565064). Overlay writes, the CLI and drafts are unchanged.6271671580,6277575090) and the cli rider on [PM seat] domain:cli — 🟢 zhuangjianguo · session_01SYEJqDL3NXxrJRBVqaMu9T #6024 (6277594189) landed with it.validateListViewFieldRefsskips it and no other rule judges aviewwrite's binding #24668, lint: under a package closure, an out-of-closure object reference is refused at the dataset and view doors but only advised at the flow door (flow-trigger-unknown-objectiswarningand not closure-keyed) — one policy across three doors? #24669 and packages: manifestdependenciescan be set only at create —PATCH /api/v1/packages/:idpersists only name / description / version, so a save refused with "declare a dependency" has no remedy short of recreating the package #24670 (adopt record6278769509).mainpublishes nothing.@objectstack/lintminorrides the next Version Packages PR, which is a human act.Generated by Claude Code