Skip to content

fix(spec,objectql): installed pages carrying a retired declaration convert at load instead of stopping the kernel build - #23962

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-23904-installed-page-load-warning
Oct 16, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-23904-installed-page-load-warning

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

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

Door Before Now
definePage / defineStack (os validate, os build), the metadata save door (PUT /api/v1/meta), package publish refused at pages.N.isDefault / pages.N.template unchanged, same sentence (pins: page-default-and-region-placement.test.ts, default-record-page-save-door.test.ts)
a sys_metadata page row read back served with _diagnostics.valid: false converted on read: isDefault deleted off a non-bound page, full-width re-spelled default, one OS_METADATA_CONVERTED notice per site (pin: page-default-and-region-placement.test.ts ledger block)
a compiled artifact inside the window its engines.protocol opens refused at the artifact door's strict parse converted before the parse, as every retired entry is (pins: the whole-step blocks in artifact-forward-conversion.test.ts and artifact-forward-conversion-package-body.test.ts, which now carry both entries)
a package body registered through manifest.register / ObjectQL.registerApp that declares engines.protocol (an installed package, a marketplace install, a hosted kernel's loader) MisplacedDefaultPageError, kernel build stopped converted before anything registers; one warn per (package, conversion) naming the conversion id, the floor and the first site (pin: registry-installed-package-retired-page-declarations.test.ts)
the same body with no declared range (a manifest a plugin builds in code), or declaring a floor newer than every retirement refused unchanged, refused (CONTROL pins in the same file and in registry-default-record-page.test.ts, untouched)
  • packages/spec/src/conversions/registry.ts: page-is-default-off-record-page-removed (strips isDefault: true from exactly the pages misplacedDefaultPageRefusal refuses, the parse's one judge) and page-template-full-width-to-default; both toMajor: 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) replays applyArtifactForwardConversions over a body that declares a protocol range; registerApp registers the converted copy (record, live manifest, every item). packages/objectql/src/plugin.ts: the manifest service converts an artifact that carries packages[] once, before resolveArtifactPackageOrder strict-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 no manifest key reads its declared range off itself (the flat package-body shape); one with a manifest object is read there, as before.
  • D3 entries 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: the isDefault and template notes carry the load-path reading (state counts unchanged).
  • Fixture: packages/objectql/src/__fixtures__/hotcrm-4.0.1-published-pages.artifact.json.

Readings (PM mechanism assumptions, measured)

  1. Call chain of the measured failure. The cloud log names MisplacedDefaultPageError, the registry's own throw. The strict body parse (resolveArtifactPackageOrder → ArtifactPackageSchema, run by the manifest service on an artifact carrying packages[], by createStandaloneStack, by loadArtifactBundle's readers) would have refused HotCRM 4.0.1 FIRST, with a Zod issue at packages.1.manifest.pages.5.isDefault (and at template / regions), never with that error name. So the kernel that failed registered the package bodies flat, through manifest.register / registerApp → SchemaRegistry.installPackage → refuseDefaultRecordPageConflicts (registry.ts package door), with no parse and no artifact file door in between. Both throws (:2717 package door, :4652 item seam) sit behind registerApp; the door now converts ahead of both. template and regions: 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.
  2. Mechanism premise, partly false. On the artifact file doors (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 at registerApp, gated on a declared range so a code-registered manifest keeps today's refusals. HotCRM 4.0.1 declares engines.protocol: ^17.7.0 (floor 17.7.0 < 18.0.0): verdict converted-forward, 152 + 3 + 1 sites across the chain.
  3. Which side a new install of a published version falls on. A published artifact always declares its range (os build stamps it), so the marketplace install of 4.0.1 takes the load posture; publishing a new version goes through os build, whose parse refuses the shapes. POST /api/v1/packages (protocol.installPackage) and the sys_packages boot hydration call registry.installPackage directly and are NOT covered by this door (see acceptance notes).
  4. Named warning. The structured OS_METADATA_CONVERTED notice 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 family MetadataPlugin's artifact door prints; the stored-row seams and artifact doors surface it through their existing channels.
  5. Artifact source. objectstack-ai/hotcrm at tag v4.0.1 (1d7148bf2d3a), cloned read-only and built with its own pinned toolchain (@objectstack/cli 17.7.0 → dist/objectstack.json, 2.4 MB, option-B, two package bodies). The repository commits no dist/ and the GitHub release API answered 403 to this session, so the artifact was built, not downloaded. The fixture is that artifact trimmed to its manifest and each body's manifest fields plus the FULL pages collection (8 pages, byte-identical). Not a hand-made shape.
  6. Ablations. (a) D2 entry: the { 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 with git checkout HEAD -- PATH, blob 2e91e262 equals HEAD, 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 --absent preflight exited 4: the id string is also prose in the D3 entry this PR edits, so dist/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 from src): 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, blob d45112dd equals HEAD, git diff HEAD empty.

The other load-time refusals of the same commit (reviewed, measured)

  • template: 'full-width' → D2 page-template-full-width-to-default. objectui at the pin daeb77fc84dd maps 'default' and 'full-width' to the one FullWidthTemplate component (renderers/layout/page.tsx, TEMPLATE_REGISTRY), so the re-spelling draws the page exactly as before. HotCRM 4.0.1 ships three such pages.
  • An unregistered template name (centered, utility-bar in 4.0.1): the renderer fell back to the page-type layout (resolveTemplate → null → HomePageLayout / AppPageLayout / UtilityPageLayout = RegionLayout(regions, pageType)), which is the same layout FullWidthTemplate draws, differing only in the data-page-layout attribute'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.
  • A region the template does not place (left_sidebar / right_sidebar on 4.0.1's three-column home page): the renderer did render them, stacked under main (getRemainingRegions → extras), so dropping them loses content and renaming them (sidebar / aside) moves them beside main: no lossless rewrite exists, and which slot was meant is the author's call (D3 page-template-closed-region-unplaced-refused). Not converted; same reach as above.

Acceptance

  • a kernel built from HotCRM 4.0.1's published bundle boots, with the named warning: 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 through manifest.register; dedupe on re-registration);
  • the same page saved or published through the authoring door is still refused: 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);
  • an ablation of the load-time path turns the boot red: §Readings 6.

Through the strict-parse artifact doors (os start --artifact, createStandaloneStack), HotCRM 4.0.1 is refused regardless of this PR, on three controlled_by_parent objects with no required master_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 on sharingModel. That door was not the measured failure and is not widened here.

Acceptance notes (observed, not changed here)

  • services/service-package boot hydration (registry.installPackage(rec.manifest) from sys_packages) and metadata-protocol's protocol.installPackage (POST /api/v1/packages) call the registry's package door directly, bypassing registerApp: a stored package record carrying a misplaced default still meets MisplacedDefaultPageError there (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 to manifest.register raw (now converted at that door) but its side readers (bindArtifactHandlers, applySideEffects) read the unconverted ledger entry; pre-existing, not a page concern.
  • The registry's two throws stay in place as the authority for a body that survives conversion (no declared range, or a floor past every retirement).

Verification

  • Build: pnpm --filter '@objectstack/cloud-connection...' build (closure, exit 0), pnpm --filter @objectstack/spec --filter @objectstack/metadata-core build (exit 0), under os-verify-lock.
  • Tests (all under the lock, exit codes captured before any pipe): spec 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-core pnpm test → exit 0; objectql registry-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; metadata plugin-artifact-forward-conversion → exit 0; runtime app-plugin-artifact-forward-conversion, standalone-stack-option-b-forward-conversion, load-artifact-bundle.granted-permissions → exit 0; cloud-connection marketplace-install-local-bundle, -hot-loaded → exit 0.
  • Typecheck: @objectstack/spec, @objectstack/metadata-core, @objectstack/objectql → exit 0.
  • pnpm --filter @objectstack/spec check:generated → green after gen:liveness-counts (0 shards rewritten). scripts/check-nul-bytes.mjs → OK.
  • Gates: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 94 families; each run with its exit code captured before any pipe; --ran reconciliation: 94 derived, 92 run (exit 0 except check-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) and pnpm check:dual-build-cjs-loads (needs every package's dist/; 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 0

Readings taken at 9fad187b (git rev-parse --short HEAD after the last commit; the gate union and the ablations ran on that tree).


Generated by Claude Code

…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
@github-actions

github-actions Bot commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/metadata-core, @objectstack/objectql, @objectstack/spec, touching 20 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/spec/liveness/page.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

22 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 68ffa5dc1af1abeb012cd1ed7dfa18be4c07a8c4.

⛔ 7 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/spec/liveness/page.json) — pages documenting those are invisible to this run
  • 1 anchor(s) matched too much of the corpus to be a work list: ObjectQL (symbol, 72 pages)
  • 5 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 143 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 68ffa5dc1af1abeb012cd1ed7dfa18be4c07a8c4 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 50fd681ea1d398d232fe704c87edc590484ac692 — the merge of head 6c52a63bd1e8b4c9fb59c366299a26b6f5650d10 into base 68ffa5dc1af1abeb012cd1ed7dfa18be4c07a8c4, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 68ffa5dc1af1abeb012cd1ed7dfa18be4c07a8c4 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review — PR #23962 (card #23904) · 2026-10-16T15:10Z

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 9fad187b5013b4868e7a647b0e44b434b1cb4920
Local-runs: none

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 main at merge-base 11fea6b), and the 41 check-runs on the head (all concluded). Nothing was built, run, re-run or ablated here. The logs behind the failed check-runs are not readable without credentials, so each is judged on its conclusion, which is the gate verdict.

Gate verdicts on this head (the check-runs)

  • 32 success, 6 skipped (path-filtered lanes, expected), 3 failure: the two shards below and the aggregate Test Core context branch protection requires. Every lane has concluded.
  • Test Core (1/6) — failure: pnpm run test in packages/spec exited 1 (job 117028249972).
  • Test Core (5/6) — failure: pnpm run test in packages/objectql exited 1 (job 117028250035).
  • Test Core (aggregate) — failure, the required context, carrying the two shards. Test Core (2/6), (3/6), (4/6) and (6/6) success. Build Core, TypeScript Type Check, every Lint and Type Check lane, Check Changeset, Spec property liveness, Governed Surface Queue Guard, Check PR Size, Flag docs affected: success.
  • These are the two packages whose full suites the dev's report (6217917388, deviation 2) declares as "CI's on the PR": spec ran the conversions, migrations and page subsets locally, objectql a registry, artifact and manifest-door subset of its 409 test files. The red therefore sits on exactly the ground local verification did not cover, and the gate verdict decides: FAIL on this head. Whether the two reds are the diff's or a pre-existing flake cannot be read from here; the next head must show both shards green, or name the failing files and show them red on main too.

① Derived judgments

Every accept-set and public-surface change the diff implies, each named right or wrong:

  1. spec — two D2 entries page-is-default-off-record-page-removed and page-template-full-width-to-default (toMajor: 18, retiredFromLoadPath: true, retiredAfter: '17.7.0', order 67, id-sorted into MAJOR_18_CONVERSIONS). Right. Both rewrites are lossless: nothing read a misplaced isDefault (the liveness row's own evidence), and full-width and default resolve to the one FullWidthTemplate at the objectui pin (the template row's evidence, already on main). The strip reuses the parse's one judge, misplacedDefaultPageRefusal, so conversion and refusal cannot disagree about which pages are misplaced. 17.7.0 is the newest stable below the 18.0.0-next.2 label, which the retired-after census requires under a prerelease label.
  2. spec — the stored-row seam widens (applyConversionsToStoredItem('page', …)): a stored page carrying the misplaced flag or full-width reads back converted and valid with an OS_METADATA_CONVERTED notice, where it was badged _diagnostics.valid: false. Right — this is the card's Wanted and the maintainer's order, 「内核构建遇到已安装、已存储的旧元数据里被退役的声明时,按过渡窗口只警告、不拒绝启动;发布时照样拒」.
  3. spec — the authoring funnel is unchanged: both entries are retired from the day they land, so definePage, defineStack (os validate, os build), the save door and publish refuse the same shapes with the same sentence (pinned in both new entry tests and in page-default-and-region-placement.test.ts). Right — the 「发布时照样拒」 half.
  4. spec — the artifact file doors widen for an artifact whose declared floor is at or below 17.7.0 (MetadataPlugin, AppPlugin, loadArtifactBundle): the two shapes convert before the strict parse, as every retired entry does. Right. HotCRM 4.0.1 stays refused at those doors on the shapes with no lossless rewrite (centered, utility-bar, the two unplaced regions) and on three ADR-0055 objects older than 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 — declared in the PR, not widened; correct under Prime Directive 12, since a conversion may only rewrite a shape it positively names.
  5. spec — the pageAssignedProfilesRemoved fixture loses isDefault: true on its team_home control page in both halves. Right: an untyped, unbound page with the flag is now a page the new entry strips, so the pair would have stopped proving "a page with nothing to strip keeps its identity"; no behaviour moves.
  6. metadata-core — applyArtifactForwardConversions reads the declared range off a flat body when the definition carries no manifest object. Right, and no existing door moves: the three production callers (MetadataPlugin's stack definition, AppPlugin's { ...manifest, ...collections } which still carries collections.manifest, and convertArtifactAtLoad on the raw file) all hand in a definition with a manifest; the only flat caller is the new registration door. The one narrowing the reading implies — a flat body declaring a current-or-newer floor yet carrying a retired shape was under the undeclared amnesty before and answers to the strict parse now — is the versioned-window posture ADR-0087 asks for ("versioned rather than a blanket amnesty"), is pinned in the new metadata-core case, and has no honest producer (os build refuses the shape at that floor). Not a second arm.
  7. objectql — ObjectQL.registerApp converts a range-declaring body before anything of it registers (convertPackageBodyForward). The accept set widens at the kernel's package registration door for a body with engines.protocol below the runtime label: the whole retired chain replays (every D2 entry, not only the two page entries), one warn per (package, conversion) naming the id, the floor and the first site, deduped per process. Right: this is the one door every registration reaches (SchemaRegistry.installPackage and the item seam both sit behind it); the policy function is the same one the artifact doors call, never a second table; it is copy-on-write and idempotent, so a body an artifact door already converted comes back by reference with no second notice. The gate on a declared range is right too: no in-repo code-authored manifest.register or registerApp body (apps/account, setup, studio, mcp, metadata-protocol) declares engines, so code manifests keep today's refusals. Cost: one extra chain walk per registration of an already-converted artifact body — acceptable, and declared.
  8. objectql — the CONTROL refusals hold: a range-less body and a body declaring a floor newer than every retirement still meet MisplacedDefaultPageError (422, the same sentence); registerItem('page', …, pkgId) still refuses. Right, pinned in the new file and untouched in registry-default-record-page.test.ts.
  9. objectql — the manifest service converts a whole option-B artifact (packages[]) before resolveArtifactPackageOrder strict-parses its bodies. Right: one window read off the artifact's own range, every body replayed under it (replayPackageBodies), a single-body payload handed through to registerApp untouched, a bundle an artifact door already converted returned by reference.
  10. objectql — public surface: convertPackageBodyForward(body) added on the class (not on IObjectQLEngine); registerApp's first parameter renamed, type unchanged. Additive, so minor is the right level.
  11. Liveness ledger (packages/spec/liveness/page.json): notes only, state counts unchanged, gate green. Right. The two D3 entries' prose names the data-at-rest halves and their conversionIds stay empty (the authoring surface keeps no mechanical edit). Right, and pinned.
  12. Fixture hotcrm-4.0.1-published-pages.artifact.json: the v4.0.1 tag's dist/objectstack.json built with its own pinned 17.7.0 toolchain, trimmed to the manifests plus the full pages collection (8 pages, declared floor ^17.7.0). Matches the card's acceptance source ("the v4.0.1 tag's dist/objectstack.json"); read from inside its own package. Right. 1380 of the PR's 2235 added lines are this file.
  13. Acceptance as the card words it: a kernel from the 4.0.1 bundle boots with the named warning (new objectql test, both at the engine door and through manifest.register); the authoring door still refuses (the controls); an ablation turns the boot red (PR body, Readings 6: both the entry and the door, restore proven). Met on the dev's own run — but the two suites that would confirm it in CI are the red shards above.

② Semver level

  • Changeset .changeset/23904-installed-page-retired-declarations-convert-at-load.md: @objectstack/spec minor, @objectstack/objectql minor, @objectstack/metadata-core minor; Clause-②: yes (widening); the ADR-0087 marker present and naming both entries; a FROM → TO door table and a Migration: line.
  • The level matches what the diff publishes: the three packages whose published source moves are exactly the three graded; yes takes at least minor and gets it; nothing an author can write is removed or renamed, so no tombstone is owed and skip-changeset would be wrong. The ! on the first line is the launch-window spelling (breaking ships as minor, the convention across the current .changeset/ corpus) — over-declared if anything, since no author's input breaks; harmless, and the body carries what a breaking changeset must.
  • Clause-②: line: yes (widening) on the PR body, in the changeset, and matching the claim (6216456972) and triage (6215783095). The direction is right: every door that moves widens its accept set at load; the one narrowing named in ①.6 has no producer and is the intended versioned-window posture, not a second arm. Right.

③ Boundary flags

The dev's flags (report 6217917388: deviations, open_questions, out-of-scope findings), each answered or escalated:

  • Open question 1 — which chain cloud's KernelManager uses (manifest.register / registerApp, now covered, against a direct registry.installPackage through protocol.installPackage or the sys_packages hydration, not covered). Answered: the dev's inference stands as far as this repository can read it — the hydration loop catches and continues, so it cannot have produced "kernel build failed", and the strict-parse doors would have named a Zod issue at pages.N.template first, so the covered door is the measured one. Escalated to the seat, two items: the cloud-side confirmation belongs to cloud#2868's pin bump (the dev's own first choice); and the two direct callers (services/service-package hydration, protocol.installPackage) are rehydration seams the ADR-0087 addendum binds, which today drop a stored package silently after restart — a contract violation under Prime Directive 10, to be filed by the seat as its own card (the dev's class-b finding, carrier none). Not the in-registry door the dev also weighed: a second judge inside the primitive is the shape the ADR forbids.
  • Open question 2 — artifact-door posture for shapes with no lossless rewrite. Answered: leave as landed. No measured producer needs that door for 4.0.1 (refused there anyway on ADR-0055 objects older than 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), a conversion may only rewrite a shape it positively names, and turning the strict-parse door into warn-and-serve is a door-policy decision for the maintainer — the card's own "return needs_decision, do not build it" clause. Nothing to escalate until a producer is measured.
  • Open question 3 — the registration window for a range-less body. Answered: keep it closed, as landed. At this door a range-less manifest is a plugin's code-authored one (every in-repo caller, ①.7), and every os build artifact declares its range; the file doors' undeclared amnesty is for a file of unknown age, which a code manifest is not.
  • Deviation 1 (HotCRM 4.0.1 built in a read-only sister clone without the verify lock): a sister repository's own toolchain build, not this repository's build or test — accepted.
  • Deviation 2 (local scope narrowed; the full spec and objectql suites "are CI's on the PR"): this is where the head fails — both suites are red in CI. Not accepted on this head; see the gate verdicts.
  • Deviations 3 to 7 (the dev-prereqs scan form not run; the mutated-leg dist preflight exit 4 explained by the id surviving as D3 prose, with the 8 to 2 hit drop plus the red suite as the reading; the liveness mis-edit corrected against the base file in a later commit; cloud source unreadable; read-only clones removed): declared and explained, no escalation.
  • Out-of-scope finding 2 (cloud-connection's marketplace install side readers bindArtifactHandlers and applySideEffects read the raw ledger entry): pre-existing and not a page concern; for the seat's dedupe list, not this PR.
  • Governed surfaces: none in the file list (.changeset/ and packages/** only); Governed Surface Queue Guard success. Not HotCRM, cloud, skills/**, docs/adr/** or .claude/** — the claim's file surface held, with the engine files on the cross-domain exception path triage named.

What turns this to PASS on the next head: Test Core (1/6) and (5/6) green, so the aggregate Test Core context concludes success — or the failing files named and shown red on main too. Sections ① to ③ stand unless the fix for the red moves a door.

Implemented-by: claude/issue-23904-installed-page-load-warning
Reviewed-by: session_01LFqdRsuM5nkqoJdHd5ztVk

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
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review — PR #23962 (card #23904) · 2026-10-16T16:15Z · delta review after FAIL 6218504915

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 6c52a63bd1e8b4c9fb59c366299a26b6f5650d10
Local-runs: none

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 9fad187b → 6c52a63b and the net diff against main at merge-base 11fea6b000), and the 41 check-runs on the head. Nothing was built, run, re-run or ablated here.

Gate verdicts on this head (the check-runs)

  • 41 check-runs, every one concluded: 35 success, 6 skipped (path-filtered lanes: Build Docs, Console Pin Gate, Temporal Conformance, Dogfood Verify CLI, the dogfood matrix, the opt-in tarball smoke), 0 failure.
  • The five required contexts, by name: Lint & Repo Gates success, TypeScript Type Check success, Test Core success (shards 1/6 through 6/6 all success — the two shards 6218504915 failed on, 1/6 spec and 5/6 objectql, are green), Build Core success, Governed Surface Queue Guard success. Check Changeset, Spec property liveness, Check PR Size, the claim and single-writer guards: success.
  • The ground of the FAIL is gone: both reds were this PR's own, as the round-2 report reads them off the job logs (the removal-version pin's JOIN rule, which fires only for entries this PR adds; the save-door pins of spec: PageTypeSchema says home is "the platform-level landing page" and utility a "floating utility panel", but the landing is the first navigation item and both types render as ordinary inline pages #23610 that this PR's data-at-rest conversion changes by design), and both suites were reproduced green locally by the dev at this head (spec 687 files / 20915 tests, objectql 435 files / 8221 tests) before CI confirmed them.

① Derived judgments

The 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:

  1. spec — the refusal sentence misplacedDefaultPageRefusal now names the release, in both variants ("… is refused on a type: 'home' page since @objectstack/spec 18: …" and "… bound to no object since @objectstack/spec 18: …"). Right, and it reaches every door that prints it: the sentence has ONE producer (ui/default-record-page.ts), and every door prints that producer's return — the page parse (checkPageDefaultPlacement, page.zod.ts:1563, so definePage, defineStack, os validate, os build and the metadata save door), the package door (misplacedDefaultPages → refuseDefaultRecordPageConflicts, objectql/default-record-page.ts:149) and the item seam (registry.ts:4651). git grep at the head finds no hard-coded copy of the sentence in packages/**, content/docs/**, skills/** or the pinned ../objectui; every existing pin asserts the prefix up to "page" / "object" or the function's own return, so none moved (shards 1/6 and 5/6 green say the same). A release name is not a tracker number, so check:doc-authoring (Lint & Repo Gates, green) is not crossed. The naming is required, not cosmetic: the removal-version class pin holds every prescription of an entry retired after the newest published stable (retiredAfter: '17.7.0', which IS the newest stable under the 18.0.0-next label, so removalMajor derives @objectstack/spec 18) to name exactly that release — the template: 'full-width' enum prescription already did ("was removed in @objectstack/spec 18", on main since 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), and this delta brings the isDefault sentence level with it.
  2. spec — the JOIN rows (retired-key-removal-version.pin.test.ts): page-is-default-off-record-page-removed joined to the two refusals an author meets at isDefault (a type: 'home' page; a record page bound to no object — the two branches of the one judge), page-template-full-width-to-default joined to the enum refusal at template. Right: each row is "the tombstones or refusals an author meets for its surface", as the pin's own failure text asks; each is rendered by a real PageSchema.safeParse at the key, so the row fails the day the parse stops refusing; and the pin's derivation (newest stable 17.7.0 on the v18 train ⇒ 18) is exercised in both directions by the existing rows.
  3. objectql — the repinned stored-row save-door reads (default-record-page-save-door.test.ts, both kernel topologies): a sys_metadata page row laid in the store with no write door in front of it (storeRow → driver.create) is read back through protocol.getMetaItem CONVERTED for the two shapes with a lossless rewrite (isDefault absent on the home page; template: 'default' where full-width was stored), served AS STORED for the unplaced left_sidebar region (no lossless rewrite, no conversion touches it), and re-saving the row as it was stored is refused 422 in every case. Right — this is exactly the posture the card and the ADR-0087 data-at-rest addendum require. The addendum (2026-07-31): "Every stored-row rehydration seam replays the FULL chain, retired entries included — applyConversionsToStoredItem … called by … getMetaItem" and "Writes stay gated; reads diagnose, never drop. saveMetaItem keeps rejecting off-spec bodies (422, tombstones included)". The card, in the maintainer's words: 「内核构建遇到已安装、已存储的旧元数据里被退役的声明时,按过渡窗口只警告、不拒绝启动;发布时照样拒」. The old pins ("read serves the row as stored") were spec: PageTypeSchema says home is "the platform-level landing page" and utility a "floating utility panel", but the landing is the first navigation item and both types render as ordinary inline pages #23610's record of "no D2 conversion", which this PR's D2 entries replace by design; the third shape's pin (served as stored, still refused at save) is unchanged, and it is what the addendum says of a row that still fails the schema after conversion (registered anyway, badged _diagnostics). Re-saving the CONVERTED body is the healing round-trip the 2026-08-02 addendum describes, and the authoring refusal of the stored shape is the 「发布时照样拒」 half.

Carried from 6218504915, re-read on this head, each still right: the two D2 entries (toMajor: 18, retiredFromLoadPath: true, retiredAfter: '17.7.0', order 67, lossless both; the strip reuses the parse's one judge); the stored-row seam widening (the card's Wanted); the authoring funnel unchanged (both entries retired from the day they land, pinned "never replays on the authoring funnel"); the artifact file doors widening only inside the versioned window, HotCRM 4.0.1 still refused there on the shapes with no lossless rewrite and on three ADR-0055 objects older than #23708; the pageAssignedProfilesRemoved fixture losing a flag the new entry would now strip; metadata-core reading a FLAT body's range off itself with the one narrowing (a flat body at a current-or-newer floor answers to the strict parse, not the undeclared amnesty) pinned and producer-less; ObjectQL.registerApp converting a range-declaring body before anything registers, one warn per (package, conversion), copy-on-write and idempotent, range-less bodies and floors past every retirement still refused (CONTROL pins, the item seam included); the manifest service converting a whole packages[] artifact once before resolveArtifactPackageOrder; the additive public surface (convertPackageBodyForward, registerApp's parameter renamed with its type unchanged); the liveness notes (state counts unchanged, gate green); the D3 prose naming the data-at-rest halves with conversionIds still empty; the fixture being the v4.0.1 tag's own build trimmed to manifests plus the full pages collection; and the card's three acceptance lines met — the kernel from the 4.0.1 bundle boots with the named warning (new objectql file, engine door and manifest.register), the authoring door still refuses (controls), the ablation of entry and door turns the boot red (PR body Readings 6, restore proven) — now with the two suites that carry those pins green in CI.

② Semver level

  • Changeset .changeset/23904-installed-page-retired-declarations-convert-at-load.md, unchanged in the delta: @objectstack/spec minor, @objectstack/objectql minor, @objectstack/metadata-core minor; Clause-②: yes (widening); the ADR-0087 marker naming both entries (check:adr-0087-registration green); a door table and a Migration: line.
  • The level matches what the diff publishes: the three packages whose published source moves are exactly the three graded; yes takes at least minor and gets it; nothing an author can write is removed or renamed, so no tombstone is owed and skip-changeset would be wrong. The delta's one published change — the isDefault refusal sentence gaining a release name — is a prescription improvement inside @objectstack/spec's minor, not a surface change. The ! on the first line is the launch-window spelling the current .changeset/ corpus uses (check:changeset-no-major green) — over-declared, harmless.
  • Clause-②: line: yes (widening) on the PR body and in the changeset, matching the claim (6216456972) and triage (6215783095). The direction is right: every door that moves widens its accept set at load; the one narrowing (①, metadata-core) has no producer and is the versioned-window posture, not a second arm. Right.
  • One prose nit, no verdict weight: the changeset's and the PR body's door table says the authoring doors refuse "with the same sentence" — after this delta the isDefault sentence carries one more clause (the release name); the refusal, its key and its status are the same. The seat may tighten the word at landing or leave it; the CHANGELOG reader's question (is the refusal unchanged?) is answered correctly either way.

③ Boundary flags

Round 2 (report 6219350488): open_questions: [], out_of_scope_findings: [], three deviations, each answered:

  • Deviation 1 (the worktree had been removed after round 1 and was recreated from the pushed branch; the dependency closure rebuilt before any test): the clean-up rule followed, then the §9 refresh — accepted.
  • Deviation 2 (the two CI job logs read through the GitHub MCP get_job_logs read, as the PM asked, and grepped from the saved result for the failing assertions): a read, not a write; the two files it named are exactly the two this delta repins — accepted.
  • Deviation 3 (the gate union re-derived and rerun at the new head, 94 families, 92 run, 2 NOT-MEASURED on recorded exit 3 — check-plugin-teardown-shape --self-test on a shallow clone, check:dual-build-cjs-loads without every package's dist — both CI's): the same two as round 1, both covered by Lint & Repo Gates and Build Core, green — accepted.
  • The PR body's "Readings taken at 9fad187b" line is one commit behind (the dev does not PATCH, as it says): the round-2 report carries the readings at 6c52a63b (full spec and objectql suites, typecheck, gate union), and the body's Readings 1 to 6 describe behaviour the delta does not touch. For the seat to note, not a defect.

Round 1 (report 6217917388), answered in 6218504915 and standing — the escalations restated so this record is complete on its own:

  • Open question 1 (which chain cloud's KernelManager uses): the covered door (manifest.register / registerApp) is the measured one — the registry's own error name, no Zod issue at pages.N.template first. Escalated to the seat, two items: the cloud-side confirmation belongs to cloud#2868's pin bump (boot a 4.0.1 environment on staging); and the two direct callers of registry.installPackage — services/service-package's sys_packages boot hydration and metadata-protocol's protocol.installPackage — are rehydration seams the ADR-0087 2026-07-31 addendum binds and today drop a stored package silently after restart (Prime Directive 10): the seat files that as its own card (the dev's class-b finding, carrier none). Not a second judge inside the registry primitive.
  • Open question 2 (artifact-door posture for shapes with no lossless rewrite): leave as landed — a conversion may only rewrite a shape it positively names, no measured producer needs that door for 4.0.1, and warn-and-serve at a strict-parse door is a door-policy decision for the maintainer (the card's own "return needs_decision, do not build it" clause).
  • Open question 3 (the registration window for a range-less body): keep it closed, as landed — at this door a range-less manifest is a plugin's code-authored one, and every os build artifact declares its range.
  • Out-of-scope finding 2 (cloud-connection's marketplace-install side readers read the raw ledger entry): pre-existing, not a page concern; the seat's dedupe list.
  • Governed surfaces: none in the 17-file list (.changeset/ and packages/** only); Governed Surface Queue Guard success; the PR is from the base repository, not a fork. The claim's file surface held (engine files on the cross-domain exception path triage named; nothing in skills/**, docs/adr/**, .claude/**, HotCRM or cloud). Changed lines 2279 + 44 = 2323, under the 3,000-line human-merge threshold.

Landing: Clause-②: yes on packages/spec/src/** is what called this tier; no governed path, so the owning seat readies and arms the queue on this PASS (draft today, auto-merge not armed, assignee os-bill — none of it this record's to set). Cloud's next pin bump (cloud#2868) carries the fix.

Implemented-by: claude/issue-23904-installed-page-load-warning
Reviewed-by: session_01LFqdRsuM5nkqoJdHd5ztVk

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Review: ACCEPT · p0 #23904 · PR #23962 @ 6c52a63bd1e8b4c9fb59c366299a26b6f5650d10 · domain:spec seat 2 (#18549) · 2026-10-16T16:16Z

Session session_01LFqdRsuM5nkqoJdHd5ztVk · os-bill. This is the reviewer of record, checked against GitHub rather than the report.


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 16, 2026 16:17
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 16, 2026
Merged via the queue into main with commit d050a8f Oct 16, 2026
42 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-23904-installed-page-load-warning branch October 16, 2026 17:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation protocol:ui size/xl tests tooling

Projects

None yet

2 participants