Repository navigation
fix(lint)!: the overlay save door refuses a list view bound to an object that exists nowhere (#24668) - #24789
Conversation
…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>
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>
…ness registers Claude-Session: https://claude.ai/code/session_019oFY6Hi35J7atvgVFnmoaK Co-authored-by: Claude <noreply@anthropic.com>
… view to 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>
…erlay-list-view-unknown-object
📓 Docs Drift CheckThis PR changes 1 package(s): 4 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 4 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # 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
|
|
Seat confirmation · deliberate correction of a pending release note · This PR modifies
Confirmed: option A. The corrected sentence stays. Restoring the base sentence would publish two contradictory statements about the same door in one release.
⛔ No Generated by Claude Code |
Contract reviewServed-tier: Reviewed on the narrowing face, adversarially: the net diff against the merge base ① Derived judgmentsPublic surface of
The refusal fires only where "exists nowhere" is decidable — proven from the code, not the dispatch.
The finding (right).
#24563's pins, each re-true read against the ruling.
The 13 consumer fixtures (10
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: Docs. The Census, checked where reading allows. Every ② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
|
Contract review adopted ·
Generated by Claude Code |
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_METADATAwith onelist-view-object-unknownissue 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-crmwithpnpm dev:crm -- --fresh -p 38617(SQLite, 38 plugins), signed in as the seeded admin, and sentPUT /api/v1/meta/view/nosuch_obj.qavfy_r1with no?package=and the body{ name, object: 'nosuch_obj', viewKind: 'list', type: 'grid', data: { provider: 'object', object: 'nosuch_obj' }, columns: [{ field: 'name' }] }.4b114eda57;packages/lint/distbuilt from the base source, andablation-dist-preflight --absentconfirmed 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_r1served it with_diagnostics: {"valid": true}. The view's own query fails:GET /api/v1/data/nosuch_objanswered404 {"error":"Object 'nosuch_obj' is not registered","code":"OBJECT_NOT_FOUND"}.packages/lintrebuilt, the rule id confirmed indist/):HTTP 422,code: INVALID_METADATA, one issue{ rule: 'list-view-object-unknown', path: 'views[0].data.object', severity: 'error' }. The follow-upGETanswered404, so nothing was stored.examples/app-showcaseandexamples/app-todogave 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.
assertRuntimeAuthoringRulesgathersobjectsasregistry.listItems('object')folded with the stored active rows throughmergePackageAwareOverlay, the mergeGET /meta/:typeanswers from. It applies no disabled-package filter.resolveWritePackageScopeanswers no closure for a missing package id, for thesys_metadatasentinel, for a package the registry cannot produce, and for a registry withoutgetPackage. In all of those casesnarrowObjectsToPackageClosurehands the collection over whole.GET /api/v1/meta/objecton the CRM stack served 81 objects: 75sys_*objects from 15 platform and plugin packages, and 6com.example.crmobjects. After an overlay objecttenant_ticketwas saved with no package, it served 82, withtenant_ticketat_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 answered200and none was refusedlist-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.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'slistandlistViews. 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: unknowncolumnsfields are accepted (200 active, draft → publish) where the same body is refused 422list-view-field-unknown#24563. Route (b) would have to givevalidateObjectReferencesthat same walk a second time. And because that function takes onlystack, it would also change the CLI verdict unless it were made runtime-aware. Crossing it ontoviewwould also bring itsviews[].objectcontainer 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-unknowncarries the CLI ladder: a prefixed name only advises, and itsos explainentry describes that ladder.list-view-object-outside-closurenames a package and a dependency remedy, and this verdict has neither.The finding keeps the error family the ruling names:
errorseverity, refused422 INVALID_METADATAat the binding key.The CLI is unchanged. The reference-integrity suite states
wholeCollection: trueonly whenruntimeWriteTypeis set andpackageScopeis absent. The gate setsruntimeWriteTypeon every gated write, and the CLI commands never do. The gate statespackageScopeexactly when it narrowed (one predicate,packageScopeInForce). Soruntime-gate.tsdid not need an edit.The nearest name goes in the hint, never the message. The gate's differential fingerprints
rule,where,pathandmessage, and anobjectwrite changes the snapshot's object names by one. If the "Did you mean" were in the message, a stored object's list view bound tocrm_invocewould read differently once an unrelated write createdcrm_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:
list-view-object-unknownexamples/app-crmexamples/app-showcaseexamples/app-todoexamples/app-multi-packageships no view (git grep). In total, 0 shipped views bind an object that exists nowhere. The control, the same universe with onenosuch_objbinding, is refused every time. Deployed rows were not measured.Changes
packages/lint/src/validate-list-view-field-refs.ts: addsLIST_VIEW_OBJECT_UNKNOWN, theListViewFieldRefOptions.wholeCollectionoption, 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 A list view bound to an object outside the writing package's dependency closure skips the field-reference check: unknowncolumnsfields are accepted (200 active, draft → publish) where the same body is refused 422list-view-field-unknown#24563 section's "Only under a stated closure" bullet is corrected.packages/lint/src/reference-integrity-suite.ts: the member adapter stateswholeCollectionfor 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 A list view bound to an object outside the writing package's dependency closure skips the field-reference check: unknowncolumnsfields are accepted (200 active, draft → publish) where the same body is refused 422list-view-field-unknown#24563 one..changeset/24668-overlay-list-view-unknown-object.md:@objectstack/lintminor, 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 isnot-required (no-migration-prescription)..changeset/pre.jsonis inexitmode (measured)..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-changesetreds on purpose for this class (DELIBERATE CORRECTION). I left it red and did not applyskip-changeset. Please confirm this correction on this PR. The new sentence is: "An object that exists nowhere 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 —validateListViewFieldRefsskips it and no other rule judges aviewwrite's binding #24668 refuses it aslist-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):listwith its fallback key,list.data.object,listViews.*, the ViewItem record) and on anobjectwrite's built-in list view;wholeCollection;runAuthoringRules('validate' | 'build' | 'lint').Protocol (
packages/metadata-protocol/src/protocol.runtime-authoring-gate.test.ts, a new#24668block, the rider declared on [PM seat] domain:engine — 🟢 os-project-manager #6367), through the realsaveMetaItem:422 INVALID_METADATAatviews[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;list-view-field-unknownverdict;publishMetaItemis refused;A list view bound to an object outside the writing package's dependency closure skips the field-reference check: unknown
columnsfields are accepted (200 active, draft → publish) where the same body is refused 422list-view-field-unknown#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: twonosuch_objlines in "a write that names no package…";#24563case (c): its "nowhere" half;runtime-gate.view-writes.test.ts([finding] Both field-resolution gates for a list view (searchableFieldsandsort) are CLI-only — the reference-integrity suite dispatches at the runtime publish gate forflowalone, so a Studio/MCPviewwrite gets neither #9313): "an overlay bound to an object outside the live universe…", which now asserts exactly onelist-view-object-unknownatviews[0].object, with the sort axis still skipped.Every other A list view bound to an object outside the writing package's dependency closure skips the field-reference check: unknown
columnsfields are accepted (200 active, draft → publish) where the same body is refused 422list-view-field-unknown#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 outsidepackages/lint/packages/specthat 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 themyapp_invoiceit creates);environment-overlay-five-types(the package manifest now shipsprobe_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 count1 → 0and the blob moving for each leg. Each leg was restored, and the restore was proven by blob equal to HEAD and an emptygit diff HEAD.error→warning. The lint pins went red: 7 failed, 59 passed, over 3 files.packages/lint/dist: severityerror→warning. The mutated build was confirmed indist/byablation-dist-preflight. The protocol pins went red: 5 failed, 163 passed. Then I restored, rebuilt, ranpreflight --absent(exit 0, tree clean), and confirmed with a control that the fix is back indist/.All three went red, as predicted.
Tests and gates
These readings were all taken on the final head
3df138798d(the branch withorigin/main795ff708d8merged), after a fullturbo run buildof that tree. Each command ran underos-verify-lock.shwith--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 at39475b9113: 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/runtimeat39475b9113: 431 files passed, 7570 passed, 19 skipped;@objectstack/cli--project unitat54fb13a743: 314 files, 4643 tests passed. The integration tier is declared to CI;@objectstack/platform-objectsat54fb13a743: 79 files, 1195 tests passed.The merge brought no change to any of those packages that my diff overlaps.
typecheckon3df138798d, one&&chain, exit 0, 0error TS:@objectstack/lint(withcheck:test-typecheck: OK),@objectstack/metadata-protocol,@objectstack/objectql(withcheck:test-typecheck: OK), and@objectstack/rest(withcheck:test-typecheck: OK).Gates. I derived the list with
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackon3df138798d(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)".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/mainexit 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/mainexit 0: "This diff introduces nomajorbump";check-nul-bytesexit 0.The PR's own check-runs are CI's to report. I have not re-run them locally.
Acceptance notes
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.tsis 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: thedomain:specseat.runtime-gate.ts. The comment at thepackageScopestatement ("from one a whole-collection snapshot merely does not carry") has the same staleness. I did not edit it either. Carrier: thedomain:specseat.assertRuntimeAuthoringRules'slistRegisteredCollectionanswers[]whenregistry.listItemsthrows. 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'sobject-reference-unknownalready depends on the same collection. Recorded as a boundary; not filed.publishPackageDraftsfor a package the registry cannot produce takes this verdict too, by construction. It is not pinned here.Generated by Claude Code