Skip to content

fix(lint)!: the save door refuses a list view written into a package and bound outside its closure - #24630

Merged
objectstack-fleet[bot] merged 8 commits into
mainfrom
claude/issue-24563-list-view-out-of-closure
Oct 19, 2026
Merged

objectstack-fleet[bot] merged 8 commits into
mainfrom
claude/issue-24563-list-view-out-of-closure

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #24563

Clause-②: yes (narrowing)

At the runtime save door, a list view written into a package is now judged against that package's closure (ruling #9612: the package, its declared dependencies, platform/system objects, and provenance-less overlay rows) instead of being skipped:

  • 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_METADATA with one list-view-object-outside-closure issue at error, 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 the dependencies of 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. duplicatePackage writes 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.
  • A binding inside the closure keeps its field verdict. An unknown columns field is refused 422 list-view-field-unknown, as the overlay door refuses it (unchanged, now pinned at both layers).
  • A write that names no package, and the CLI commands, are unchanged.

Patch rounds 1 and 2: refused, not advised

The seat answered the round-0 question with B (record 6273946447 on this PR) and amended the claim's line to Clause-②: yes (narrowing) (6273958360 on #24563). list-view-object-outside-closure is now an error, so the door narrows: the binding the round-0 head reported is refused. The public surface still grows by an optional AuthoringRuleContext.packageScope, an optional second argument to validateListViewFieldRefs, and two exports (LIST_VIEW_OBJECT_OUTSIDE_CLOSURE, ListViewFieldRefOptions). Widen plus narrow is Clause-②: yes (narrowing). The changeset is minor with a ! title, FROM / TO / Fix lines and the ADR-0087 disposition not-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). The duplicatePackage case whose copied view now stays outside the copy's closure is re-pinned to the two refusals, and the changeset's door list names duplicatePackage.

What changes

  • packages/lint/src/validate-list-view-field-refs.ts. Skip 1 (:818) splits in two:

    • when the caller states a package closure (ListViewFieldRefOptions.packageScope), a binding missing from stack.objects is refused once per list view (error), at the key it was read from (data.object, else the container's objectName / object, else the object's name). The positions are not judged against anything else;
    • with no closure stated, the skip is unchanged. Skip 2 (an object that is present but has no readable field map) is unchanged under a closure too.

    An empty graph under a closure is still judged, because every binding is then outside it. The module header gains the section that explains this, and its "does NOT own" note names the one exception.

  • packages/lint/src/runtime-gate.ts. runRuntimeAuthoringRules puts packageScope on the rule context exactly when it narrowed the snapshots. One predicate, packageScopeInForce, now decides both that and narrowObjectsToPackageClosure's early return. The four limbs are untouched. The guard's behaviour is byte-identical, and the existing "narrows NOTHING when no scope / an empty package id" pins hold it.

  • packages/lint/src/authoring-rules.ts (interface only): AuthoringRuleContext.packageScope?, documented as set only by the runtime gate and read only by this rule.

  • packages/lint/src/reference-integrity-suite.ts. ReferenceIntegrityRunOptions.packageScope?. The validateListViewFieldRefs member hands the scope to the rule. Its runtimeTypes are unchanged (['flow', 'view', 'object']). validateObjectReferences is unchanged (['flow', 'dataset'], :224).

  • packages/lint/src/index.ts. Exports the new rule id and options type (rule-id-barrel-exports.test.ts requires the id).

  • Pins.

    • Lint level: a block in runtime-gate.package-closure.test.ts, through runRuntimeAuthoringRules, covering:

      • out-of-closure binding refused, nosuch_obj refused, and refused under an empty closure;
      • in-closure unknown field refused, in-closure known field admitted;
      • declared dependency admitted (known field) and refused (unknown field);
      • system object by prefix and by isSystem, and a provenance-less overlay object, each keeping its field verdict;
      • the no-package write unchanged (the stranger column still refused, nosuch_obj still silent, an empty package id narrows nothing);
      • 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;
      • the CLI reading unchanged.
    • Protocol level: the engine-lane rider declared on [PM seat] domain:engine — 🟢 os-project-manager #6367 (6271671580). It is a block in packages/metadata-protocol/src/protocol.runtime-authoring-gate.test.ts, through the real saveMetaItem, publishMetaItem and publishPackageDrafts, over a registry with three packages and a platform object:

      • (a) the active save is refused 422 INVALID_METADATA at views[0].data.object, nothing stored, for the stranger's object and for nosuch_obj; the draft save of the same body is not gated;
      • (b) in-closure unknown columns field refused 422 at views[0].columns[1].field and nothing stored, and the known field saves clean, for own, declared-dependency and platform objects;
      • (c) the overlay door is unchanged;
      • (d) draft promotion: refused, nothing stored, and the in-closure case refused;
      • (e) package draft publish: refused (failed[0], INVALID_METADATA, the closure issue), nothing stored, and the in-closure case refused;
      • (f) a view bound to an object drafted in the same batch: no phantom refusal, and its fields judged;
      • (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.

      Test file only. packages/metadata-protocol/src/protocol.ts is untouched (PR fix(spec,metadata-protocol)!: the /meta door no longer authors sharing rules; Setup lists declared sharing rules #24599).

  • Changeset: .changeset/24563-list-view-out-of-closure.md, @objectstack/lint minor, ! title, Clause-②: yes (narrowing), FROM / TO / Fix lines (FROM and TO name saveMetaItem publish, the draft promotion, publishPackageDrafts and duplicatePackage), the stored-row sentence, and the ADR-0087 disposition not-required (no-migration-prescription) (check-adr-0087-registration --base origin/main: "1 declared-breaking changeset(s), each carrying an ADR-0087 disposition").

  • Docs: one sentence in content/docs/deployment/validating-metadata.mdx (see "File surface beyond the claim").

Premises (measured on this branch)

P1. Holds. The probe was the real saveMetaItem / publishPackageDrafts over the protocol.package-closure-gate.test.ts stub-engine shape: a registry holding com.acme.crm (declares com.acme.core), com.acme.core, a stranger com.other.analytics, and a platform object. The body is the card's, a flattened list overlay with columns: [title, zzz_nope]. It ran before and after through the same probe; the HTTP door was not booted.

  • Before (origin/main 5a871e4e41, lint built from it):
    • written into com.acme.crm, bound to the stranger's object: RESOLVED success=true state=active advisories=[];
    • bound to nosuch_obj: RESOLVED success=true state=active advisories=[];
    • overlay (no package), stranger's object: REFUSED 422 INVALID_METADATA issues=[["list-view-field-unknown","error","views[0].columns[1].field"]];
    • draft → publishPackageDrafts: outcome=published published=1 failed=[] probes={"issues":[],...}.
    • The in-closure controls (own, declared dependency, platform) were already refused 422 list-view-field-unknown.
  • After (patch round 1, a0c24ab647), through the protocol block on the same harness:
    • stranger's object: refused 422 INVALID_METADATA, issues=[["list-view-object-outside-closure","error","views[0].data.object"]], no active row;
    • nosuch_obj: the same refusal;
    • overlay: unchanged (422 list-view-field-unknown, and nosuch_obj there 200 with advisories=[]);
    • publishPackageDrafts: success=false publishedCount=0, failed[0] INVALID_METADATA with the closure issue;
    • in-closure controls: unchanged.
  • After, through the booted HTTP door: the dogfood file view-container-cross-package-default.dogfood.test.ts (real showcase composition, REST) run with its Studio-created package declaring NO dependency answered expected 422 to be 200 on PUT /api/v1/meta/view/os_qa_keyed_probe?package=com.example.repairassets and expected 'refused' to be 'published' on POST /packages/com.example.repairassets/publish-drafts. With the dependency declared it passes 4/4.
  • (Round 0, 8a295d8477, for the record: the same probe answered RESOLVED success=true with the finding at warning in advisories.)
  • The protocol block run against the ablated rule (the skip restored, below) is the same "before" through the pinned door: 4 red with expected [] to deeply equal [ 'list-view-object-outside-closure' ].

P2. Holds: advisory.

  • Rule validateFlowTriggerReadiness, issue code flow-trigger-unknown-object, severity warning.
  • Produced at packages/lint/src/validate-flow-trigger-readiness.ts:446-449, the record-trigger branch: objectName && !objectNames.has(objectName) && !objectName.startsWith('sys_') with severity: 'warning'. The timeRelative twin is at :498-501.
  • It reaches the door through authoring-rules.ts:1365 (surfaces: CLI_AND_RUNTIME, runtimeTypes: ['flow']).
  • Pinned at the lint level at runtime-gate.package-closure.test.ts:246 ("Measured severity here is advisory (warning), so it reports rather than refuses"), and at the door at protocol.package-closure-gate.test.ts:211 (['flow-trigger-unknown-object'], success: true).
  • One more reading the seat should see. The dataset door does not agree. validateObjectReferences runs on dataset writes, and under the same closure it REFUSES an out-of-closure base object: errors=["error:object-reference-unknown@datasets.crm_ds.object"], and [] for the same body with no package. 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.

P3. Holds. No shipped view binds an object outside its package's closure. The door's view rule set (validatePresetComparands, validateReferenceIntegrity, validateVisibilityPredicates, validatePredicatePathRefs) was run over every shipped view, with its package's scope and without, before and after this change.

  • Universe: every package's objects stamped with its owner, plus all 83 platform objects from every *.object.ts under packages/ outside spec. All 83 are system objects (isSystemObject), so 118 objects in all.
  • Packages and their manifest dependencies:
    • examples/app-crm (composed stack): com.example.crm, none, 3 views;
    • examples/app-todo (composed stack): com.example.todo, none, 1 view;
    • examples/app-showcase (the barrels the config imports, because the composed config imports unbuilt connectors): com.example.showcase, none, 6 views;
    • examples/app-multi-package: com.example.multi.core (none) and com.example.multi.orders (com.example.multi.core), 0 views;
    • the create-objectstack blank template (its barrels): 0 views;
    • packages/qa/downstream-contract (ContractStack): com.objectstack.downstream-contract, none, 1 view.
  • 11 views, 39 list-view bindings: 0 outside the closure, and 0 findings scoped or whole, before and after. Every example object written as an object under its package's scope: 0 list-view findings.
  • The platform's 97 built-in list views: none retargets data.object.
  • Control through the same harness: the CRM lead view retargeted at showcase_task, under com.example.crm, draws 3 advisories (list and its two listViews) and 0 errors at round 0.
  • Re-run at patch round 1 (a0c24ab647, universe 119 objects after main added sys_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.

Mechanism assumptions

  • G1. The list-view rule reports the binding itself, scoped by a stated closure. Adding 'view' to validateObjectReferences was measured and rejected, for three reasons:

    • its ladder answers error for an unprefixed unresolved name but warning for 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"];
    • it reads only the container's own views[].object. A body binding through data.object alone answered [];
    • it would refuse an overlay write bound to an object that exists nowhere (["error:object-reference-unknown@views[0].object"] with no package; today 200), which changes the overlay door.

    Reporting only under a stated closure keeps the overlay door and the CLI byte-identical. The only channel that tells a rule a closure is in force is the rule context, hence packageScope.

  • G2. Held. Both layers are pinned (above). There was no protocol.ts change.

  • 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 manifest dependencies, or bind an object it may reach), following PR feat(lint)!: the visibility-predicate family runs on the page save door (ruling B on #23388) #24509's changeset. check-adr-0087-registration --base origin/main decides not-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.)

  • G4. Main moved 5a871e4e41 → c3954057fe (5 commits, none in lint or metadata-protocol; spec moved). It was merged with bash scripts/pm/os-regen-merge.sh: steps 1–3, with no additional changes. Step 4: the metadata-protocol dependency closure was rebuilt, and pnpm --filter @objectstack/spec check:generated reports all 14 generated artifacts up to date. This branch edits no generated artifact.

Verification

Patch rounds 1 to 3 (final head 700c21cbbd)

  • Three commits on 8a295d8477: a0c24ab647 (round 1), 483b3ec995 (round 2: the duplicatePackage re-pin and the changeset door list), and 700c21cbbd (round 3: one changeset prose line). Round 3 replaces the parenthetical ".changeset/pre.json is open in next pre mode" with ".changeset/pre.json is in exit mode, so the next version pass cuts 18.0.0 and this change ships in it", measured on main since c46b5f14cf (chore(release): exit Changesets pre mode (next), so the next version pass cuts the 18.0.0 GA #24555). There is no merge, because no file this PR touches changed on main (now cc95f5e63e).
  • Round 2, at 483b3ec995, each run under os-verify-lock.sh after rebuilding the objectql dependency closure:
    • pnpm --filter @objectstack/objectql test --maxWorkers=2: 447 files / 8414 tests passed;
    • protocol-duplicate-package-unprefixed-object.test.ts: 4/4;
    • lint runtime-gate.package-closure.test.ts: 27/27;
    • metadata-protocol protocol.runtime-authoring-gate.test.ts: 162/162.
    • Only objectql's result changes in round 2, so the rest of the sweep was not re-run (the seat's instruction).
  • Round 1 consumer sweep, at the a0c24ab647 tree. It covered every package whose tests save or publish through the gate (git grep -l "publishPackageDrafts\|saveMetaItem" -- "packages/**/*.test.ts"). Each ran pnpm --filter PKG test --maxWorkers=2 under os-verify-lock.sh:
    • @objectstack/lint 148 files / 6934 tests passed;
    • @objectstack/metadata-protocol 248 passed, 3 skipped / 20733 passed, 19 skipped;
    • @objectstack/objectql 445 passed, 2 failed / 8407 passed, 7 failed. 6 were protocol-duplicate-package-names-in-step.test.ts (re-trued, 6/6) and 1 was the duplicatePackage case re-pinned in round 2;
    • @objectstack/rest 300 / 6095 passed; @objectstack/runtime 423 / 6655 passed; @objectstack/core 106 / 2580; @objectstack/metadata 63 / 892; @objectstack/platform-objects 78 / 1188; @objectstack/plugin-security 188 / 3754; @objectstack/service-automation 192 / 2567; @objectstack/service-cluster 7 / 105; @objectstack/service-datasource 45 / 838; @objectstack/cli (both projects) 474 passed, 1 skipped / 6018 passed;
    • @objectstack/dogfood: the 7 files the grep names plus the re-trued view-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). A git grep for package-scoped view writes (meta/view…?package=) in packages/qa/dogfood/test names only the re-trued file;
    • @objectstack/spec was 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.
  • Typechecks (round 1): lint (with check:test-typecheck), metadata-protocol, objectql and dogfood exit 0.
  • Ablation (round 1, the seat's ask): the severity was set back to warning. severity: 'error' became severity: String(object) === 'ablated_24563_warning' ? 'error' : 'warning' through scripts/ablation-replace.mjs (anchor 1 → 0, blob f38af8f08988 → 377fdb85d3dd). Lint was rebuilt, and the marker was in 4 built files.
    • 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.
    • 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.
    • Restore: blob == HEAD, git diff HEAD empty, rebuild, --absent from all 20 built files, tree clean. The lint file passed 27/27 and the protocol file 162/162.
    • The lint source (blob f38af8f08988) is unchanged in 483b3ec995.

Round 0

  • Pre-merge head cab356c0ce (the same code as fe5bafb2be, which added only the changeset and the docs sentence):
    • pnpm --filter @objectstack/metadata-protocol exec vitest run --maxWorkers=2: 248 files passed (3 skipped), 20731 tests passed (19 skipped). typecheck passed.
  • Merge head 8a295d8477:
    • pnpm --filter @objectstack/lint exec vitest run --maxWorkers=2: 148 files, 6934 tests passed. typecheck (with check:test-typecheck) passed.
    • metadata-protocol: protocol.runtime-authoring-gate.test.ts, protocol.package-closure-gate.test.ts and protocol-publish-drafts-closure.test.ts passed 175/175. typecheck passed. The merge brought no metadata-protocol or lint change.
  • Ablation, after the fix was committed (fe5bafb2be):
    • Mutation, through scripts/ablation-replace.mjs: if (scope) reportOutsideClosure(...) became if (scope && String(object) === 'ablated_24563_no_report') reportOutsideClosure(...). Anchor 1 → 0, blob df4ce7aa8c22 → 7340a202512b. Lint was rebuilt, and ablation-dist-preflight found the marker in 4 built files.
    • Mutated: in the lint file, 5 red and 22 green. The reds were the three report cases, the object-door assertion in the flow case, and the scoped half of the CLI case. The field-verdict and no-package cases stayed green. The protocol block had 4 red ((a) ×2, (d), (e)) and 4 green ((b) ×2, (c), (f)).
    • Restore leg: blob equal to the HEAD blob, git diff HEAD empty, rebuild, then --absent: absent from all 20 built files, tree clean. The lint file passed 27/27 and the protocol file 160/160.
  • Consumer suites were not rerun. The public surface grows by optional members only, and no consumer passes the new argument.

Gates

Verified head 483b3ec995, and the final head 700c21cbbd

700c21cbbd changes one prose line in the changeset, and no test or derived family reads it differently. On it, check-adr-0087-registration, check-changeset-no-major and check-empty-changeset (each --base origin/main) exited 0.

  • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, no paths: 95 families from 13 changed paths against merge base c3954057f, 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.
    • --ran: "95 derived famil(ies) accounted for — 94 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit 3)", exit 0.
    • 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 on main since 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.
  • First runs that were not 0, re-run after building their prerequisite:
    • check:skill-examples (client-react had no .d.ts) and check:lean-entry-closure (objectql/dist/core absent) each exited 3. After building client, client-react and objectql, both exited 0.
    • check:dual-build-cjs-loads exited 3 (PREREQUISITE NOT MET). It is NOT MEASURED and left to CI.
    • Round 1's check-engine-split-ratio shallow refusal is cleared by the deepened clone; it exits 0 here.
  • 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 major bump"); check-empty-changeset --base origin/main exit 0.
  • Not local: test:repo and pnpm lint (CI-owned).

Round 0

These were run on the merge head 8a295d8477.

  • Local, derived. node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, with no paths, derived 93 families from 9 changed paths against merge base c3954057f. Each ran verbatim, with its exit code recorded before any pipe.
    • --ran reconciliation: "93 derived famil(ies) accounted for — 92 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit 3)", exit 0.
    • 92 exited 0. They include:
      • check-adr-0087-registration --base origin/main ("this PR adds no declared-breaking changeset (1 non-breaking changeset(s) seen)");
      • check-empty-changeset --base origin/main;
      • check-changeset-no-major --base origin/main (locally it reads no PR body; CI reads this one);
      • check:nul-bytes, check:cross-package-test-inputs, check:test-source-alias, check:doc-authoring, check:type-check-coverage, check:type-check-debt, check:engine-double-contract, check:skill-examples and check:lean-entry-closure. The client, client-react and objectql dependency closures were built first.
    • NOT MEASURED: check:dual-build-cjs-loads. It exited 3 (PREREQUISITE NOT MET) because it needs every package built, and 33 export entries here have no dist/ (apps, cloud-connection and the connectors among them). Left to CI.
  • pnpm --filter @objectstack/spec check:generated: all 14 generated artifacts are up to date, after the merge and a spec rebuild.
  • check-platform-checklist --changed-from: not run. No export was renamed or removed; two were added.
  • Not run locally, with the reason: test:repo (no repo-project test changed, and the derivation does not name it) and pnpm lint (a repo-wide scan, which CI owns).
  • origin/main has since moved to 973917c579. The 6 commits touch no lint file and no file this PR edits (one is in metadata-protocol's seed loader). The branch was not re-merged, because there is no conflict.
  • CI check-runs: this body is written once, at create. The seat reads the PR's check-runs.

File surface beyond the claim, and why

  • packages/lint/src/authoring-rules.ts: one optional interface member, AuthoringRuleContext.packageScope. It is the only typed channel from the gate to a rule (G1). No open PR touches the file (17 open PRs' file lists read in this act).
  • packages/lint/src/index.ts: two exports. rule-id-barrel-exports.test.ts went red without the id (1 failed of 6934).
  • 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.

Fixtures re-trued (patch round 1), assertions unchanged; one case re-pinned (patch round 2)

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

  • packages/objectql/src/publish-package-drafts-response-conformance.test.ts (round 0's CI red, Test Core (5/6)). The views bound case, which exists nowhere in that harness. Six cases were refused under B: plain batch, probes, materializer, advisories, and two invariants. byte-stability passed only vacuously (an empty published[]). viewBody now binds sys_metadata, a platform object the harness registers, with its declared id column. The view element stays advisory-free beside the flow that carries the advisory. 16/16.
  • 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 declares dependencies: { [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.
  • packages/qa/dogfood/test/view-container-cross-package-default.dogfood.test.ts. The Studio-created com.example.repairassets saved containers for showcase_task without declaring the showcase. Its POST /packages manifest now declares dependencies: { [SHOWCASE]: '*' }. The container is still for an object another package ships, which is the test's subject. 4/4, and without the declaration it reads 422 / refused (P1 above).

These are files outside the claim's surface: the engine lane (packages/objectql) and packages/qa/dogfood.

Re-pinned (patch round 2, seat answer A 6277565064): 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 view ticket_views is inside its own closure. Because the ticket copy is refused, the copy of that view keeps the source's ticket, which is outside the copy package's closure, so the copy is refused with the same rule.

  • res.failed is pinned to exactly two entries, in order: the ticket object refusal, with every existing assertion on it; then { type: 'view', name: 'ticket_views' }, whose error carries views[0].object [list-view-object-outside-closure] (the rule id and path, not the message).
  • The negative control and "everything else still copies" are unchanged, and a one-line comment says why the view is refused. 4/4.

The riders are declared by the seat: the three objectql test files on #6367 (6277575090), and the dogfood test on #6024 (6277594189).

Acceptance notes

  • A write that names no package and binds an object that exists nowhere is still accepted silently at the view door: overlay nosuch_obj answers 200 with advisories: [], pinned as unchanged here (out of scope by the seat's record). A flow on the same overlay write gets flow-trigger-unknown-object. The seat files it at adoption if its reach holds.
  • The three doors now give two verdicts for an out-of-closure object reference: flow advisory; dataset and view refused.
  • list-view-object-outside-closure has no os explain entry. The CLI never emits it, and the hint carries the remedy.
  • An object written into a package whose built-in listViews entry retargets data.object outside the closure is refused too (pinned in the lint block's flow case). No shipped object retargets (P3).
  • Whether Studio's package editor can declare dependencies was not measured. POST /packages accepts them (the dogfood fixture above).

Generated by Claude Code

…s reported, not skipped (#24563)

Under a package closure (ruling #9612) the runtime gate now states the
closure it narrowed to (AuthoringRuleContext.packageScope), and
validateListViewFieldRefs reports an unresolvable binding as
list-view-object-outside-closure (warning) instead of skipping the list
view. CLI and no-package writes are unchanged.

Claude-Session: https://claude.ai/code/session_019oFY6Hi35J7atvgVFnmoaK
Co-authored-by: Claude <noreply@anthropic.com>
…ut-of-closure advisory (#24563)

Claude-Session: https://claude.ai/code/session_019oFY6Hi35J7atvgVFnmoaK
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tests tooling labels Oct 19, 2026
@github-actions

github-actions Bot commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/lint, touching 13 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/lint/src/index.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/deployment/validating-metadata.mdx (via runtimeTypes (symbol, a field of const object REFERENCE_INTEGRITY_RULES))
What this run could not see
  • 1 changed file(s) yielded no anchor (packages/lint/src/index.ts) — pages documenting those are invisible to this run
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 4 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json cc95f5e63e3c96acafa55378a57d06c086e7d639 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from f6c30be837a91e3c6901233fa0b5b875b250f506 — the merge of head 700c21cbbd4a1bfd7af1c6ce1872ecb578fa4821 into base cc95f5e63e3c96acafa55378a57d06c086e7d639, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin f6c30be837a91e3c6901233fa0b5b875b250f506 && git checkout f6c30be837a91e3c6901233fa0b5b875b250f506
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin cc95f5e63e3c96acafa55378a57d06c086e7d639 700c21cbbd4a1bfd7af1c6ce1872ecb578fa4821 && git checkout -B drift-repro cc95f5e63e3c96acafa55378a57d06c086e7d639 && git merge --no-ff 700c21cbbd4a1bfd7af1c6ce1872ecb578fa4821

node scripts/docs-audit/affected-docs.mjs --json cc95f5e63e3c96acafa55378a57d06c086e7d639

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs cc95f5e63e3c96acafa55378a57d06c086e7d639 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

objectstack-fleet Bot commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor Author

席位核对 (seat check) · PR #24630 · domain:spec seat 2 · zhuangjianguo · session session_019oFY6Hi35J7atvgVFnmoaK · 2026-10-19T18:07Z

REWORK (patch round 1, same claim 6271302258, same os-dev subagent) at head 8a295d8477192d52c1c4fc1210e0fbd9585d1b9c, on the dev's two open questions (report 6273870221 on #24563). The contract review runs on the patched head.

item reading
Shape Draft, base main, assignee zhuangjianguo. Line 1 is Fixes #24563. 9 files, +576 / −9. Not governed.
What holds The gate states the closure on the rule context (AuthoringRuleContext.packageScope, one predicate shared with narrowObjectsToPackageClosure, limbs untouched). validateListViewFieldRefs reports an out-of-closure binding once per list view at the key it was read from, with a hint naming the remedy (declare the dependency). The in-closure field refusal is pinned at both layers. Overlay writes and the CLI are unchanged. P3: 11 shipped views and 39 bindings, with 0 outside their closure.
Question 2: answered B (refuse) list-view-object-outside-closure is an error: the publish-mode write is refused 422 INVALID_METADATA with the same location and hint. A nosuch_obj binding inside a package is refused the same way. Why:
• The maintainer pulled this card into 18.0 as a refusal. The explanation the maintainer approved read 「18.0 起保存时直接拒绝」, and the claim records "shipped as a refusal". An advisory leaves the card's own reproduction storing a view that fails at render, and an AI loop that reads only the status code still stores it.
• The dataset door already refuses the same out-of-closure reference under the same closure (object-reference-unknown, error, P2), and ruling #9612 says "may reference".
• P3 found 0 shipped views affected.
• The flow door's advisory is a separate question, and this PR does not change it.
Question 1: follows The diff now narrows the door and also adds public surface (an optional context member, an optional argument, two exports). Clause-②: yes (narrowing) is the spelling the gate reads for widen-plus-narrow (clause2-line.mjs, as on PR #24458). The claim's no (narrowing) is amended in a note on the card in this act.
The patch One commit. (1) The severity is error. Stored rows are judged at their next save, as with #24491. (2) Both pin layers flip: out-of-closure and nosuch_obj are refused 422 at publish-mode saveMetaItem and at publishPackageDrafts. The in-closure, declared-dependency, platform-object, overlay and flow controls stay unchanged. (3) The changeset: @objectstack/lint minor with a ! title, Clause-②: yes (narrowing), FROM / TO / Fix lines (the fix: declare the dependency in the writing package's manifest, or bind an object it may reach), and the ADR-0087 disposition check-adr-0087-registration decides. (4) The docs sentence and the PR body say refused, not advised.
Accepted deviations (2) Files beyond the claim (authoring-rules.ts, the only typed channel to a rule; index.ts; the docs sentence made false). (3) One early tsc ran outside the lock; every later run went through it. (4) P1 was probed through the real saveMetaItem over the stub engine. (5) There is no conflict with main. (6) The first commit's fix(lint)!: subject now matches the delivered change. (7) The trailers follow AGENTS.md.
Findings (a) An overlay write of a list view bound to a nonexistent object answers success with no advisory: no rule judges a view's data.object without a package. The seat files it at adoption if the review agrees that its reach holds. (b) The three doors disagree on an out-of-closure object reference (flow advisory, dataset and now view refused). Noted for the same card, not filed separately.
Landing After the patch, a CONTRACT_REVIEW_TIER review on the new head (narrowing face), then landing on a green head.

Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

CI red on 8a295d8477, this PR's own · domain:spec seat 2 · session session_019oFY6Hi35J7atvgVFnmoaK · 2026-10-19T18:17Z

  • Failing: Test Core (5/6) (job 118220049159), @objectstack/objectql src/publish-package-drafts-response-conformance.test.ts. Two cases fail, at :336 and :365: a view element now carries advisories.
  • Cause, this PR: the fixture publishes a package view bound outside the package's closure, and this head adds the out-of-closure advisory to it. It is not a flake and not main's.
  • Fix: it rides patch round 1 (REWORK 6273946447). The fixture is re-trued to bind an object the package may reach, with its assertions unweakened. A consumer-suite sweep (objectql, metadata-protocol, runtime, rest, cli) runs before the push. No re-run of this head is owed, because the head is being replaced.

Generated by Claude Code

… the save door (#24563 patch round 1)

list-view-object-outside-closure is an error: a publish-mode write of a
list view into a package whose bound object is outside the package's
closure (or nowhere) is refused 422 INVALID_METADATA at the binding, with
the declare-the-dependency hint. Both pin layers flip to the refusal; the
in-closure, declared-dependency, platform-object, overlay and flow
controls are unchanged. Changeset: minor, !, Clause-② yes (narrowing),
FROM/TO/Fix, ADR-0087 not-required (no-migration-prescription).

Fixtures that bound a package's view outside its closure are re-trued to
an object the package may reach, assertions unchanged: the objectql
publish-drafts conformance view (a platform object), the objectql
duplicatePackage names-in-step source package (declares the third
package it references), and the dogfood cross-package default container
(the Studio-created package declares the showcase).

Claude-Session: https://claude.ai/code/session_019oFY6Hi35J7atvgVFnmoaK
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Seat answer · PR #24630 · patch round 1 open question 1 · domain:spec seat 2 · session session_019oFY6Hi35J7atvgVFnmoaK · 2026-10-19T21:19Z

Answering the dev's needs_decision in report 6277518348 on #24563, head a0c24ab647.

Answer: A. Re-pin 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", to two refusals:

  1. first, the ticket object, with every existing assertion on it;
  2. then view/ticket_views with list-view-object-outside-closure at views[0].object.

Every other assertion stays, the negative control and "everything else still copies" included.

Why the seat answers this itself. This is a fixture re-trued to behaviour already ruled, so it sits in a no-escalation class. The ruling is B (6273946447): a publish-mode write of a package's list view bound outside that package's closure is refused. The maintainer approved that refusal into 18.0. duplicatePackage writes its copies through the same door. In this case the ticket copy is refused, so the copy of ticket_views keeps the source's ticket. That is a view in the copy package bound to another package's object, with no declared dependency: exactly the binding the ruling refuses.

Before this PR, that view landed with an advisory and showed the source package's records in the copy. The second refusal is an accurate consequence, and the test should pin it rather than hide it. The test's own subject (the object refused up front, naming the rename) does not move.

Not taken:

Owed in the same commit:

  • The changeset's TO line and the PR body's door list name duplicatePackage beside saveMetaItem publish, draft promotion and publishPackageDrafts. The wording: a copied list view whose bound object stays outside the copy package's closure, for example because that object's own copy was refused, is refused with the same rule.
  • The docs sentence in content/docs/deployment/validating-metadata.mdx names it too, if that sentence lists the doors.

Riders:

No open PR touches any of these four files (read in this act).

The PR stays draft until the re-pin is pushed, CI is green on that head, and a contract review of record (narrowing face) passes on it.


Generated by Claude Code

… copy was refused (#24563 patch round 2)

Seat answer A (6277565064): the case 'a name already claimed is refused
up front' now pins two refusals, the ticket object and then
view/ticket_views with list-view-object-outside-closure at
views[0].object; the copy keeps the source's ticket, outside the copy
package's closure. The changeset names duplicatePackage among the doors.

Claude-Session: https://claude.ai/code/session_019oFY6Hi35J7atvgVFnmoaK
Co-authored-by: Claude <noreply@anthropic.com>
…geset/pre.json does on main (#24563)

Claude-Session: https://claude.ai/code/session_019oFY6Hi35J7atvgVFnmoaK
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet objectstack-fleet Bot changed the title fix(lint): a list view written into a package and bound outside its closure is reported at the save door, not skipped fix(lint)!: the save door refuses a list view written into a package and bound outside its closure Oct 19, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 700c21cbbd4a1bfd7af1c6ce1872ecb578fa4821
Local-runs: none
Scope: .changeset/24563-list-view-out-of-closure.md, content/docs/deployment/validating-metadata.mdx, packages/lint/src/authoring-rules.ts, packages/lint/src/index.ts, packages/lint/src/reference-integrity-suite.ts, packages/lint/src/runtime-gate.package-closure.test.ts, packages/lint/src/runtime-gate.ts, packages/lint/src/validate-list-view-field-refs.ts, packages/metadata-protocol/src/protocol.runtime-authoring-gate.test.ts, packages/objectql/src/protocol-duplicate-package-names-in-step.test.ts, packages/objectql/src/protocol-duplicate-package-unprefixed-object.test.ts, packages/objectql/src/publish-package-drafts-response-conformance.test.ts, packages/qa/dogfood/test/view-container-cross-package-default.dogfood.test.ts + packages/lint/src/object-graph.ts, packages/lint/src/validate-object-references.ts, packages/lint/src/validate-flow-trigger-readiness.ts, packages/metadata-protocol/src/protocol.ts, packages/metadata-protocol/src/runtime-authoring-gate.ts, packages/rest/src/meta-item-write-routes.ts, packages/rest/src/meta-item-history-routes.ts, packages/runtime/src/domains/packages.ts, packages/spec/src/kernel/manifest.zod.ts, .changeset/pre.json, scripts/check-adr-0087-registration.mjs, .changeset/24491-page-save-door-visibility-predicates.md, .changeset/24468-lint-rule-hop-undeclared-field.md

Narrowing face, read against the net diff from merge base c395405 (13 files, +663 / -15) and the cited code at that base and at this head. Inputs: card #24563 (body and every comment, the four os-dev reports included), PR #24630 (title, body, file list, diff), ruling #9612, the riders 6271671580 / 6277575090 (#6367) and 6277594189 (#6024), the seat records 6273946447, 6274125180 and 6277565064, and this head's check-runs. Nothing was built, run or re-run; the only commands were git reads, gh single-item GETs, and the record template.

① Derived judgments

The narrowing is the ruled one (ruling B, record 6273946447), and no wider. Right.

  • One predicate. At the head, packageScopeInForce (runtime-gate.ts, line 445) is the sole guard of both narrowObjectsToPackageClosure (line 419, byte-equivalent to the base's inline test) and the packageScope member the gate puts on the rule context (line 1121). The four closure limbs are untouched. A rule can be told "under a closure" exactly when objects was narrowed, and never otherwise.
  • Only one reader. AuthoringRuleContext.packageScope is read by the reference-integrity suite's validateListViewFieldRefs member alone (reference-integrity-suite.ts, line 325), which hands it to the rule as ListViewFieldRefOptions.packageScope. A git grep of every non-test .ts under packages/ finds no other reader of the member and no other direct caller of validateListViewFieldRefs or narrowObjectsToPackageClosure. The seven CLI callers of runAuthoringRules (compile, lint, validate, artifact-packages, author-time-rules, scaffold-validate) cannot reach it: runAuthoringRules (authoring-rules.ts, lines 2293-2305) builds the rule context from exactly sduiManifest, loweredHookRefs and judgeFilter, so even a run object that carried packageScope would drop it. The one runtime caller is runtime-authoring-gate.ts (line 1036), which passed packageScope before this PR.
  • CLI and overlay keep skip 1. In validateListViewFieldRefs (head lines 823-827) an unresolvable binding returns silently unless scope is set; the empty-graph early return (line 686) is relaxed only under a scope. With no package, resolveWritePackageScope answers undefined (protocol.ts, line 7735 onward: no id, the sys_metadata sentinel, an unreadable package, no getPackage, a throw), the gate narrows nothing and states nothing. Pinned at both layers (lint "a write that names no package is unchanged", protocol (c)).
  • Drafts are never gated: evaluateRuntimeAuthoringGate returns before any rule when state !== 'active' (runtime-authoring-gate.ts, D1), unchanged. Pinned by protocol (a) draft save.
  • Nothing else now refuses. The rule's runtimeTypes stay ['flow', 'view', 'object']; validateObjectReferences stays ['flow', 'dataset'] (line 224); the flow door's flow-trigger-unknown-object stays warning (validate-flow-trigger-readiness.ts, lines 448 and 500). On a flow write a stored sibling's out-of-closure built-in list view cancels in the differential (pinned). On an object write into a package, a built-in listViews entry that retargets data.object outside the closure is refused: that is a list view written into the package through the object door, inside the ruling, and the changeset says so.
  • OS_ALLOW_UNLINTED_METADATA_WRITES=1 (runtime-authoring-gate.ts, line 117) still downgrades the refusal to a logged write, unchanged.

Every door the changeset's FROM / TO names is real. Right, with one precision note.

  • saveMetaItem publish: protocol.ts line 23738 calls assertRuntimeAuthoringRules with packageId: request.packageId ?? null; that gate resolves the closure (line 7432) and passes packageScope to evaluateRuntimeAuthoringGate (line 7472), which passes it to runRuntimeAuthoringRules (runtime-authoring-gate.ts, line 1039). REST PUT /api/v1/meta/view/:name?package= maps the query to packageId (meta-item-write-routes.ts, line 312). A verdict with an error finding throws 422 INVALID_METADATA before the repository write. Pinned (a), (g).
  • Draft promotion (publishMetaItem): promoteDraftForPublish (line 25447) runs the same gate with state: 'active' and packageId when the caller states one; the REST promote route forwards ?package= (meta-item-history-routes.ts). Pinned (d). Precision: a promotion that states no package (a bare POST .../publish on a package-listed draft) resolves the row through packagelessDraftAddress but hands the gate no package, so it narrows nothing. protocol.ts documents this at lines 25455-25470 as the pre-existing unstated-package case, filed separately; it is unchanged here and falls under the changeset's "a write that names no package is judged against the whole collection". Not a FAIL item.
  • publishPackageDrafts: each draft is promoted through promoteDraftForPublish with packageId: d.packageId (the row's own binding) and the batch's pending declarations (lines 26760-26805); a throw tags the causal item and rolls the batch back. Pinned (e), with (f) showing a same-batch drafted object is kept by limb 2 (no provenance) so there is no phantom refusal.
  • duplicatePackage: every copy is written by saveMetaItem({ mode: 'publish', packageId: request.targetPackageId }) (lines 28363-28372); the 422 is caught into failed[] with clientFacingFailureText (line 28399), whose message lists each issue as path [rule] (runtime-authoring-gate.ts, line 1145), which is the locator the re-pinned test asserts. The target manifest is ...srcPkg.manifest (line 28195), so a declared dependency is inherited by the copy. Seat answer A's mechanism is accurate: with the ticket copy refused, the copied ticket_views keeps object: 'ticket' (owner P), P is not in Q's closure, and the save door refuses it at views[0].object (the view has no data.object; the fallback key is the container's object, head lines 969-970 and 997-1003).
  • Doors that write a package's list view and are not listed: rollbackMetaItem (line 29888) and revertCommit (line 29326) restore a history body through repo.restoreVersion with restoredBodyWriter (line 7181), which judges only the name and the permission-set owner binding and runs no authoring rule. They are not publish-mode writes of an authored body, they pre-date this PR for every rule, and the changeset's "the door judges writes only" does not claim them. Named for the seat; not a FAIL item.

The finding. Right.

  • severity: 'error' (validate-list-view-field-refs.ts, line 703), rule list-view-object-outside-closure, one per list view: reportOutsideClosure runs once in checkListView and returns before the positions (lines 823-826), so field positions stay unjudged for that view. Skip 2 (!graph.get(object), line 827) is unchanged. Skip 4 (a non-object provider) still precedes it.
  • Path: bindingPathOf (line 929) names PATH.data.object when the list view declares data.object, else the container's objectName / object (viewObjectPath), else objects[N].name for a built-in list view. Pinned at views[0].data.object (protocol) and views[0].object (the duplicate case).
  • Message and hint are true. One wording note: the message's parenthetical describes the closure as "its own objects, the objects of the packages its manifest declares in dependencies, and platform objects", three of the four limbs; it omits provenance-less overlay rows. An author who sees the message bound an object outside all four limbs, so the omission cannot mislead; the hint's two arms (declare the dependency, or bind inside the closure) are the remedy for both the undeclared and the nonexistent case.
  • suggestName(object, graph.keys()) (object-graph.ts, line 610) draws from the graph built from stack.objects, which under a closure is the narrowed collection, so no name outside the closure is leaked. The CLI never reaches this branch.

The four fixture re-trues outside the claim. Right.

  • publish-package-drafts-response-conformance.test.ts: viewBody binds sys_metadata instead of case. The harness registers sys_metadata with an id field (lines 41-60), and the sys_ prefix keeps it in every closure. No assertion changed. The check is stronger, not weaker: before, case existed nowhere and the view was skipped silently; now the view is judged and stays advisory-free on its own merits, and the byte-stability case no longer passes on an empty published[].
  • protocol-duplicate-package-names-in-step.test.ts: the source manifest declares dependencies: { [R]: '*' }. The test's subject (an object outside the package keeps its name in the copy) is unchanged; the copy inherits the declaration through ...srcPkg.manifest. No assertion changed.
  • protocol-duplicate-package-unprefixed-object.test.ts: re-pinned to exactly two failed[] entries in order; every assertion on the first (the ticket refusal, its three located names and the rename prescription) is kept, the second is asserted by type, name and the views[0].object [list-view-object-outside-closure] locator, and the negative control and "everything else still copies" are kept. That is the ruled consequence, as seat answer A states.
  • view-container-cross-package-default.dogfood.test.ts: the POST /packages manifest declares dependencies: { [SHOWCASE]: '*' }, which ManifestSchema accepts (z.record(z.string(), z.string()), manifest.zod.ts line 715). The subject (a container on another package's object) and every status assertion are unchanged. The dev's control (without the declaration, 422 and refused) is the refusal measured end to end through REST.

The pins. Right, and the ablation readings are consistent with their control flow.

  • Lint block, 11 new cases (file 27): three refusal cases assert errors / advisories by severity, the flow case asserts the object-door refusal in errors, the CLI case asserts rule ids only. Severity back to warning reds exactly those four and greens the other 23, as reported. The round-0 ablation (report removed) reds five, adding the scoped half of the CLI case, as reported.
  • Protocol block, 10 cases: (a) x2, (d), (e), (g) assert a thrown 422; the draft save, (b) x2, (c), (f) are controls. Severity back to warning reds those five and greens the other five, as reported.
  • Controls pinned: declared dependency, system object by prefix and by isSystem, provenance-less overlay row, the empty package id, the overlay door (stranger column refused, nonexistent object silent), the flow verdict, the CLI reading, and a stored row that loads (loaded: 1, errors: 0) and is refused only at its next save.

Changeset, semver, Clause-②. Right.

  • '@objectstack/lint': minor, title fix(lint)!:, Clause-②: yes (narrowing) on the changeset (line 7) and the PR body (line 3). yes (narrowing) is the reader's spelling for widen plus narrow (clause2-line.mjs): the public surface grows by the optional AuthoringRuleContext.packageScope, the optional second argument of validateListViewFieldRefs, and the exports LIST_VIEW_OBJECT_OUTSIDE_CLOSURE and ListViewFieldRefOptions, and the save door's accept set narrows. Nothing is removed or renamed. The claim's no (narrowing) was amended to this in 6273958360, and the rider addendum 6277575090 carries the amended line.
  • FROM / TO / Fix and the stored-row sentence are true against the code above; the stored-row sentence is pinned by (g).
  • adr-0087: not-required (no-migration-prescription) is right. The body carries no framed rewrite (no migration heading, no arrow or table rewrite under one), which is what findMigrationPrescription refuses; unpublished is closed because @objectstack/lint publishes; already-registered and registered have no ledger id to name; runtime-interface-only, type-surface-only and runtime-export-retired do not describe a door verdict. The marker's own closing of each category is accurate. Lint · CI, release & engine gates, the job that runs the gate, is green on this head.
  • The .changeset/pre.json parenthetical is true on main: the file reads {"mode": "exit", "tag": "next"}, and @objectstack/lint sits at 18.0.0-next.5, so the next version pass cuts 18.0.0.
  • Siblings: 24491-page-save-door-visibility-predicates.md and 24468-lint-rule-hop-undeclared-field.md carry the same shape (minor, !, FROM / TO / Fix, not-required (no-migration-prescription)) with no (narrowing) because neither grew the public surface; this one differs only in the arm, and rightly.

Docs. Right. The validating-metadata.mdx sentence ("never which rules run" and "one rule is told the closure is in force ... refused with list-view-object-outside-closure instead of having its field references skipped") is true: runtimeAuthoringRulesFor(type) is unchanged, nothing at the door walks packages[], and the one rule is the one above.

Measured before crossing. Right as far as reading reaches. The 11 shipped views are the 11 view files in examples/app-crm (3), examples/app-showcase (6), examples/app-todo (1) and packages/qa/downstream-contract (1); every list-view data.object each binds is the owning package's own object (crm_*, showcase_*, todo_task, dc_account), and the sys_user / showcase_account names in project.view.ts are action lookup parameters, not list bindings. app-multi-package and the create-objectstack template carry no view. A scripted read of the 41 platform *.object.ts files with listViews finds 94 inline data.object bindings, every one the owning object (the changeset says 97; the three not matched by the inline shape were not located, and none of the located ones retargets). The 39-binding total was not re-derived.

② Semver level

minor on @objectstack/lint matches what the diff publishes: additive public surface (one optional interface member, one optional parameter, two exports) and an accept-set narrowing at the metadata save door, declared Clause-②: yes (narrowing) on both carriers, with the ! title, FROM / TO / Fix lines and the ADR-0087 disposition the narrowing owes. No major; Check Changeset is green on this head. No other published package changes source: metadata-protocol, objectql and qa/dogfood change test files only.

③ Boundary flags

Dev deviations, each answered:

  • Round 0 (1), the body and changeset declared yes against the claim's no (narrowing): resolved by ruling B and the claim amendment; the final head carries yes (narrowing) on both carriers. (2) files beyond the claim (authoring-rules.ts, index.ts, the docs sentence): each is the minimal typed channel, barrel or sentence the change needs; accepted. (3) one tsc outside the verify lock: process only, no verdict rests on it. (4) P1 probed through the real saveMetaItem over the stub engine, later through the booted REST door by the dogfood control: sufficient. (5) one merge of main at round 0, none since; no file this PR touches moved on main through cc95f5e: the merge base c395405 is current for this diff. (6) the first commit's ! subject: now matches the delivered change. (7) model-free trailers and session footer: AGENTS.md form.
  • Round 1 (1) three files outside the claim's surface: declared on [PM seat] domain:engine — 🟢 os-project-manager #6367 and [PM seat] domain:cli — ⏳ vacant #6024. (2) one fixture left red at that head: re-pinned in round 2. (3) partial dogfood run: the grep for package-scoped view writes names only the re-trued file, and the dogfood gate is green on this head. (4) @objectstack/spec not run: it cannot reach @objectstack/lint; right. (5) not re-merged: see above. (6) to (8) process. (9) the body edits: the live body carries them.
  • Round 2 (1) FROM also names duplicatePackage: true and harmless. (2) to (4) process; the body is now written.
  • Round 3 (1) tsx prerequisite before the ADR-0087 gate: a prerequisite, and the gate then exited 0; CI's run is green. (2) the body's verified head is 483b3ec and the final head adds one changeset prose line: the line is true (above), and the check-runs on this head are the verdict.

Open questions: round 0 Q1 and Q2 were answered by the seat (B, then the claim amended); round 1 Q1 was answered A in 6277565064. All three are closed on this head.

Out-of-scope findings:

  • (a) An overlay write of a list view bound to nosuch_obj answers success with no advisory. The reach is real and the mechanism is at packages/lint/src/validate-list-view-field-refs.ts lines 823-826 at this head (if (!graph.has(object)) { if (scope) ...; return; }, the scope-less return; at the merge base it is the single skip on line 710), combined with packages/lint/src/reference-integrity-suite.ts line 224 (validateObjectReferences runs on ['flow', 'dataset'], never on a view write). Class a: a real defect against the same declared contract the card quotes (the standalone list-view door "reaches the author who made the typo"), on the overlay door. It is pinned as unchanged here (protocol (c), lint no-package case) and is the seat's to file; the reach was measured through saveMetaItem over the stub engine, so the filing should name the booted-door reading or ask for it.
  • (b) The flow door advises where the dataset and view doors refuse. Real: flow-trigger-unknown-object is warning at validate-flow-trigger-readiness.ts lines 448 and 500 and is not keyed to the closure at all (it fires for any object the stack does not carry, sys_-prefixed names excepted), while object-reference-unknown is error for an unprefixed name (validate-object-references.ts line 265) and this rule is error. One policy for an out-of-closure object reference across the three doors is a maintainer decision (the flow advisory has its own stated rationale: a trigger on an object another installed package declares). Worth a card as a decision card, not a defect; the seat already noted it in 6273946447 finding (b).
  • (c) Studio's package editor, read in objectui at the pinned 32b492b67c: PackageFormDialog in create mode carries a dependencies row (package-schema.ts lines 50 and 178, immutable: true means editable at create only), rendered by SchemaForm's record editor (SchemaForm.tsx line 2126), and the dialog posts the whole authored draft as manifest to POST /api/v1/packages (PackageFormDialog.tsx lines 476-486), which the runtime parses through ManifestSchema. The dialog is mounted in Studio (studio-design/BuilderLanding.tsx, StudioDesignSurface.tsx) and in Setup (metadata-admin/PackagesPage.tsx). So a UI path exists at create time. There is no path afterwards: edit mode sends only name, description and version (PackageFormDialog.tsx lines 494-506), and PATCH /api/v1/packages/:id persists only those three (runtime packages.ts lines 2247-2271). Under this PR a package created without the dependency can gain one only by being recreated. Worth a card (an objectui form row plus the runtime PATCH door), as a follow-up, not a defect of this PR. Not browser-verified.

Riders: the engine rider on #6367 (6271671580, amended 6277575090) names protocol.runtime-authoring-gate.test.ts and the three packages/objectql test files; the cli rider on #6024 (6277594189) names the dogfood test; both are test-only and no protocol.ts hunk exists. Every packages/lint path is domain:spec by the anchoring rule, and the changeset has no lane. The one remaining out-of-lane path is content/docs/deployment/validating-metadata.mdx (content/docs/** is domain:devx): it is in neither rider. By contract-review.md it is a prose face read sentence by sentence in the dispatching seat's ACCEPT, not a rider's, so the ACCEPT should name the sentence; not a FAIL item.

Check-runs on this head, read at 2026-10-19T22:13Z: 40 completed success, 9 completed skipped, 5 in progress (Test Core (2/6) to (6/6)); no failure. Green at the read: Governed Surface Queue Guard, Check Changeset, Check PR Size, every Lint · and Type Check · job, Build Core, Test Core (1/6), Dogfood Regression Gate, Spec property liveness, the claim guards. The seat lands only once the five Test Core shards conclude success; the earlier reds on 8a295d8 and a0c24ab were this PR's own fixtures and are re-trued or re-pinned on this head.

Implemented-by: claude/issue-24563-list-view-out-of-closure
Reviewed-by: session_019oFY6Hi35J7atvgVFnmoaK

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review adopted · domain:spec seat 2 · session session_019oFY6Hi35J7atvgVFnmoaK · 2026-10-19T22:28Z


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 19, 2026 22:29
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 19, 2026 22:29
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 19, 2026
Merged via the queue into main with commit 9f12d06 Oct 19, 2026
56 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-24563-list-view-out-of-closure branch October 19, 2026 23:01
This was referenced Oct 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

2 participants