Repository navigation
fix(spec,objectql): installed pages carrying a retired declaration convert at load instead of stopping the kernel build - #23962
Conversation
…onvert at load instead of stopping the kernel build Two retired ADR-0087 D2 entries (page-is-default-off-record-page-removed, page-template-full-width-to-default) and the versioned forward conversion at the kernel's package registration door for a body that declares its protocol range; the authoring, save and publish doors keep refusing. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016MPJ3Y2xUTT42wq9ZiK3MR
…ields Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016MPJ3Y2xUTT42wq9ZiK3MR
…version Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016MPJ3Y2xUTT42wq9ZiK3MR
📓 Docs Drift CheckThis PR changes 3 package(s): 22 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 7 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 143 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 50fd681ea1d398d232fe704c87edc590484ac692 && git checkout 50fd681ea1d398d232fe704c87edc590484ac692
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 68ffa5dc1af1abeb012cd1ed7dfa18be4c07a8c4 6c52a63bd1e8b4c9fb59c366299a26b6f5650d10 && git checkout -B drift-repro 68ffa5dc1af1abeb012cd1ed7dfa18be4c07a8c4 && git merge --no-ff 6c52a63bd1e8b4c9fb59c366299a26b6f5650d10
node scripts/docs-audit/affected-docs.mjs --json 68ffa5dc1af1abeb012cd1ed7dfa18be4c07a8c4
|
Contract review — PR #23962 (card #23904) · 2026-10-16T15:10ZServed-tier: Inputs read, and nothing else: card #23904 (body and all five comments, 6215481679 through 6217917388), PR #23962 (body, its 14-file list, the net diff against Gate verdicts on this head (the check-runs)
① Derived judgmentsEvery accept-set and public-surface change the diff implies, each named right or wrong:
② Semver level
③ Boundary flagsThe dev's flags (report 6217917388: deviations, open_questions, out-of-scope findings), each answered or escalated:
What turns this to PASS on the next head: Test Core (1/6) and (5/6) green, so the aggregate Implemented-by: VERDICT: FAIL |
…se-naming prescriptions and repin the stored-row save-door reads The misplaced-isDefault refusal now names the release that first refused the flag (@objectstack/spec 18), as the removal-version pin requires of every freshly retired D2 entry; the save-door test reads a stored row back through the conversion chain. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016MPJ3Y2xUTT42wq9ZiK3MR
Contract review — PR #23962 (card #23904) · 2026-10-16T16:15Z · delta review after FAIL 6218504915Served-tier: Inputs read, and nothing else: card #23904 (body and all six comments, 6215481679 through the round-2 dev report 6219350488), PR #23962 (body, its 17-file list, the earlier record 6218504915, the delta Gate verdicts on this head (the check-runs)
① Derived judgmentsThe delta (three files, +44 / −13) moves no door; sections ① to ③ of 6218504915 stand for the net diff, re-read here against the head, and the three judgments the delta adds are these:
Carried from 6218504915, re-read on this head, each still right: the two D2 entries ( ② Semver level
③ Boundary flagsRound 2 (report 6219350488):
Round 1 (report 6217917388), answered in 6218504915 and standing — the escalations restated so this record is complete on its own:
Landing: Implemented-by: VERDICT: PASS |
Review: ACCEPT · p0 #23904 · PR #23962 @
|
Fixes #23904
Clause-②: yes (widening)
An already-installed package or stored page carrying a declaration spec 18 retires no longer stops the environment's kernel build. The mechanism is the ADR-0087 D2 transition pattern, not a tolerance switch in the registry: two retired conversion entries that data at rest replays, plus the versioned artifact window opened at the one door every package registration reaches. The authoring, save and publish doors refuse exactly what they refused before.
What changes
definePage/defineStack(os validate,os build), the metadata save door (PUT /api/v1/meta), package publishpages.N.isDefault/pages.N.templatepage-default-and-region-placement.test.ts,default-record-page-save-door.test.ts)sys_metadatapage row read back_diagnostics.valid: falseisDefaultdeleted off a non-bound page,full-widthre-spelleddefault, oneOS_METADATA_CONVERTEDnotice per site (pin:page-default-and-region-placement.test.tsledger block)engines.protocolopensartifact-forward-conversion.test.tsandartifact-forward-conversion-package-body.test.ts, which now carry both entries)manifest.register/ObjectQL.registerAppthat declaresengines.protocol(an installed package, a marketplace install, a hosted kernel's loader)MisplacedDefaultPageError, kernel build stoppedwarnper (package, conversion) naming the conversion id, the floor and the first site (pin:registry-installed-package-retired-page-declarations.test.ts)registry-default-record-page.test.ts, untouched)packages/spec/src/conversions/registry.ts:page-is-default-off-record-page-removed(stripsisDefault: truefrom exactly the pagesmisplacedDefaultPageRefusalrefuses, the parse's one judge) andpage-template-full-width-to-default; bothtoMajor: 18,retiredFromLoadPath: true,retiredAfter: '17.7.0', order 67, inserted at their id-sorted positions with their definitions directly above the entry that follows (the step-18 merge-shape test is green).packages/objectql/src/engine.ts:ObjectQL.convertPackageBodyForward(body)replaysapplyArtifactForwardConversionsover a body that declares a protocol range;registerAppregisters the converted copy (record, live manifest, every item).packages/objectql/src/plugin.ts: themanifestservice converts an artifact that carriespackages[]once, beforeresolveArtifactPackageOrderstrict-parses its bodies. Idempotent and copy-on-write: a body an artifact door already converted comes back by reference with no notice.packages/metadata-core/src/artifact-forward-conversion.ts: a definition with nomanifestkey reads its declared range off itself (the flat package-body shape); one with amanifestobject is read there, as before.page-is-default-off-record-page-refused/page-template-closed-region-unplaced-refused: prose now names the data-at-rest halves; no new tombstone, no new key.packages/spec/liveness/page.json: theisDefaultandtemplatenotes carry the load-path reading (state counts unchanged).packages/objectql/src/__fixtures__/hotcrm-4.0.1-published-pages.artifact.json.Readings (PM mechanism assumptions, measured)
MisplacedDefaultPageError, the registry's own throw. The strict body parse (resolveArtifactPackageOrder→ArtifactPackageSchema, run by themanifestservice on an artifact carryingpackages[], bycreateStandaloneStack, byloadArtifactBundle's readers) would have refused HotCRM 4.0.1 FIRST, with a Zod issue atpackages.1.manifest.pages.5.isDefault(and attemplate/regions), never with that error name. So the kernel that failed registered the package bodies flat, throughmanifest.register/registerApp→SchemaRegistry.installPackage→refuseDefaultRecordPageConflicts(registry.tspackage door), with no parse and no artifact file door in between. Both throws (:2717package door,:4652item seam) sit behindregisterApp; the door now converts ahead of both.templateandregions: the registry never judged them (parse-only refusals, as feat(spec)!: a page declares nothing the console does not honour — isDefault only on a bound record page, a closed template set, regions its template places #23708 recorded), so on that chain they never fired; they fire at the strict-parse doors only.loadArtifactBundle,MetadataPlugin,AppPlugin) the registry already sees converted bytes, so the D2 entry alone fixes them. On the flat registration chain above nothing converted before the registry, so the entry alone would not have fixed the measured path; the versioned window is therefore opened atregisterApp, gated on a declared range so a code-registered manifest keeps today's refusals. HotCRM 4.0.1 declaresengines.protocol: ^17.7.0(floor 17.7.0 < 18.0.0): verdictconverted-forward, 152 + 3 + 1 sites across the chain.os buildstamps it), so the marketplace install of 4.0.1 takes the load posture; publishing a new version goes throughos build, whose parse refuses the shapes.POST /api/v1/packages(protocol.installPackage) and thesys_packagesboot hydration callregistry.installPackagedirectly and are NOT covered by this door (see acceptance notes).OS_METADATA_CONVERTEDnotice of each entry, surfaced at the registration door as one[ObjectQL] package '…' … ADR-0087 conversion 'CONVERSION-ID' converted N site(s) forward (first at …)warn per (package, conversion), deduped per process, the wording familyMetadataPlugin's artifact door prints; the stored-row seams and artifact doors surface it through their existing channels.objectstack-ai/hotcrmat tagv4.0.1(1d7148bf2d3a), cloned read-only and built with its own pinned toolchain (@objectstack/cli17.7.0 →dist/objectstack.json, 2.4 MB, option-B, two package bodies). The repository commits nodist/and the GitHub release API answered 403 to this session, so the artifact was built, not downloaded. The fixture is that artifact trimmed to itsmanifestand each body's manifest fields plus the FULLpagescollection (8 pages, byte-identical). Not a hand-made shape.{ conversion: pageIsDefaultOffRecordPageRemoved, order: 67 }line deleted from step 18 (scripts/ablation-replace.mjs --delete --hold, anchor 1 → 0 on disk), spec rebuilt: the boot test 4 of 8 red (the same four cases as (b)), the 4 refusal controls green; restored withgit checkout HEAD -- PATH, blob2e91e262equalsHEAD, anchor 1 again, spec rebuilt,ablation-dist-preflight.mjs @objectstack/spec 'page-is-default-off-record-page-removed'→ present in 8 built files, 8 of 8 green. The mutated leg's--absentpreflight exited 4: the id string is also prose in the D3 entry this PR edits, sodist/migrations/index.{js,mjs}still carried it (2 hits, both migrations) while the 6 conversion-dist files had lost it — the dist reading of the mutation is the 8 → 2 drop, read off the two preflight outputs. (b) Registration door (engine.ts, the declared-range guard replaced by an unconditional return, source resolved fromsrc): 4 of 8 cases red (× registers both package bodies …,× … registers through manifest.register …,× … without a second warning …,× an artifact declaring ^17.7.0 …), the 4 refusal controls green; restored, blobd45112ddequalsHEAD,git diff HEADempty.The other load-time refusals of the same commit (reviewed, measured)
template: 'full-width'→ D2page-template-full-width-to-default. objectui at the pindaeb77fc84ddmaps'default'and'full-width'to the oneFullWidthTemplatecomponent (renderers/layout/page.tsx,TEMPLATE_REGISTRY), so the re-spelling draws the page exactly as before. HotCRM 4.0.1 ships three such pages.centered,utility-barin 4.0.1): the renderer fell back to the page-type layout (resolveTemplate→null→HomePageLayout/AppPageLayout/UtilityPageLayout=RegionLayout(regions, pageType)), which is the same layoutFullWidthTemplatedraws, differing only in thedata-page-layoutattribute's source. Deliberately NOT converted: "any string outside the set" is not a shape a conversion positively recognizes (the module's own rule), so a D2 entry would rewrite shapes it cannot name. It stays with the parse; the registry never judged it, so the measured chain is unaffected.left_sidebar/right_sidebaron 4.0.1'sthree-columnhome page): the renderer did render them, stacked undermain(getRemainingRegions→extras), so dropping them loses content and renaming them (sidebar/aside) moves them besidemain: no lossless rewrite exists, and which slot was meant is the author's call (D3page-template-closed-region-unplaced-refused). Not converted; same reach as above.Acceptance
registry-installed-package-retired-page-declarations.test.ts› "a kernel built from HotCRM 4.0.1's published bundle boots, with the named warning" (both bodies at the engine door with the artifact's package list; the measured package alone throughmanifest.register; dedupe on re-registration);page-default-and-region-placement.test.ts(parse,defineStack),default-record-page-save-door.test.ts(save door, untouched), and the CONTROL block of the new file (range-less body, newer-than-every-retirement body, the item seam);Through the strict-parse artifact doors (
os start --artifact,createStandaloneStack), HotCRM 4.0.1 is refused regardless of this PR, on threecontrolled_by_parentobjects with no requiredmaster_detail(ADR-0055 §1, a D3 semantic refusal older than #23708) and on the two shapes above with no lossless rewrite. Measured by parsing the built artifact after the full chain: 11 issues, 8 on pages, 3 onsharingModel. That door was not the measured failure and is not widened here.Acceptance notes (observed, not changed here)
services/service-packageboot hydration (registry.installPackage(rec.manifest)fromsys_packages) andmetadata-protocol'sprotocol.installPackage(POST /api/v1/packages) call the registry's package door directly, bypassingregisterApp: a stored package record carrying a misplaced default still meetsMisplacedDefaultPageErrorthere (the hydration catches it, so the boot continues without that package). Seam:spec:ADR-0087 addendum 2026-07-31 (every rehydration seam replays the chain)→runtime:packages/services/service-package/src/index.ts hydration loop, consumer: the registry. Reported for the seat, not fixed in this PR.cloud-connection's marketplace install hands the fetched bundle tomanifest.registerraw (now converted at that door) but its side readers (bindArtifactHandlers,applySideEffects) read the unconverted ledger entry; pre-existing, not a page concern.Verification
pnpm --filter '@objectstack/cloud-connection...' build(closure, exit 0),pnpm --filter @objectstack/spec --filter @objectstack/metadata-core build(exit 0), underos-verify-lock.src/conversions src/migrations src/ui/page-default-and-region-placement.test.ts src/ui/page.test.ts src/ui/default-record-page.test.ts src/stack-default-record-page.test.ts src/ui/object-refinement-check-exports.test.ts+scripts/conversions-major18-merge.test.ts→ green after the repin (1157 tests); metadata-corepnpm test→ exit 0; objectqlregistry-installed-package-retired-page-declarations,registry-default-record-page,artifact-load-path,registry-artifact-co-ownership,registry-security-catalog-namespace,protocol-install-package,manifest-service-residual-objects→ exit 0; metadataplugin-artifact-forward-conversion→ exit 0; runtimeapp-plugin-artifact-forward-conversion,standalone-stack-option-b-forward-conversion,load-artifact-bundle.granted-permissions→ exit 0; cloud-connectionmarketplace-install-local-bundle,-hot-loaded→ exit 0.@objectstack/spec,@objectstack/metadata-core,@objectstack/objectql→ exit 0.pnpm --filter @objectstack/spec check:generated→ green aftergen:liveness-counts(0 shards rewritten).scripts/check-nul-bytes.mjs→ OK.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 94 families; each run with its exit code captured before any pipe;--ranreconciliation: 94 derived, 92 run (exit 0 exceptcheck-engine-split-ratio.mjs --days 90, exit 2: shallow clone, refuses the 90-day window), 2 NOT-MEASURED on a recorded exit 3 (prerequisite):check-plugin-teardown-shape.mjs --self-test(needs an unshallow clone) andpnpm check:dual-build-cjs-loads(needs every package'sdist/; only the cloud-connection closure was built here). Both are CI's on the PR.check:adr-0087-registration --base origin/main,check:changeset-no-major,check:empty-changeset,check:spec-changes,check:upgrade-guide,check:liveness,check:api-surface(rerun on the rebuilt dist: surface unchanged),check:docs,check:authorable-surface: exit 0Readings taken at
9fad187b(git rev-parse --short HEADafter the last commit; the gate union and the ablations ran on that tree).Generated by Claude Code