Repository navigation
fix(rest,runtime)!: /layers and ?layers=true answer a disabled package's non-object item as a missing name, on both transports - #24267
Conversation
…object item absent One step in the shared layered chain (createMetaLayeredAnswer), after the no-layer absence and before the per-caller gate, answers a disabled package's item of every type but object and package as the missing name's absence. The owner is the plain read's kept envelope packageId under the same ?package= scope, asked of the engine registry's isPackageDisabled, through one optional source both transports wire. getMetaItemLayered is unchanged and keeps serving its resolution readers. Claude-Session: https://claude.ai/code/session_01JeRdtdgEYYdtYB4isWp3bh Co-authored-by: Claude <noreply@anthropic.com>
…bled package's item Both spellings on both transports, before, during and after the disable, for a code-shipped, an overlay-only and a combined item, with a control, an author and a member caller, the ?package= scope, the object row, and the engine slot's outage and absence. The #7557 table gains the producer and plugin-security's direct layered readers as resolution rows, and the objectQLProvider census counts the fifth consumer. Claude-Session: https://claude.ai/code/session_01JeRdtdgEYYdtYB4isWp3bh Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JeRdtdgEYYdtYB4isWp3bh Co-authored-by: Claude <noreply@anthropic.com>
…ta-disabled-layered-doors
📓 Docs Drift CheckThis PR changes 2 package(s): 7 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 5 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 35 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 69376fb00c7e874cbafe2df2d02302c76f6d9d03 && git checkout 69376fb00c7e874cbafe2df2d02302c76f6d9d03
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 53ef7524368075a1f64003b9dfb1609d3d206d69 9d275b85e3c5f624998f6047adc7c5b196c63730 && git checkout -B drift-repro 53ef7524368075a1f64003b9dfb1609d3d206d69 && git merge --no-ff 9d275b85e3c5f624998f6047adc7c5b196c63730
node scripts/docs-audit/affected-docs.mjs --json 53ef7524368075a1f64003b9dfb1609d3d206d69
|
Contract reviewServed-tier: Inputs: card #24210 (body; every comment, 6239527781 through 6246528708, including triage 6239967422, the seat ruling 6241893667, the claim 6245752510, the two landings 6243759173 and 6245732037, and this part's Check-runs at read time (2026-10-18T06:32:44Z): 39 runs; 27 ① Derived judgmentsA. One step, one chain, four doors. B. The source is optional, and an absent source withholds nothing. The step reads C. The owner read is the by-name read's own field and predicate. On REST: D. The E. What the extra read costs and can throw. It is paid only after step 1 (a missing name pays nothing), only for non-object, non-package types, and only once an engine with F. The resolution readers keep serving. G. The object row and H. The census. I. Accept-set and public-surface changes the diff implies, each judged. (1) REST What each door answers now (a disabled package's non-object item | triage's answer | a missing name), on this head, author and member alike:
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
|
CI red ·
Generated by Claude Code |
…n decision of what it hides The step asked the owner's disable state whatever the plain read answered and restated the protocol's never-hidden type in the chain, which the item-read census counted as a type with no parity cell. The source now answers withheld only when the plain read serves no item and its envelope names a disabled owner, so which types are hidden stays the protocol's. Claude-Session: https://claude.ai/code/session_01JeRdtdgEYYdtYB4isWp3bh Co-authored-by: Claude <noreply@anthropic.com>
…ta-disabled-layered-doors
|
CI red (addendum) ·
Generated by Claude Code |
…ide it takes Claude-Session: https://claude.ai/code/session_01JeRdtdgEYYdtYB4isWp3bh Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Inputs: card #24210 (body and every comment, 6239527781 through 6247240647: triage 6239967422, the seat ruling 6241893667, the claims 6245752510 and 6246547899, the two Check-runs on this head: at the first read Test Core (3/6) was ① Derived judgmentsA. The interdiff (the earlier head's net diff against this head's), every difference named. Four files move; the two changesets, the #7557 pin, the REST pin and the census test are byte-identical between the two net diffs. (1) B. The new condition is the protocol's own by-name decision, exactly. On this tree C. What the logic change moved relative to the earlier record (6246648398). On the real protocol the served set at the four doors is unchanged for every type but D. The census on the earlier head, and why it is green here for the dev's reason. E. Accept-set and public-surface changes the diff implies, each judged. (1) REST What each door answers now (a disabled package's non-object item | an object | a
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
席位核对 ·
|
| Item | Reading |
|---|---|
| Shape | Draft PR → main. The body opens Fixes #24210; this is the card's last part (the remaining list in landing 6245732037 was exactly these four doors). Both card/branch claim checks are green. |
| Scope | 9 files (+464 / −21), claim 6245752510 as re-declared in 6246547899. One step in the shared chain createMetaLayeredAnswer, with one optional source wired on both transports and one optional request member (the ?package= scope). Three pin files, the census, two changesets. No route body; getMetaItemLayered untouched. Not governed (check-governed-merges.mjs --pr 24267: 0 of 9). |
| Rework | Head ed6b7108ec had record 6246648398 (PASS) and then went red twice (notes 6246674049 and 6246896684). First, the runtime repo parity census: metaType !== 'package' had made package a keyed type with no cell. Then a test-typecheck error in the new pin. The fix (report 6247240647) drops that guard, and each transport's source now withholds only when the plain read serves no item AND its envelope names a disabled owner. So the chain never restates which types the protocol hides. The seat diffed both net diffs against their own merge-bases: exactly those lines, plus one new pin and a test-double type, differ. The merge 5d2ec3188 has an empty remerge-diff. |
| Doors | Non-object /layers and ?layers=true, on both transports, answer a disabled package's item with the missing name's bytes, for an author and a member alike; re-enable restores each caller's view. Objects keep 404 OBJECT_PACKAGE_DISABLED. package items are served, as the plain read serves them. A shared name under ?package= of an enabled package is served. |
| Review face | Narrowing (Clause-②: no (narrowing)). This head has its own CONTRACT_REVIEW_TIER record, 6247381651: PASS. It shows the new condition is extensionally the protocol's own getMetaItem decision, and that the census is green with neither the census file nor vitest.repo-tests.json touched. The seat re-verified the interdiff in the tree. |
| Prose face | Two changesets, unchanged since ed6b7108ec: minor, BREAKING, Clause-②: no (narrowing), and the adr-0087 not-required (no-migration-prescription) marker. Their served-set sentence ("every type but object and package") still holds exactly. Not spelled in them: these doors answer a plain-read fault as itself, and REST answers 503 on a rejecting engine slot, now including package's layered reads. The PR body names both, as on PR #24247; accepted without a changeset edit. No sentence is false. |
| Gates | Rework report at 9d275b85e: 70 derived, 70 run, 0 NOT MEASURED. For rest, runtime and objectql: typecheck, test and test:repo all exit 0 (runtime test:repo 808/808). The item == null ablation reddens exactly the new pin; the restore is clean. |
| CI | At this read: 47 check-runs, 39 success, 8 skipped, 0 failed. Test Core (6/6) and both Type Check lanes, red on the earlier heads, are green. The skips are the six path or opt-in skips plus the body-edit re-runs of Auto Label and Check PR Size, all on the EXPECTED_SKIPS roster. |
Disposition:
open_questionsof report 6246528708: A, A and A, as the record answers them: the owner is the plain read's envelope; the scope is one optional request member; the dispatcher'sgetObjectQLServicefail-open is an existing seam property, below the filing gate, and recorded on the seat post.- Report 6247240647 adds no
open_questions. Its deviations are accepted: round 1 ran only runtime'slocalproject, and this round rantestandtest:repofor all three packages. - Named, nothing owed (record ③.12):
package's layered reads now pay the owner read; the served set is unchanged.
Landing: ordinary queue (pr_ready + automerge_enable through the relay). On merge, #24210 closes.
Generated by Claude Code
Fixes #24210
Clause-②: no (narrowing) (owes one
CONTRACT_REVIEW_TIERreview before the queue)The card's last part (claim 6245752510): non-object
GET /meta/:type/:name/layersand the deprecatedGET /meta/:type/:name?layers=true, on both transports, now answer a disabled package's item exactly as a missing name, as the list read and the by-name read already do. PR #24223 and PR #24247 closed every other door; this part closes the last two spellings.What changed
packages/rest/src/meta-item-read-gate.ts, the shared layered chaincreateMetaLayeredAnswer: one step, 1b, after step 1 (no layer, so absent) and before step 2 (the per-caller gate). For every type butobject, it answers{ reason: 'absent' }when the new optional sourceMetaItemReadGateSources.isItemOfDisabledPackagesays the plain read withholds the item for a disabled owner. Which types are hidden stays the plain read's decision (packagenever is), not a second copy in the chain. Both spellings on both transports call this chain, so no transport carries a step of its own.MetaLayeredRequestgains an optionalpackageId(the?package=scope), which the step passes to the source.RestServerclass membermetaItemReadGateSources. Withheld means the plain read under the same scope serves noitemand its kept envelopepackageIdis one the engine registry'sisPackageDisabledanswerstruefor. The engine is reached throughwiredEngineOrLoud, as/publishedreaches it.metaItemReadGateSourcesinruntime/src/domains/meta.ts. The same rule, throughdeps.getObjectQLandisServiceItemOfDisabledPackage.serveMetaItemLayered(one property) andanswerMetaLayered(one property).getMetaItemLayered, per ruling 6241893667 Q1 A: the producer keeps serving its resolution readers.404 OBJECT_PACKAGE_DISABLED, thrown by the read before the chain runs.package, which is never hidden.registerMetadataEndpointsInner,meta-item-read-routes.tsormeta-collection-routes.ts. No spec change, no new error code, no new public protocol method.The PM's hypotheses, measured
SchemaRegistry, before and after the disable:packageId, and itscodeandeffectivelayers carry_packageId.packageId, and_packageIdon neitheroverlaynoreffective.packageIdandcode._packageId, but noteffective._packageId.packageIdnames the owner in all three cases, withheld item or not. That field isextractProtectionof the served document, the same_packageIdthe by-name read's predicateisItemOfDisabledPackageasks. It is also what/publishedand plugin-security'sreadServedPackageBindingalready read.503 SERVICE_UNAVAILABLE, never a layered view. A plain read that rejects is answered as that fault, never a layered view. On the dispatcher see the Acceptance notes.meta-disabled-package-doors.test.ts(REST).security-catalog-echo.tsandpackaged-permission-set-lock-gate.tscallgetMetaItemLayereddirectly; REST'sfetchCurrentMetaDocumentcallsgetMetaItemthengetMetaItemLayereddirectly. None goes through the chain. The Package disable and uninstall never reach the metadata/data layer: a disabled package's objects still serve rows, and uninstall leaves 7 orphanedsys_metadatarows #7557READERStable gains three resolution rows: the producer for an app, the producer for a permission set (the two plugin readers), and the plain read's envelopepackageId(the owner every one of these reads).Measured: the
?package=scope mattersThe same name can be stored in two packages. On the real protocol,
twin_dashwas saved in a disabled package and in an enabled one.getMetaItemLayered({ packageId })answers each package's copy, and the unscoped by-name read resolves the disabled package's copy. An unscoped owner check would therefore withhold the enabled package's layered view under?package=of the enabled package. So the scope rides into the owner read. That needspackageIdonMetaLayeredRequest, and so a one-property edit in each caller of the chain (see Deviations).HTTP, before and after, both transports
REST:
pnpm dev:crm -- --freshover a real socket. "Before" is basee74ec56ac(the three source files restored to the base, thenrestandruntimerebuilt; no other bundle carries the step). "After" is this branch after a fullpnpm build(76/76).app.probe-24210c(disabled) andapp.healthy-24210c(control).probe_saved_dash, pageprobe_page, appprobe_app, objectprobe_gadget.twin_dashis stored in both packages./auth/admin/create-user.PATCH /packages/com.example.crm/disableanswers422 WRITABLE_PACKAGE_REQUIRED). So a code-shipped or combined item of a disabled package is not reachable on a live server. Those shapes are measured on the dispatcher harness below and pinned on REST with the real protocol./layers, dashboard, page, appRESOURCE_NOT_FOUND, byte for byte the missing name?layers=true, same threeDeprecation;Linknames its own path)/layersand?layers=true,twin_dash?package=app.probe-24210ctwin_dash?package=app.healthy-24210c(control)/layersand?layers=true, objectprobe_gadgetOBJECT_PACKAGE_DISABLEDhealthy_saved_dash,pipeline_dashboard(shipped plus overlay, enabled package)Mechanical comparison over the stored readings:
Dispatcher: the real
createHonoAppover a realLiteKernelwithObjectQLPluginandSqliteWasmDriver, through its fetch handler (app.request; no in-repo host mounts the dispatcher's/metaon a socket). It is thepackages/qa/http-conformanceshape, in a scratch harness that was removed afterwards. The disabled package (installedcode, disabled on the registry) carries a code-shipped page, an overlay-only dashboard and a combined dashboard; the control package carries the same. Callers are an author (manage_metadata) and a member.Linkaside (it names its own path).Pins
packages/rest/src/meta-disabled-package-doors.test.ts(real protocol andSchemaRegistry), five new cases:?package=scope;wiredEngineOrLoud: rejecting is503, absent serves as before.packages/runtime/src/domains/meta-disabled-package-doors.test.ts, four new cases:?package=scope, including the scope the plain read was asked under;packages/objectql/src/package-disable-enforcement.test.ts: the threeREADERSresolution rows above.packages/rest/src/objectql-slot-consumer-census.test.ts: the census counts a fifthobjectQLProviderconsumer (the layered owner step, throughwiredEngineOrLoud) and cites its pin. The census went red on the new consumer before this edit, as designed.Verification
Rework after CI (seat message,
Test Core (6/6)oned6b7108ec).@objectstack/runtime'stest:repo(vitest run --project repo) failed:meta-list-projection-parity.test.ts, "every type the item read keys on, and every translatable type, is a census cell",AssertionError: expected [ 'package' ] to deeply equal []. Reproduced locally. The census derives every type literal the item read's handlers and shared steps compare against, and step 1b'smetaType !== 'package'madepackagea keyed type with no parity cell. The literal also restated a decision the protocol already makes. Fix in527c7bc4ba(and the test type in9d275b85e3); the lines that moved againsted6b7108ec:meta-item-read-gate.ts: the step's condition drops&& metaType !== 'package'(one line); the source's and step 1b's docblocks say what withheld means.rest-server.ts(metaItemReadGateSourcesonly): the plain read's cast gainsitem, and the return becomesenvelope?.item == null && registry.isPackageDisabled(envelope?.packageId) === true(two lines; docblock three lines).runtime/src/domains/meta.ts(metaItemReadGateSourcesonly): the same two-line change (docblock three lines).meta-disabled-package-doors.test.ts(runtime): one new case, "the plain read decides which types are hidden: an item it still serves keeps its layered view", and the double'sgetMetaItemtypedPromise<any>so the case can override it. Ablating theitem == nullhalf turns exactly that case red (1 failed / 13 passed), restored blob equal to HEAD.Final head
9d275b85e3(withorigin/main53ef752436merged, PR #24260 included, textually clean).pnpm --filter '@objectstack/runtime...' --filter '@objectstack/objectql...' buildexit 0; fullpnpm build76/76.@objectstack/rest: typecheck 0;test0 (295 files, 6047 passed, 326 skipped);test:repo0 (5 files, 270 passed, 16 skipped).@objectstack/runtime: typecheck 0 (on9d275b85e3; it was 1 on the commit before, the test double's type, fixed there);test0 (416 files, 6552 passed, 19 skipped);test:repo0 (3 files, 808 passed).@objectstack/objectql: typecheck 0;test0 (437 files, 8244 passed);test:repo0 (1 file, 5 passed).test:repo(808/808) were re-run on9d275b85e3.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack(no paths) derived 70 commands at9d275b85e3. 69 exited 0 on the first pass;check:dual-build-cjs-loadsrefused withPREREQUISITE NOT MET(exit 3, nodist/in a fresh worktree) and exited 0 after the full build.--ran: "70 derived, 70 run, 0 NOT-MEASURED, 0 UNRUN" (exit 0).ed6b7108ec(68/68). The rework narrows when the step fires (only when the plain read serves no item), which every withheld row above already satisfies.Deviations
rest-server.ts'smetaItemReadGateSourcesonly. This PR also adds one property,packageId, to the request literalserveMetaItemLayeredhands the chain.serveMetaItemLayeredis a class member, outsideregisterMetadataEndpointsInnerand outsidemeta-item-read-routes.ts. On the dispatcher the same edit lands inanswerMetaLayered. The reason is the measured?package=collision above: without the scope, the owner step would withhold an enabled package's copy of a shared name.packages/rest/src/objectql-slot-consumer-census.test.tsis not on the claim's file list. Its count moves from 4 to 5, because the census exists to make each new consumer examined, the way the second half moved it from 3 to 4.Acceptance notes
HttpDispatcher.getObjectQLServicecatches a failedobjectqlresolution and answersnull. On the dispatcher, then, an engine that cannot be resolved reads as "no registry to ask", and the layered doors serve as before. That is what the dispatcher's/publishedowner check, landed in PR fix(rest,runtime,metadata-protocol)!: /published, REST /state, _drafts and the event-door absence body answer a disabled package's item as the by-name read does #24247, already does; REST is loud (503). This is read in code, an existing property of the dispatcher's seam, not new here. A loud answer there means changing that seam for every dispatcher consumer, which is outside this card. No class; no carrier.Generated by Claude Code