Skip to content

Commit faead58

Browse files
fix(lint)!: the overlay save door refuses a list view bound to an object that exists nowhere (#24668) (#24789)
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 #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 - `packages/lint/src/validate-list-view-field-refs.ts`: adds `LIST_VIEW_OBJECT_UNKNOWN`, the `ListViewFieldRefOptions.wholeCollection` option, and the refusal in skip 1's branch (a closure still wins when both are stated). The module docblock gains the section "A binding that exists nowhere in the whole collection is REFUSED", and the #24563 section's "Only under a stated closure" bullet is corrected. - `packages/lint/src/reference-integrity-suite.ts`: the member adapter states `wholeCollection` for a runtime write with no closure. - `packages/lint/src/index.ts`: exports the new id (the barrel contract, `rule-id-barrel-exports.test.ts`). - `content/docs/deployment/validating-metadata.mdx`: one sentence beside the #24563 one. - `.changeset/24668-overlay-list-view-unknown-object.md`: `@objectstack/lint` `minor`, BREAKING, with FROM / TO / Fix and the stored-row sentence. `Clause-②: yes (narrowing)`: the public surface grows (a new export and a new option) and the accept set narrows. The ADR-0087 disposition is `not-required (no-migration-prescription)`. `.changeset/pre.json` is in `exit` mode (measured). - **Deliberate correction of a pending release note.** I edited one "Unchanged" bullet of `.changeset/24563-list-view-out-of-closure.md`. It said "An object that exists nowhere stays unjudged there", which this PR makes false, and it ships in the same 18.0.0 pass. `check-empty-changeset` reds on purpose for this class (DELIBERATE CORRECTION). I left it red and did not apply `skip-changeset`. **Please confirm this correction on this PR.** The new sentence is: "An object that exists nowhere gets no closure verdict there; #24668 refuses it as `list-view-object-unknown` (see that changeset)." Why this correction rides in this PR: it is the same verdict, it is a one-sentence change, the file is held by no open claim, and the changeset gates already read it. ## Pins - **Lint** (`packages/lint/src/runtime-gate.whole-collection.test.ts`, new, 14 cases): - the refusal, at the binding, with the field positions unjudged; - the refusal on every rung (container `list` with its fallback key, `list.data.object`, `listViews.*`, the ViewItem record) and on an `object` write's built-in list view; - a binding that resolves keeps its field verdict, for package, stranger, platform and overlay objects; - an empty collection judges nothing; - a closure wins over `wholeCollection`; - a stored sibling's dangling binding is charged neither to a flow write nor to an object write that creates the object nearest to it; - the CLI is unchanged: the rule with no statement, the suite with no runtime type, and `runAuthoringRules('validate' | 'build' | 'lint')`. - **Protocol** (`packages/metadata-protocol/src/protocol.runtime-authoring-gate.test.ts`, a new `#24668` block, the rider declared on #6367), through the real `saveMetaItem`: - (a) the overlay write is refused `422 INVALID_METADATA` at `views[0].data.object`, with nothing stored, and the controls are admitted: a package's object, a stranger package's object, a platform object, and a stored overlay object; - (b) a resolving binding keeps its `list-view-field-unknown` verdict; - (c) a draft is admitted, and its promotion through `publishMetaItem` is refused; - (d) a write naming a package the registry cannot produce takes this verdict; - (e) a stored view loads at boot, and its next save is refused with the row untouched. - **#24563's pins.** The dispatch assumed they would stay green unchanged. That was falsified for exactly the assertions that pinned the overlay silence this ruling reverses, and I re-trued each one to the new verdict: - `runtime-gate.package-closure.test.ts`: two `nosuch_obj` lines in "a write that names no package…"; - the protocol `#24563` case (c): its "nowhere" half; - `runtime-gate.view-writes.test.ts` (#9313): "an overlay bound to an object outside the live universe…", which now asserts exactly one `list-view-object-unknown` at `views[0].object`, with the sort axis still skipped. Every other #24563 assertion is untouched and green. ## 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 #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](https://claude.ai/code/session_019oFY6Hi35J7atvgVFnmoaK)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent a43405d commit faead58

23 files changed

Lines changed: 637 additions & 33 deletions

‎.changeset/24563-list-view-out-of-closure.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ Clause-②: yes (narrowing)
2121
**Unchanged.**
2222

2323
- A list view bound to an object inside the closure (the package's own, a declared dependency's, a platform/system object, an overlay row): same field verdicts, same refusals.
24-
- A write that names no package (an overlay write) is judged against the whole collection, exactly as before. An object that exists nowhere stays unjudged there.
24+
- A write that names no package (an overlay write) is judged against the whole collection, exactly as before. An object that exists nowhere gets no closure verdict there; #24668 refuses it as `list-view-object-unknown` (see that changeset).
2525
- The flow door: `flow-trigger-unknown-object` stays an advisory.
2626
- `os validate`, `os build` and `os lint`: no closure is stated, so the rule skips an unresolvable binding as before.
2727
- `OS_ALLOW_UNLINTED_METADATA_WRITES=1` still turns a refusal into a logged write.
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
---
2+
'@objectstack/lint': minor
3+
---
4+
5+
fix(lint)!: the save door refuses a list view written with no package and bound to an object that exists nowhere (#24668)
6+
7+
Clause-②: yes (narrowing)
8+
9+
**Why.** A runtime write that names no package (an overlay write) is judged against the whole collection the deployment serves: every installed package's objects, the platform's, and the stored rows. Nothing narrows it. The list-view field rule still read a binding missing from that collection as "another package's object", which is the CLI's question, and skipped the whole list view. So a list view bound to a misspelled or deleted object was stored with every field reference unjudged: `200`, no advisory. It then failed only at render, because the data door answers `404 OBJECT_NOT_FOUND` for the object. The same body written into a package is refused `list-view-object-outside-closure`, and a dataset over an unknown object is refused `object-reference-unknown`.
10+
11+
**What is refused.** A list view judged against the whole collection whose bound object exists nowhere in it is refused with one `list-view-object-unknown` issue. The issue is located at the key the binding is read from: `views[0].data.object` for a standalone list view, otherwise the container's `objectName` / `object`. An object whose built-in `listViews` entry retargets `data.object` to an object that exists nowhere is refused the same way. The list view's field references stay unjudged, because there is no field map to judge them against. The hint names the nearest existing object when one is close.
12+
13+
**BREAKING — what moves for consumers.** This is an accept-set narrowing on the metadata save door, shipped as `minor` under the launch-window convention for accept-set narrowings (`.changeset/pre.json` is in `exit` mode, so the next version pass cuts 18.0.0 and this change ships in it).
14+
15+
- FROM: a publish-mode write that names no package, of a list view bound to an object that exists nowhere, answered `200` and was stored, with no issue. This covers `PUT /api/v1/meta/view/:name` with no `?package=` (and `saveMetaItem` in publish mode), the promotion of such a draft (`POST /api/v1/meta/view/:name/publish`, `publishMetaItem`), and a write naming a package the registry cannot produce, which narrows nothing.
16+
- TO: the same writes answer `422 INVALID_METADATA`, with the `list-view-object-unknown` issue at the binding, and nothing is stored.
17+
- Fix: bind the view to an object that exists (the hint names the nearest one), or save the object first, then save the view again. Once the binding resolves, its fields are judged as before: an unknown `columns` field is refused `422 list-view-field-unknown`. Saving the view as a draft (`mode: 'draft'`) is still allowed, because drafts are never gated; publishing that draft is judged.
18+
19+
**The next save of a stored list view bound to an object that exists nowhere is refused with the fix; stored views keep serving until then.** The door judges writes only. A stored row is not re-judged at boot, and it loads and is served as before.
20+
21+
**Unchanged.**
22+
23+
- A list view bound to an object that exists (an installed package's, a platform object, a stored overlay object): same field verdicts, same refusals.
24+
- A write into a package is judged against its closure: `list-view-object-outside-closure` as before. When both could apply, the closure verdict wins.
25+
- `os validate`, `os build` and `os lint`: the rule skips an unresolvable binding as before. There a stack is one app, and an unresolved binding may be a dependency package's object.
26+
- A host that supplies no object collection at all: nothing is judged, as before.
27+
- `OS_ALLOW_UNLINTED_METADATA_WRITES=1` still turns a refusal into a logged write.
28+
- Measured before crossing, through the booted door (`pnpm dev:<app> -- --fresh`): the 49 views shipped in `examples/app-crm` (10), `app-showcase` (36) and `app-todo` (3), and the built-in list views of every object each stack serves, bind no object that exists nowhere. A probe list view bound to each object those stacks serve (82 in the CRM stack, one of them an overlay object saved with no package, then 109 and 76) was admitted every time. `examples/app-multi-package` ships no view. Deployed rows were not measured.
29+
30+
**API.**
31+
32+
- `validateListViewFieldRefs(stack, options?)` accepts `wholeCollection: true` in `ListViewFieldRefOptions`. The reference-integrity suite sets it for a runtime write with no package closure in force; `runAuthoringRules` never does.
33+
- `LIST_VIEW_OBJECT_UNKNOWN` (`'list-view-object-unknown'`) is exported.
34+
35+
<!-- adr-0087: not-required (no-migration-prescription) a refusal at the metadata save door of a list view written with no package and bound to an object that exists nowhere in the collection the deployment serves: no authorable key, spelling, export or stored shape moves, and no stored row is read, rewritten or converted. A stored list view with such a binding keeps loading until it is next saved, and the repair is the author's choice of which object to bind (or to save the object first), which no ledger entry can derive. The other categories are closed on facts: the package publishes (not unpublished); no ADR-0087 id covers this door (not already-registered); and the change is a door verdict, not a declaration or an export (not runtime-interface-only, type-surface-only or runtime-export-retired). -->

‎content/docs/deployment/validating-metadata.mdx‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -713,7 +713,12 @@ That changes what a rule can **resolve**, never which rules run, and nothing at
713713
this door walks `packages[]`. One rule is told the closure is in force: a list
714714
view bound to an object outside it is refused with
715715
`list-view-object-outside-closure` instead of having its field references
716-
skipped (#24563). So "runs per package" names one thing at the three
716+
skipped (#24563). With no closure in force — a write that names no package —
717+
the context is the whole collection the deployment serves, so the same rule
718+
refuses a list view bound to an object that exists nowhere in it with
719+
`list-view-object-unknown` (#24668); the three CLI doors keep skipping such a
720+
binding, because there it may be a dependency package's object. So "runs per
721+
package" names one thing at the three
717722
CLI doors and another one here, and the `runtime publish` column says neither.
718723

719724
A clean run walks the registry and reports timing:

‎packages/lint/src/index.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -493,11 +493,15 @@ export type {
493493
// [#24563] Its THIRD: a list view judged under a package closure whose bound
494494
// object is outside that closure — refused (`error`) where skip 1 judged
495495
// nothing; `ListViewFieldRefOptions` is how the runtime gate states the closure.
496+
// [#24668] Its FOURTH: a list view judged against the whole collection (a
497+
// runtime write that names no package) whose bound object exists nowhere —
498+
// refused the same way; `wholeCollection` is the statement.
496499
export {
497500
validateListViewFieldRefs,
498501
LIST_VIEW_FIELD_UNKNOWN,
499502
LIST_VIEW_FIELD_DOTTED,
500503
LIST_VIEW_OBJECT_OUTSIDE_CLOSURE,
504+
LIST_VIEW_OBJECT_UNKNOWN,
501505
} from './validate-list-view-field-refs.js';
502506
export type {
503507
ListViewFieldRefFinding,

‎packages/lint/src/reference-integrity-suite.ts‎

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -318,11 +318,29 @@ export const REFERENCE_INTEGRITY_RULES: readonly ReferenceIntegrityRule[] = [
318318
// missing-collection argument above is unchanged: the binding is judged
319319
// against `stack.objects`, the collection every per-write snapshot carries,
320320
// and the gate states the scope only when it narrowed that collection.
321+
//
322+
// [#24668] And the other half of that sentence: a runtime write with NO
323+
// closure in force is judged against the whole collection the host serves
324+
// (`RuntimeStackContext.objects`: registry plus stored rows, unnarrowed), so
325+
// a binding missing from it exists nowhere, and the member refuses it
326+
// (`list-view-object-unknown`, `error`) — stated as `wholeCollection`.
327+
// `runtimeWriteType` is the signal because the gate sets it on every gated
328+
// write and the CLI commands never do, and `packageScope` is absent exactly
329+
// when the gate narrowed nothing (one predicate, `packageScopeInForce`). The
330+
// CLI keeps skip 1: its stack is one app, and an unresolved binding there
331+
// may be a dependency package's object.
321332
{
322333
name: 'validateListViewFieldRefs',
323334
runtimeTypes: ['flow', 'view', 'object'],
324335
run: (stack, ctx) =>
325-
validateListViewFieldRefs(stack, ctx?.packageScope ? { packageScope: ctx.packageScope } : undefined),
336+
validateListViewFieldRefs(
337+
stack,
338+
ctx?.packageScope
339+
? { packageScope: ctx.packageScope }
340+
: ctx?.runtimeWriteType !== undefined
341+
? { wholeCollection: true }
342+
: undefined,
343+
),
326344
},
327345
// [#15254] The same question, one surface IN: the field-name lists the
328346
// OBJECT itself carries (`highlightFields`, `publicSharing.redactFields`),
@@ -679,7 +697,8 @@ export interface ReferenceIntegrityRunOptions {
679697
* `stack.objects` to — spells the same key as
680698
* `AuthoringRuleContext.packageScope`. Present only when a closure was in
681699
* force; ABSENT on the CLI commands and on a write that names no package.
682-
* Read by `validateListViewFieldRefs` only.
700+
* Read by `validateListViewFieldRefs` only — which reads its absence beside
701+
* a set `runtimeWriteType` as the whole collection (#24668).
683702
*/
684703
packageScope?: { readonly packageId: string; readonly dependencies: readonly string[] };
685704
}

‎packages/lint/src/runtime-gate.package-closure.test.ts‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -385,16 +385,17 @@ describe('the view door under a package closure (#24563)', () => {
385385
expect(scoped(listViewOn('tenant_overlay')).advisories).toEqual([]);
386386
});
387387

388-
it('a write that names no package is unchanged — the whole collection, field verdict and silence alike', () => {
388+
it('a write that names no package is judged against the whole collection — field verdict unchanged, no closure verdict', () => {
389389
// ⛔ The overlay door: the stranger's object is in the whole collection, so
390-
// its unknown column is refused exactly as before, and an object that
391-
// exists nowhere stays skip 1's silence (no scope, so no closure verdict).
390+
// its unknown column is refused exactly as before. An object that exists
391+
// nowhere gets no closure verdict (no scope); [#24668] it is refused as
392+
// existing nowhere instead (`runtime-gate.whole-collection.test.ts`).
392393
expect(rules(whole(listViewOn('other_0_0')).errors)).toEqual([FIELD_UNKNOWN]);
393394
expect(whole(listViewOn('other_0_0')).advisories).toEqual([]);
394-
expect(whole(listViewOn('nosuch_obj'))).toMatchObject({ errors: [], advisories: [] });
395+
expect(rules(whole(listViewOn('nosuch_obj')).errors)).toEqual(['list-view-object-unknown']);
395396
// An empty package id narrows nothing, so it states no closure either.
396397
const empty = { packageId: '', dependencies: [] } as RuntimePackageScope;
397-
expect(scoped(listViewOn('nosuch_obj'), empty)).toMatchObject({ errors: [], advisories: [] });
398+
expect(rules(scoped(listViewOn('nosuch_obj'), empty).errors)).toEqual(['list-view-object-unknown']);
398399
});
399400

400401
it('the flow verdict is unchanged, and a stored object\'s out-of-closure list view is not charged to it', () => {

‎packages/lint/src/runtime-gate.view-writes.test.ts‎

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -123,10 +123,15 @@ describe('a flattened list overlay at the runtime publish gate (#9313)', () => {
123123
expect(errors, JSON.stringify(errors)).toEqual([]);
124124
});
125125

126-
it('an overlay bound to an object outside the live universe is not judged', () => {
127-
// Skip ① — the same cross-package posture every walked surface takes.
126+
it('an overlay bound to an object outside the live universe: its field positions are not judged, its binding is', () => {
127+
// Skip ① for the field axes — one typo is one finding, never one per
128+
// position. [#24668] The binding itself is refused once: with no package
129+
// named, the live universe is the whole collection, so an object missing
130+
// from it exists nowhere (`runtime-gate.whole-collection.test.ts`).
128131
const { errors } = gate(overlay({ object: 'pkg_external_case', sort: 'no_such_field' }));
129-
expect(errors, JSON.stringify(errors)).toEqual([]);
132+
expect(errors.map((e) => [e.rule, e.path]), JSON.stringify(errors)).toEqual([
133+
['list-view-object-unknown', 'views[0].object'],
134+
]);
130135
});
131136

132137
// ── [#14107] the rest of the same overlay's field surface ──

0 commit comments

Comments
 (0)