Repository navigation
Commit faead58
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
File tree
- .changeset
- content/docs/deployment
- packages
- lint/src
- metadata-protocol/src
- objectql/src
- rest/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
21 | 21 | | |
22 | 22 | | |
23 | 23 | | |
24 | | - | |
| 24 | + | |
25 | 25 | | |
26 | 26 | | |
27 | 27 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
713 | 713 | | |
714 | 714 | | |
715 | 715 | | |
716 | | - | |
| 716 | + | |
| 717 | + | |
| 718 | + | |
| 719 | + | |
| 720 | + | |
| 721 | + | |
717 | 722 | | |
718 | 723 | | |
719 | 724 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
493 | 493 | | |
494 | 494 | | |
495 | 495 | | |
| 496 | + | |
| 497 | + | |
| 498 | + | |
496 | 499 | | |
497 | 500 | | |
498 | 501 | | |
499 | 502 | | |
500 | 503 | | |
| 504 | + | |
501 | 505 | | |
502 | 506 | | |
503 | 507 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
318 | 318 | | |
319 | 319 | | |
320 | 320 | | |
| 321 | + | |
| 322 | + | |
| 323 | + | |
| 324 | + | |
| 325 | + | |
| 326 | + | |
| 327 | + | |
| 328 | + | |
| 329 | + | |
| 330 | + | |
| 331 | + | |
321 | 332 | | |
322 | 333 | | |
323 | 334 | | |
324 | 335 | | |
325 | | - | |
| 336 | + | |
| 337 | + | |
| 338 | + | |
| 339 | + | |
| 340 | + | |
| 341 | + | |
| 342 | + | |
| 343 | + | |
326 | 344 | | |
327 | 345 | | |
328 | 346 | | |
| |||
679 | 697 | | |
680 | 698 | | |
681 | 699 | | |
682 | | - | |
| 700 | + | |
| 701 | + | |
683 | 702 | | |
684 | 703 | | |
685 | 704 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
385 | 385 | | |
386 | 386 | | |
387 | 387 | | |
388 | | - | |
| 388 | + | |
389 | 389 | | |
390 | | - | |
391 | | - | |
| 390 | + | |
| 391 | + | |
| 392 | + | |
392 | 393 | | |
393 | 394 | | |
394 | | - | |
| 395 | + | |
395 | 396 | | |
396 | 397 | | |
397 | | - | |
| 398 | + | |
398 | 399 | | |
399 | 400 | | |
400 | 401 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
123 | 123 | | |
124 | 124 | | |
125 | 125 | | |
126 | | - | |
127 | | - | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
128 | 131 | | |
129 | | - | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
130 | 135 | | |
131 | 136 | | |
132 | 137 | | |
| |||
0 commit comments