Skip to content

fix(lint)!: the overlay save door refuses a list view bound to an object that exists nowhere (#24668) - #24789

Merged
objectstack-fleet[bot] merged 8 commits into
mainfrom
claude/issue-24668-overlay-list-view-unknown-object
Oct 20, 2026
Merged

objectstack-fleet[bot] merged 8 commits into
mainfrom
claude/issue-24668-overlay-list-view-unknown-object

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #24668

Clause-②: yes (narrowing)

The overlay save door (a runtime write that names no package) now refuses a list view bound to an object that exists nowhere in the collection the deployment serves. It answers 422 INVALID_METADATA with one list-view-object-unknown issue at the binding, and nothing is stored. This is the overlay twin of #24563, which refuses the same body under a package closure (list-view-object-outside-closure), and it matches the dataset door's verdict for an unknown base object (object-reference-unknown). The CLI (os validate / os build / os lint) keeps skip 1, drafts stay ungated, and a binding that resolves is judged exactly as before.

The premises, measured

P1: the booted reading (before and after)

Booted examples/app-crm with pnpm dev:crm -- --fresh -p 38617 (SQLite, 38 plugins), signed in as the seeded admin, and sent PUT /api/v1/meta/view/nosuch_obj.qavfy_r1 with no ?package= and the body { name, object: 'nosuch_obj', viewKind: 'list', type: 'grid', data: { provider: 'object', object: 'nosuch_obj' }, columns: [{ field: 'name' }] }.

  • Before (base 4b114eda57; packages/lint/dist built from the base source, and ablation-dist-preflight --absent confirmed the new rule id was not in it): HTTP 200, {"success":true,…,"state":"active","message":"Saved view 'nosuch_obj.qavfy_r1' (env-wide, state=active) [seq=1]"}, with no advisory. GET /api/v1/meta/view/nosuch_obj.qavfy_r1 served it with _diagnostics: {"valid": true}. The view's own query fails: GET /api/v1/data/nosuch_obj answered 404 {"error":"Object 'nosuch_obj' is not registered","code":"OBJECT_NOT_FOUND"}.
  • After (this branch, packages/lint rebuilt, the rule id confirmed in dist/): HTTP 422, code: INVALID_METADATA, one issue { rule: 'list-view-object-unknown', path: 'views[0].data.object', severity: 'error' }. The follow-up GET answered 404, so nothing was stored. examples/app-showcase and examples/app-todo gave the same 422 at the same path.

P2: is the no-package snapshot really the whole collection?

Yes, as far as I could measure it.

  • Code. assertRuntimeAuthoringRules gathers objects as registry.listItems('object') folded with the stored active rows through mergePackageAwareOverlay, the merge GET /meta/:type answers from. It applies no disabled-package filter. resolveWritePackageScope answers no closure for a missing package id, for the sys_metadata sentinel, for a package the registry cannot produce, and for a registry without getPackage. In all of those cases narrowObjectsToPackageClosure hands the collection over whole.
  • Booted. GET /api/v1/meta/object on the CRM stack served 81 objects: 75 sys_* objects from 15 platform and plugin packages, and 6 com.example.crm objects. After an overlay object tenant_ticket was saved with no package, it served 82, with tenant_ticket at _packageId: sys_metadata. Then, through the door on this branch, I saved a probe list view bound to each served object (with one real column each), and deleted it after each save. Every one of them answered 200 and none was refused list-view-object-unknown: 82 of 82 on CRM, 109 of 109 on showcase, 76 of 76 on todo. So no object the deployment serves is missing from the snapshot.
  • The boundary. An EMPTY collection is not treated as a whole one. No deployment serves zero objects, so an empty graph means the host supplied no universe, and the rule then judges nothing, as before. This is pinned.

P3: the route is (a)

The list-view rule now refuses an unresolvable binding when it is told that its snapshot is the whole collection.

  • One finding per list view, at the binding key. The rule already walks every rung: an object's built-in listViews, the flattened overlay, the ViewItem record, a container's list and listViews. It already computes the binding's key (bindingPathOf) for 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. Route (b) would have to give validateObjectReferences that same walk a second time. And because that function takes only stack, it would also change the CLI verdict unless it were made runtime-aware. Crossing it onto view would also bring its views[].object container rung to the runtime door, which its own docblock says it does not run on. That is a widening the ruling did not make.

  • The rule id. This adds a new id, list-view-object-unknown, to the rule's own family (list-view-field-unknown, list-view-object-outside-closure, list-view-field-dotted: one id per finding class). Reusing an existing id did not fit:

    • object-reference-unknown carries the CLI ladder: a prefixed name only advises, and its os explain entry describes that ladder.
    • list-view-object-outside-closure names a package and a dependency remedy, and this verdict has neither.

    The finding keeps the error family the ruling names: error severity, refused 422 INVALID_METADATA at the binding key.

  • The CLI is unchanged. The reference-integrity suite states wholeCollection: true only when runtimeWriteType is set and packageScope is absent. The gate sets runtimeWriteType on every gated write, and the CLI commands never do. The gate states packageScope exactly when it narrowed (one predicate, packageScopeInForce). So runtime-gate.ts did not need an edit.

  • The nearest name goes in the hint, never the message. The gate's differential fingerprints rule, where, path and message, and an object write changes the snapshot's object names by one. If the "Did you mean" were in the message, a stored object's list view bound to crm_invoce would read differently once an unrelated write created crm_invoice. That unrelated write would then be refused for a finding it did not make. This is pinned, with a non-vacuity check that the suggestion really does differ between the two universes.

Census, measured before crossing

Through the booted door of the three bootable examples, I counted every view each stack serves and the built-in list views of every object it serves:

stack served objects shipped views built-in list views list-view-object-unknown
examples/app-crm 81 10 86 0
examples/app-showcase 109 36 98 0
examples/app-todo 76 3 86 0

examples/app-multi-package ships no view (git grep). In total, 0 shipped views bind an object that exists nowhere. The control, the same universe with one nosuch_obj binding, is refused every time. Deployed rows were not measured.

Changes

Pins

Consumer sweep and fixtures

The sweep ran @objectstack/lint, metadata-protocol, objectql, runtime, rest, cli (unit tier), platform-objects, and the 11 dogfood files that write views. The fixtures outside packages/lint / packages/spec that bound a view to an object that exists nowhere through the overlay door are listed below, so the seat can declare the riders. Each was re-trued to a real object, with its assertions unchanged:

  • packages/objectql/src (10 files):
    • object: 'case' → 'sys_metadata', an object each harness registers, in: delete-meta-response-conformance, protocol-metadata-row-actor, protocol-publish-canonical-fold, protocol-recorded-by-null, protocol-revert-org-scope, protocol-save-meta-repo-path-real-engine, publish-meta-response-conformance, save-meta-response-conformance;
    • protocol-commit-history (the mixed-type build binds its view to the myapp_invoice it creates);
    • environment-overlay-five-types (the package manifest now ships probe_obj, which its view and its dataset bind to).
  • packages/rest/src (3 files), where each harness now registers the object its fixture view binds to: meta-diff-default-range-labels (lead), meta-view-chart-binding (crm_lead), meta-view-overlay-options-bag (showcase_task).

No fixture was left red, and no assertion was weakened.

Ablation (one-shot; no permanent test)

Done with scripts/ablation-replace.mjs, which reported the anchor count 1 → 0 and the blob moving for each leg. Each leg was restored, and the restore was proven by blob equal to HEAD and an empty git diff HEAD.

  • A1, lint layer, in source: severity error → warning. The lint pins went red: 7 failed, 59 passed, over 3 files.
  • B1, lint layer: the refusal line removed. 9 failed, 57 passed, over 3 files.
  • A2, protocol layer, through packages/lint/dist: severity error → warning. The mutated build was confirmed in dist/ by ablation-dist-preflight. The protocol pins went red: 5 failed, 163 passed. Then I restored, rebuilt, ran preflight --absent (exit 0, tree clean), and confirmed with a control that the fix is back in dist/.

All three went red, as predicted.

Tests and gates

These readings were all taken on the final head 3df138798d (the branch with origin/main 795ff708d8 merged), after a full turbo run build of that tree. Each command ran under os-verify-lock.sh with --maxWorkers=2. Times are shared-box readings.

  • @objectstack/lint, whole package: 149 files, 6958 tests passed.

  • @objectstack/metadata-protocol, whole package: 251 passed, 3 skipped (254); 20816 passed, 19 skipped.

  • @objectstack/objectql, whole package: 451 files, 8495 tests passed.

  • @objectstack/rest, the 3 re-trued files: 18 passed. The whole package ran before the merge at 39475b9113: 304 files passed, plus 3 files red, which were the 3 re-trued since.

  • @objectstack/dogfood, the 11 files that write views: 90 passed.

  • These ran before the merge:

    • @objectstack/runtime at 39475b9113: 431 files passed, 7570 passed, 19 skipped;
    • @objectstack/cli --project unit at 54fb13a743: 314 files, 4643 tests passed. The integration tier is declared to CI;
    • @objectstack/platform-objects at 54fb13a743: 79 files, 1195 tests passed.

    The merge brought no change to any of those packages that my diff overlaps.

  • typecheck on 3df138798d, one && chain, exit 0, 0 error TS: @objectstack/lint (with check:test-typecheck: OK), @objectstack/metadata-protocol, @objectstack/objectql (with check:test-typecheck: OK), and @objectstack/rest (with check:test-typecheck: OK).

  • Gates. I derived the list with node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack on 3df138798d (96 families) and ran every one, recording each exit before any pipe. Reconciliation with --ran: "96 derived, 96 run, 0 NOT-MEASURED, 0 UNRUN (a DERIVED zero)".

    • 95 exited 0.
    • 1 exited 1: node scripts/check-empty-changeset.mjs --base origin/main. That is the DELIBERATE CORRECTION class above, left red on purpose.
  • The changeset gates by name:

    • check-adr-0087-registration --base origin/main exit 0: "1 declared-breaking changeset(s), each carrying an ADR-0087 disposition", [BREAKING+bang+clause-②-narrowing] not-required (no-migration-prescription);
    • check-changeset-no-major --base origin/main exit 0: "This diff introduces no major bump";
    • check-nul-bytes exit 0.
  • The PR's own check-runs are CI's to report. I have not re-run them locally.

Acceptance notes

  • A stale comment in authoring-rules.ts. AuthoringRuleContext.packageScope's docblock says an object "missing from a whole one may come from another package". That now holds only for the CLI. I did not edit it: authoring-rules.ts is outside this card's declared surface, and lint: a lookup or master_detail field whose reference is a stored-metadata-body table saves, builds and validates, but the engine never hydrates it — refuse it at authoring time #24677 may touch it. Carrier: the domain:spec seat.
  • A stale comment in runtime-gate.ts. The comment at the packageScope statement ("from one a whole-collection snapshot merely does not carry") has the same staleness. I did not edit it either. Carrier: the domain:spec seat.
  • A degraded registry read. assertRuntimeAuthoringRules's listRegisteredCollection answers [] when registry.listItems throws. With a non-empty stored half, that universe is partial, and a view bound to a registry object would then be refused. This is a fault path, not an observed state: I have no reproduction and no reach, and the dataset door's object-reference-unknown already depends on the same collection. Recorded as a boundary; not filed.
  • Not pinned. publishPackageDrafts for a package the registry cannot produce takes this verdict too, by construction. It is not pinned here.

Generated by Claude Code

…ct that exists nowhere

A runtime write that names no package is judged against the whole collection
the host serves, so a list-view binding missing from it exists nowhere.
validateListViewFieldRefs now refuses it once, at the binding key
(list-view-object-unknown, error) when the reference-integrity suite states
the whole collection (a runtime write with no closure in force). The CLI keeps
skip 1.

Claude-Session: https://claude.ai/code/session_019oFY6Hi35J7atvgVFnmoaK
Co-authored-by: Claude <noreply@anthropic.com>
… list-view verdict

Claude-Session: https://claude.ai/code/session_019oFY6Hi35J7atvgVFnmoaK
Co-authored-by: Claude <noreply@anthropic.com>
…ve universe once

Claude-Session: https://claude.ai/code/session_019oFY6Hi35J7atvgVFnmoaK
Co-authored-by: Claude <noreply@anthropic.com>
…e whole-collection list-view verdict

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 20, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/lint, touching 6 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.

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

  • content/docs/concepts/metadata-lifecycle.mdx (via /api/v1/meta/view/:name (route, a path literal in a comment on a changed line))
  • content/docs/kernel/services-checklist.mdx (via /api/v1/meta/view/:name (route, a path literal in a comment on a changed line))
  • content/docs/protocol/objectui/index.mdx (via /api/v1/meta/view/:name (route, a path literal in a comment on a changed line))
  • content/docs/ui/forms.mdx (via /api/v1/meta/view/:name (route, a path literal in a comment on a changed line))

⛔ 1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17/17-6.mdx (via /api/v1/meta/view/:name (route, a path literal in a comment on a changed line))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

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 46bb5e3e5405fad31ccba6fe571e98b9234aac78 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from e20c09f614b09cadef4bbd61154053e6938d5583 — the merge of head 3df138798da76645ca5eb529a7b8a924a3d14bdf into base 46bb5e3e5405fad31ccba6fe571e98b9234aac78, 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 e20c09f614b09cadef4bbd61154053e6938d5583 && git checkout e20c09f614b09cadef4bbd61154053e6938d5583
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 46bb5e3e5405fad31ccba6fe571e98b9234aac78 3df138798da76645ca5eb529a7b8a924a3d14bdf && git checkout -B drift-repro 46bb5e3e5405fad31ccba6fe571e98b9234aac78 && git merge --no-ff 3df138798da76645ca5eb529a7b8a924a3d14bdf

node scripts/docs-audit/affected-docs.mjs --json 46bb5e3e5405fad31ccba6fe571e98b9234aac78

⚠️ 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 46bb5e3e5405fad31ccba6fe571e98b9234aac78 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Seat confirmation · deliberate correction of a pending release note · domain:spec seat 2 (#18549) · session session_019oFY6Hi35J7atvgVFnmoaK · 2026-10-20T06:27Z

This PR modifies .changeset/24563-list-view-out-of-closure.md. That is the pending note of #24563, which this seat landed (PR #24630, 9f12d06770), so this seat owns the note.

Confirmed: option A. The corrected sentence stays. Restoring the base sentence would publish two contradictory statements about the same door in one release.

Check Changeset stays red, by design. The seat records the three conditions for queueing with that red:

  1. The gate's own source names this class and says to leave it red: scripts/check-empty-changeset.mjs, "DELIBERATE CORRECTION → do NOT restore it; get it confirmed on the PR". So does the step text in .github/workflows/pr-automation.yml: "LEAVE THIS CHECK RED … 'Check Changeset' is not one of the required contexts".
  2. pr-automation.yml runs on pull_request only, with no merge_group trigger, so the queue build is not affected.
  3. This comment names the gate and the reason.

⛔ No skip-changeset label: the gate refuses it for this class.


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 3df138798da76645ca5eb529a7b8a924a3d14bdf
Local-runs: none
Scope: .changeset/24563-list-view-out-of-closure.md, .changeset/24668-overlay-list-view-unknown-object.md, content/docs/deployment/validating-metadata.mdx, 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.view-writes.test.ts, packages/lint/src/runtime-gate.whole-collection.test.ts, packages/lint/src/validate-list-view-field-refs.ts, packages/metadata-protocol/src/protocol.runtime-authoring-gate.test.ts, packages/objectql/src/delete-meta-response-conformance.test.ts, packages/objectql/src/environment-overlay-five-types.test.ts, packages/objectql/src/protocol-commit-history.test.ts, packages/objectql/src/protocol-metadata-row-actor.test.ts, packages/objectql/src/protocol-publish-canonical-fold.test.ts, packages/objectql/src/protocol-recorded-by-null.test.ts, packages/objectql/src/protocol-revert-org-scope.test.ts, packages/objectql/src/protocol-save-meta-repo-path-real-engine.test.ts, packages/objectql/src/publish-meta-response-conformance.test.ts, packages/objectql/src/save-meta-response-conformance.test.ts, packages/rest/src/meta-diff-default-range-labels.test.ts, packages/rest/src/meta-view-chart-binding.test.ts, packages/rest/src/meta-view-overlay-options-bag.test.ts + packages/lint/src/runtime-gate.ts, packages/lint/src/authoring-rules.ts, packages/lint/src/object-graph.ts, packages/lint/src/rule-explanations.ts, packages/lint/src/rule-id-barrel-exports.test.ts, packages/lint/src/validate-object-references.ts, packages/metadata-protocol/src/protocol.ts, packages/metadata-protocol/src/runtime-authoring-gate.ts, packages/objectql/src/registry.ts, packages/cli/src/commands/compile.ts, packages/cli/src/commands/lint.ts, packages/cli/src/commands/validate.ts, packages/cli/src/utils/artifact-packages.ts, packages/cli/src/utils/author-time-rules.ts, packages/cli/src/utils/scaffold-validate.ts, scripts/check-empty-changeset.mjs, .github/workflows/pr-automation.yml, examples/app-crm/src/views, examples/app-showcase/src/ui/views, examples/app-todo/src/views

Reviewed on the narrowing face, adversarially: the net diff against the merge base 795ff708d8 (23 files, +637 / -33), card #24668 (body and every comment), the PR body and file list, the cited code at the base and at the head, and the head's check-runs. The dispatch order and the seat's own conclusions were read as claims to test, not as inputs.

① Derived judgments

Public surface of @objectstack/lint (two additions, both right).

  • LIST_VIEW_OBJECT_UNKNOWN ('list-view-object-unknown') is exported from validate-list-view-field-refs.ts and re-exported on the index barrel. rule-id-barrel-exports.test.ts requires exactly that; runtime.ts carries no rule id, and the closure sibling LIST_VIEW_OBJECT_OUTSIDE_CLOSURE is on index alone, so parity holds.
  • ListViewFieldRefOptions.wholeCollection?: boolean is added beside packageScope. Read at one place, const whole = !scope && options?.wholeCollection === true, so a stated closure wins whenever both are given. Right, and pinned at the rule and through the gate.

The refusal fires only where "exists nowhere" is decidable — proven from the code, not the dispatch.

  • wholeCollection is stated at one site: the reference-integrity member for validateListViewFieldRefs, as ctx?.packageScope ? { packageScope } : ctx?.runtimeWriteType !== undefined ? { wholeCollection: true } : undefined.
  • runtimeWriteType is set at exactly one non-test site in the repository: runtime-gate.ts runRuntimeAuthoringRules (runtimeWriteType: args.type). validate-expressions.ts reads it and sets nothing. runAuthoringRules builds its context from sduiManifest, loweredHookRefs and judgeFilter only, so even a caller that passed runtimeWriteType or packageScope on the run would not reach the rules with them. Its seven non-test callers are all CLI (commands/compile.ts, commands/lint.ts, commands/validate.ts, utils/artifact-packages.ts, utils/author-time-rules.ts, utils/scaffold-validate.ts). validateReferenceIntegrity has one non-test caller, the authoring-rules.ts registry entry, which forwards that same context. So the flag is never set on os validate / os build / os lint. Right; the "CLI unchanged" pins (the rule with no statement, the suite with no write type, the three commands) hold it.
  • ctx.packageScope is stated by runRuntimeAuthoringRules only under packageScopeInForce (a non-empty packageId string), the same predicate narrowObjectsToPackageClosure narrows on, so the flag is never stated beside a closure that is in force and never absent beside a snapshot that was narrowed. runRuntimeAuthoringRules has one non-test caller, metadata-protocol/src/runtime-authoring-gate.ts evaluateRuntimeAuthoringGate, which forwards packageScope only when the host stated one. Right; runtime-gate.ts did not need an edit.
  • The no-package snapshot is the whole served collection. assertRuntimeAuthoringRules gathers objects as registry.listItems('object') folded with the stored half. listItems('object') routes to getAllObjects, which walks every objectContributors key and applies no isPackageDisabled filter (that filter sits on the non-object branch of listItems and on actions; getMetaItems applies its own when serving), so a disabled package's objects are in the gate's universe, in the conservative direction. foldStoredCollection folds state: 'active' rows of the environment through mergePackageAwareOverlay, the read API's own merge, with no disabled-package filter by declaration. resolveWritePackageScope answers undefined for no id, the sys_metadata sentinel, a package the registry cannot produce, a registry without getPackage, and a throw; in each, narrowObjectsToPackageClosure hands the collection over whole, and evaluateRuntimeAuthoringGate folds a batch's own pending drafts on top. Right. One consequence worth naming: a draft-only object is not in the universe (#7529's refuse-at-publish ruling), so a publish-mode view bound to an object that exists only as a draft is refused; the view's own draft save stays ungated, and that is the ruled posture, not a new one.
  • Partial paths, weighed. (a) listRegisteredCollection answers [] on a thrown listItems: with a non-empty stored half the universe is the stored rows alone, and a view bound to a registry object would be refused. getAllObjects is a map walk plus resolveObject; a throw there is a fault, not a state, and the same universe already decides object-reference-unknown on the dataset door, widget-dataset-unknown and every list-view-field-unknown verdict, so this PR adds one rule to an exposure that exists, not a new exposure class. (b) The mirror fault the dev did not name: foldStoredCollection on a store read failure other than a missing table answers the registry half alone, warned once, so a view bound to a stored-only overlay object would be refused for that write. Same class, and the write itself needs the store. (c) publishPackageDrafts for a package the registry cannot produce reaches the gate through promoteDraftForPublish with the package stated, resolveWritePackageScope answers undefined, and the write is judged against the whole collection plus its batch's pending drafts; the package's own objects are either pending (folded) or stored active (folded), so "exists nowhere" stays decidable there. Unpinned at that door; right by construction. None of the three is a reason to withhold the verdict.
  • "An empty collection is not a whole one." The guard if (graph.size === 0 && !scope) return findings; is the merge base's own line (base :686), unchanged; the new flag inherits it. Right: the host that hands a universe over hands a non-empty one (the platform's objects are always registered), so an empty graph without a closure means no universe was supplied, and the degrade is to the pre-PR silence, fail-open on a fault. Under a closure an empty graph stays the 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 verdict, because there the host did narrow. Pinned in both shapes (empty objects, and no context at all).

The finding (right).

  • severity: 'error', rule: LIST_VIEW_OBJECT_UNKNOWN, path: bindingPath from the existing bindingPathOf, one per list view, on every rung the walk already reads (container list, list.data.object, listViews.*, the ViewItem record, the flattened overlay, an object write's built-in listViews). The field positions stay unjudged (the return after the report), and skip 2 (!graph.get(object)) is untouched.
  • The message names only the written binding; the hint carries the remedy and suggestName(object, graph.keys()), which is ' Did you mean "x"?' or the empty string. Neither string carries a tracker number.
  • The differential fingerprint is rule, where, path, message joined by NUL (runtime-gate.ts :603); the hint is outside it. The message depends on the written name alone, so an object write that creates the name nearest to a stored sibling's dangling binding moves that sibling's hint and not its fingerprint, and the sibling's finding cancels between the baseline and candidate passes. Right, and pinned with a non-vacuity control (the two hints differ, the two messages are equal). No existing assertion depends on the split the other way: the 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 closure finding keeps its package id in the hint, and no pin reads this rule's message for a suggestion.
  • The rule id. object-reference-unknown carries the four-rung prefix ladder (an unresolved name with a platform prefix is an advisory there) and its os explain entry (rule-explanations.ts :3457) describes that ladder; at the whole-collection door there is no ladder, a sys_-prefixed name that exists nowhere is refused, and suppressWarnings plus every consumer filter key on the id. Reusing it would make its explanation false on the view door. list-view-object-outside-closure names a package and a dependency remedy this verdict has neither of. A new id in the rule's own family is the sound choice. Registered where a rule id must be: the barrel (required by the barrel test), the docs sentence and the changeset. rule-explanations.ts (178 entries) carries list-view-field-unknown and list-view-field-dotted but not the closure sibling, so the new id is at parity with its nearest sibling; an os explain entry for the closure/unknown pair is a follow-up for the seat, not a condition.

#24563's pins, each re-true read against the ruling.

The 13 consumer fixtures (10 objectql, 3 rest): none vacuous, nothing dropped.

  • Eight objectql harnesses bind sys_metadata, which each registers (registerObject(sysMetadataObject, …) read in every file); columns: ['id'] resolves because id is an injected column of the graph. No assertion in those files read the old case binding back.
  • protocol-commit-history: the mixed-type build now binds myapp_invoice, the object the same build creates (invoiceBody: name, amount), with columns: ['id', 'name']; the revert assertions (failed [], revertedCount 2, both rows gone) are unchanged.
  • environment-overlay-five-types: the package manifest ships probe_obj, which its view and dataset already bound; the "exactly these five types" assertion reads DEFAULT_METADATA_TYPE_REGISTRY, not the manifest.
  • rest: lead (name, owner), crm_lead (stage, amount) and showcase_task (title, location) are registered with the fields each fixture's columns use. Vacuity check on meta-view-overlay-options-bag: its refusal case still binds crm_lead, which that harness does not register, but the pinned 422 is the Zod unrecognized_keys issue at timeline / options.timeline, and the protocol runs the schema check before the authoring gate, so the gate's verdict cannot be what satisfies it; the 200 case binds the registered showcase_task. Non-vacuous.

Ablations, read against the pins' control flow (consistent). The three lint files hold 66 cases. Severity to warning (A1, 7 failed / 59 passed) reds the refusal, hint, every-rung, object-write and stored-sibling-control cases plus the two re-trued pins; the refusal line deleted (B1, 9 / 57) reds two more, exactly the assertions that read findings regardless of severity: validateReferenceIntegrity(stack, { runtimeWriteType: 'view' }) toContain and the before/after non-vacuity control. Protocol through dist (A2, 5 failed / 163 passed): (a) refuse, (c) promotion, (d) unresolvable package, (e) next save, and #24563 (c)'s nowhere half; (a) control and (b) are severity-independent. The numbers are what the control flow predicts.

Docs. The validating-metadata.mdx sentence is true: with no closure in force the context is the whole collection and the rule refuses list-view-object-unknown; the three CLI doors keep skipping, because runAuthoringRules never states the flag and an unresolved binding there may be a dependency package's object.

Census, checked where reading allows. Every object: binding in the example view files names an object the same app declares or a platform object: crm crm_opportunity / crm_lead / crm_activity; showcase showcase_inquiry / showcase_task / showcase_project / showcase_field_zoo / showcase_contact / showcase_business_unit / showcase_account / sys_user; todo todo_task. examples/app-multi-package ships no view file. The served count of 49 is the dev's booted reading and is not re-derived here.

② Semver level

  • .changeset/24668-overlay-list-view-unknown-object.md: @objectstack/lint minor, BREAKING with the bang, FROM / TO / Fix stated (the overlay write, the draft promotion, a write naming a package the registry cannot produce), the stored-row sentence present and pinned by (e). Clause-②: yes (narrowing) on both carriers (the PR body and the changeset); yes is right because the public surface grows (one export, one option) and (narrowing) because the accept set shrinks, which is BREAKING and takes at least minor. .changeset/pre.json reads mode: exit. The ADR-0087 disposition not-required (no-migration-prescription) is right: no authorable key, spelling, export or stored shape moves, no stored row is read or rewritten, and the repair is the author's choice of binding. The head's Check Changeset job runs Require an ADR-0087 disposition (step 13) and Guard against accidental major bumps (step 15) green.
  • Deliberate correction of .changeset/24563-list-view-out-of-closure.md. One sentence, in the "Unchanged" bullet: "stays unjudged there" becomes "gets no closure verdict there; lint: an overlay write (no package) of a list view bound to an object that exists nowhere answers 200 with no issue — validateListViewFieldRefs skips it and no other rule judges a view write's binding #24668 refuses it as list-view-object-unknown". True: the old sentence is false once both notes ship in one 18.0.0 pass. The class is right: scripts/check-empty-changeset.mjs's two-class text names DELIBERATE CORRECTION as "your change may have made this PENDING release note false, and you rewrote it in the same stroke", and both PRs are the same seat's. The three red-by-design conditions hold: (1) the gate source says "do NOT restore it; get it confirmed on the PR", and the pr-automation.yml step text says "LEAVE THIS CHECK RED" and that Check Changeset is not a required context; (2) pr-automation.yml triggers on pull_request only, with no merge_group, so the queue build does not carry this red; (3) PR comment 6284866516 records the correction and the conditions. No skip-changeset label, which the gate refuses for this class. The job's red is step 12 alone (the foreign-changeset refusal); steps 11, 13 and 15 are green.

③ Boundary flags

  • Check-runs on the head, read 2026-10-20T06:37Z. Green (success): Lint and Repo Gates and every Lint · lane, TypeScript Type Check with Type Check · workspace, · source gates, · consumer gates, · debt ledger, Build Core, Build Docs, Governed Surface Queue Guard, Check PR Size, Check Documentation Links, Flag docs affected by code changes, the three claim guards, Auto Label, Spec property liveness, Dogfood Regression Gate (the gate job; its matrix shards are skipped, push-battery only), filter. skipped by design on a PR run: Temporal Conformance, Dogfood Verify CLI, Console Pin Gate, Packed-tarball smoke. failure: Check Changeset, the deliberate-correction class judged in ② and not a required context. Test Core, re-read 2026-10-20T06:42Z: shards 1/6 and 5/6 success; shards 2/6, 3/6, 4/6 and 6/6 still in_progress, so the required Test Core context has no conclusion yet. This verdict is on the contract and does not stand in for those four shards: the seat arms the queue only once Test Core reads success (Multi-agent discipline §7), and a red there is read before anything else is done with this record.
  • Dev deviation (1), the 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 pins. The dispatch's premise that they stay green unchanged was falsified for exactly the assertions that pinned the overlay silence; each re-true is the ruled consequence (①). Accepted.
  • Dev deviation (2), files beyond the claim. packages/lint/src/index.ts is the barrel contract's own requirement; runtime-gate.view-writes.test.ts is a lint pin, inside "the lint pins or a sibling"; the 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 changeset correction is confirmed by the seat (②). Accepted.
  • Dev deviation (3), the riders. The protocol pin is declared on [PM seat] domain:engine — 🟢 os-project-manager #6367 (6282830389), the 10 objectql fixtures by addendum there (6284875625), the 3 rest fixtures on [PM seat] domain:cli — ⏳ vacant #6024 (6284881226); all test-only, no protocol.ts, objectql or rest source change (the file list confirms). Accepted.
  • Dev deviation (4), the draft pin at the protocol layer only. Right: evaluateRuntimeAuthoringGate returns before any rule runs when state is not active; the lint gate has no draft state to pin.
  • Dev deviation (5), a new rule id. Sound (①).
  • Dev deviation (6), runtime-gate.ts and authoring-rules.ts untouched. Right: the signal already existed (①).
  • Dev deviation (7), model-free commit trailers. The AGENTS.md form; accepted.
  • Out-of-scope finding, authoring-rules.ts AuthoringRuleContext.packageScope docblock ("one missing from a whole one may come from another package"): now true only for the CLI, and it ships in the published declarations. The file is outside the claim's surface and in lint: a lookup or master_detail field whose reference is a stored-metadata-body table saves, builds and validates, but the engine never hydrates it — refuse it at authoring time #24677's possible path, so it is not fixed here: the seat carries it as a one-line truth fix in whichever of the two PRs lands second, or as its own comment-truth card. Not blocking.
  • Out-of-scope finding, the runtime-gate.ts comment at the packageScope statement ("from one a whole-collection snapshot merely does not carry"): the same staleness. The file is on the claim's surface, so a one-line truth fix would fit this PR without a breach; it is not a condition of this verdict, and it may ride with the item above. Not blocking.
  • Fault-path boundary, listRegisteredCollection answering [] on a throw. A silent registry-side degrade beside a warned store-side one (①). Pre-existing, no reproduction, and the same universe already decides three other refusals; the dev's boundary note is the right record, and no card is filed from this review. If the engine seat wants the registry-side catch to say what it lost, that is its own card, not this PR's.
  • Boundary, publishPackageDrafts for a package the registry cannot produce. Takes this verdict by construction through promoteDraftForPublish; a pin there is a test-only rider on the engine lane, not a condition.
  • Nothing escalated. No FAIL item.

Implemented-by: claude/issue-24668-overlay-list-view-unknown-object
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-20T06:54Z


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 20, 2026 06:55
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 20, 2026
Merged via the queue into main with commit faead58 Oct 20, 2026
41 of 42 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-24668-overlay-list-view-unknown-object branch October 20, 2026 07:25
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