Skip to content

feat(cli): os migrate security-catalog-overlays — list, and with --apply delete, the environment rows a v18 cold boot refuses - #22523

Merged
objectstack-fleet[bot] merged 15 commits into
mainfrom
claude/issue-22371-overlay-cleanup-step
Oct 9, 2026
Merged

objectstack-fleet[bot] merged 15 commits into
mainfrom
claude/issue-22371-overlay-cleanup-step

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #22371
Clause-②: yes (widening)

Item 1 of ruling A′ (record 6073500921), as amended by ruling letter B (record 6074838935): the offline step that lists, and with --apply deletes, the environment-wide rows a v18 cold boot refuses. Item 2 (retiring the three in-kernel remedies in plugin-security) is a separate domain:services card and is not in this PR.

What it is

os migrate security-catalog-overlays, a sibling in the os migrate family (the meta --stored conventions: preview by default, --apply, --yes, --force, --json, --database-url / $OS_DATABASE_URL).

Why a sibling, not a mode of os migrate meta --stored. The ruling named meta --stored as the nearest landing, and the step departs from it on three counts:

  • The operations differ. --stored --apply rewrites every stored row through saveMetaItem, while the step deletes one refused population. Under one --apply, the flag would carry a second, destructive meaning.
  • The boots differ. --stored boots without the host config and hydrates sys_metadata, which is exactly the boot the cold-boot refusal stops over a compiled artifact. The step needs serve's composition, with the auth-gated security plugin and hydration off.
  • The exit contracts differ. --stored exits 1 when rows remain uncanonical; the step exits 1 when the next boot would be refused.

As a sibling, the step keeps the family's conventions (preview by default, --apply / --yes / --force / --json / --database-url, the occupancy gate), and the refusal message names it directly.

  • It lists every active, environment-wide sys_metadata row (organization_id IS NULL, state = 'active') of type permission or position, the legacy plurals permissions / positions included, whose name a configured package holds: the population the cold-boot check (ADR-0048 N.3) refuses. A draft row and an organization-scoped row are never listed.
  • It composes what os serve composes, for the first phase only, and hydrates nothing. The host config's plugins and the application or compiled artifact (the existing composeHostStack declaration boot), plus the security plugin behind serve's auth gate. The boot runs with sys_metadata hydration off, so the cold-boot check meets an empty environment half and the boot comes up with no server.
  • It reads the holders off the registry through one new @objectstack/objectql export, findPackageHeldSecurityCatalogNames(registry). The cold-boot check now computes its conflicts from the same private reading (that list met with the bare slot), so the step and the boot share one reading of "who holds a name".
  • --apply deletes through the engine's audited write path. A row stored under the canonical type goes through the protocol's deleteMetaItem (the DELETE /api/v1/meta/TYPE/NAME door): a sys_metadata_history tombstone and a sys_metadata_audit row, actor os migrate security-catalog-overlays. A legacy-plural row is out of that door's reach (it folds the type before reading, and the audit trail refuses a non-canonical type), so it goes through the SysMetadataRepository delete beneath it, addressed by its stored type, name, package_id and checksum, which writes the same history tombstone. One audit line per row names the door it took. Nothing is adopted; managed_by and package_id are never rewritten.
  • Exit status. Preview: 1 when it lists a row (the next boot would be refused), 0 when it lists none. --apply: 0 when every listed row is gone, 1 when one could not be deleted. A host config that exists and cannot be loaded is refused before any row is read.
  • The refusal names it. The cold-boot NAMESPACE_CONFLICT message now points at the step for the environment's rows. The envelope is unchanged.

The first reading: does materializeStackPlugin cover the auth-gated security plugin?

No. materializeStackPlugin (packages/core/src/stack-plugins.ts, from 97610a533) is the rule for one entry of a stack's own plugins array. The security plugin is composed by serve's "5d" auth step, gated inline in serve.ts on four conditions: the stack mounts no AuthPlugin, the auth tier is on, the composition is not a host kernel, and an auth secret resolves (with the development fallback).

Extraction done here: packages/core/src/stack-auth.ts, exported from @objectstack/core:

  • resolvePlatformAuthComposition({ plugins, tiers, secret }) returns { composes: true, secret } or { composes: false, reason }. The checks run in serve's order.
  • It comes with the tier rule resolveStackTiers and its STACK_TIER_PRESETS / CAPABILITY_TO_TIER. Serve.TIER_PRESETS and Serve.CAPABILITY_TO_TIER are now handles over these.
  • Also exported: resolveAuthSecret, stackSuppliesAuthPlugin and isHostKernelComposition.

serve asks this rule. Its gate line if (!hasAuthPlugin && tierEnabled('auth')) is kept, now reading the rule's own predicate and tier set; the host-kernel and no-secret branches read the rule's answer. What serve composes is unchanged.

Still owed (not in this PR's surface):

  • --preset and --dev (patch round 2, 7f961c71a9). The step takes serve's two flags with serve's meaning, through the rules serve itself now calls. --preset: both commands list Object.keys(STACK_TIER_PRESETS) as options, and the value reaches resolveStackTiers. --dev: isDevelopmentBoot (@objectstack/core, now serve's isDev) decides the secret fallback, and stackBootPlugins (cli/utils/stack-collections.ts, now serve's plugins line) merges devPlugins. The flags move the composition only. The step keeps the one-shot boot posture: NODE_ENV is untouched, no .env* file loads, and the standalone stack gets no dev key. None of these moves the gate or the held names.
  • AuthPlugin, the organizations plugin and the audit plugin are still constructed by serve alone. None of them holds a catalog name. Measured: the only manifest registration of permission sets among serve's platform plugins is security-plugin.ts's.
  • @objectstack/verify's harness composes AuthPlugin and the security plugin unconditionally. That is verify: the in-process handle boots a leaner stack than serve and has no door for eight things an app's tests need (requires[] capabilities, system/predicate update, the form door, user-less triggers, …), measured by hotcrm#2013 #22301's territory and is untouched here.

Premises re-measured (zone 2), on origin/main e148ca9842

  1. packages/runtime/src/standalone-stack.ts:910 hard-coded hydrateMetadataFromDb: true. Every os migrate database command booted through schema-migrate.ts:492 (new Runtime) and :522 (runtime.start()). Held. createStandaloneStack now accepts hydrateMetadataFromDb: false, and bootSchemaStack accepts hydrateMetadata: false. Both default to on.
  2. findEnvironmentHeldSecurityCatalogNames, declaredSecurityCatalogNames, BUILT_IN_SECURITY_CATALOG_NAMES and ENVIRONMENT_HELD_SECURITY_CATALOG_TYPES had 0 exports from packages/objectql/src/index.ts and core.ts. Held. None of the four is exported now either; the one new export is the package half of the reading.
  3. materializeStackPlugin does not cover it (above).
  4. The refused set depends on the boot env. Reproduced in the pin below. On one database and one compiled artifact, the cold boot was refused on 3 names without OS_AUTH_SECRET and 4 with it (the fourth is member_default, com.objectstack.plugin-security, stored under the legacy plural).
  5. Do a non-hydrated kernel's deletions write the same history and audit rows as a hydrated kernel's? Yes, for deleteMetaItem. Measured in a scratch run (not committed), with one database and two kernels of the step's own composition: one booted without hydration over stored rows, and one booted with hydration before the rows were written into it. Each called deleteMetaItem on one permission set and one position. The sys_metadata_history rows and the sys_metadata_audit rows were column-for-column identical apart from ids and timestamps (operation_type: delete, source: protocol.deleteMetaItem, recorded_by / actor = the step, outcome: allowed, code: ok, lock_state: none), and so were the receipts. What hydration does not decide, and the step's composition does: the security plugin's mutation projector registers in its start(), which a declaration boot suppresses, so no sys_permission_set re-projection runs in the step. The next boot's reconciler re-projects, as after Discard Overlay.

The ADR-0087 marker on #22307's changeset

.changeset/22307-cold-boot-catalog-refusal.md (unreleased; pre.json lists 0 consumed changesets):

  • The marker. It flips from not-required (no-migration-prescription) to registered security-catalog-environment-overlay-refused. That is a new D3 semantic entry (packages/spec/src/migrations/entries/semantic/18.security-catalog-environment-overlay-refused.ts, plus its generated region in registry.ts). Its replacement prescribes the step before the first v18 boot.
  • Why a registration. The gate's closed vocabulary has no other arm for "a migration prescription naming a step". Measured with the gate's own findMigrationPrescription on the base and on the rewritten body: null for both. The old marker would therefore still have passed mechanically; the flip carries out the ruling, not a gate demand. spec-changes.json and the upgrade guide do not move: major-18 entries do not project before protocol 18.
  • The upgrade shape now says to run the step before the first v18 boot.
  • The after-upgrade bullets point at the step, and the SQL stays as the statement of what the step deletes.
  • "No os command deletes a sys_metadata row offline" is rewritten. The pre-upgrade remedies are unchanged.

Tests

Local runs. Builds and tests went through scripts/pm/os-verify-lock.sh; each step's exit code was captured before any pipe. The branch merged origin/main 446c8b2a6, which moved core, runtime and plugin-security. After the merge, at ed5af305c8, these all exited 0: the whole-workspace build (72/72); typecheck of core, objectql, runtime and cli; core tests (88 / 2288); runtime tests (345 / 4878 passed, 19 skipped); the 7 touched cli unit files (186); and the 4 touched cli integration files (103). The numbers below are the pre-merge runs at 5a5cb1057.

  • Build of @objectstack/{objectql,spec,core,runtime,cli} and their closure: 59/59 tasks, exit 0.

  • Typecheck:

    • core 0, objectql 0, runtime 0;
    • cli 0, including check:test-typecheck.
  • Package tests:

    • core 87 files / 2276 tests;
    • objectql 391 / 7718;
    • runtime 345 / 4878 passed, 19 skipped;
    • spec src/migrations/* (3 files) 200 tests.
  • CLI unit tier: 274 files / 4055 tests, 1 red. The red was test/normalized-call-sites.test.ts: it flagged the new stack.requires read in schema-migrate.ts. That stack is createStandaloneStack's result, whose requires the runtime already resolves over package bodies. The read is now classified as a top-level row with that reason. Re-run: 11/11.

  • CLI integration, for the files this diff touches:

    • the new security-catalog-overlays.integration.test.ts;
    • schema-migrate.one-shot-family.integration.test.ts, where the step is now a declared caller with a preview mode and an --apply mode;
    • schema-migrate.host-composition.integration.test.ts;
    • schema-migrate.requires-providers.integration.test.ts.

    Total: 4 files / 103 tests, exit 0.

  • The step's pin (security-catalog-overlays.integration.test.ts) runs the command in-process through oclif over one SQLite database and one compiled artifact.

    • The preview, auth off and auth on. It lists the permission row, the package-bound position row and the legacy-plural positions row. With auth on it also lists the legacy-plural permissions/member_default row, held by com.objectstack.plugin-security. The controls are never listed: an environment-wide name no package holds, an organization-scoped row and a draft row.
    • Consistency. In both postures, the list equals the cold-boot refusal's conflicts[] over the same database and configuration. The refusal comes from the same composition booted with hydration on.
    • The preview writes nothing and exits 1.
    • --apply deletes exactly the 4 listed rows and every control stays. It writes 4 history tombstones and 2 ADR-0010 audit rows: one per canonical row, actor = the step. Then the cold boot comes up, and a second preview lists 0 and exits 0. With auth off, member_default is left alone and the boot comes up.
  • Ablation, run once and not committed: composeAuthGatedSecurity: true set to false in the command through scripts/ablation-replace.mjs (anchor 1 to 0, blob a484cc0a4 to 92908a4cb).

    • The pin went red, 3 of 4. The auth-on apply case listed 3 rows where the cold boot refuses 4 (member_default missing), and both preview cases lost their securityPlugin answer.
    • Restore proven: the blob is back to a484cc0a4 and git diff HEAD is 0 bytes.
  • The public door, measured by hand with the built CLI (bin/run.js, NODE_ENV=production) over one fixture database:

    • os serve without OS_AUTH_SECRET exits 1 and refuses 3 names. With it, os serve exits 1 and refuses 4: the same 3, plus member_default held by com.objectstack.plugin-security.
    • os migrate security-catalog-overlays --json lists exactly those 3 and those 4 rows, with securityPlugin no-secret and composed respectively.
    • After --apply with auth on (4 deleted: 2 via protocol.deleteMetaItem, 2 via sys-metadata-repository), os serve with auth on on the same database printed Server is ready. It was stopped by its own timeout.
  • Lint, measured over a stated narrowing:

    • pnpm exec eslint --no-inline-config --format json over the 18 changed .ts files: 18 file results, 0 errors, 0 warnings, 0 ignored.
    • The population is read from eslint.config.mjs: none of the 18 is ignored.
    • Invariance: eslint.config.mjs never enables type-aware linting (no parserOptions.project, no typed rules, stated at its lines 326 to 328), so this diff cannot move a verdict on an untouched file.

Patch round (head ae87d67c8d)

  • The merge. origin/main da989bbb24 was merged through scripts/pm/os-regen-merge.sh (16e58dc320).
  • The protocol-18 regeneration is its own commit (7dc9788339): spec-changes.json +14 and docs/protocol-upgrade-guide.md +3, additions only. check:spec-changes, check:upgrade-guide, check:migration-registry and check:generated all exit 0, the last against a freshly built spec dist.
  • Builds, typecheck and tests at ae87d67c8d:
    • the whole-workspace build passed (72/72), and typecheck exits 0 for core, objectql, runtime and cli;
    • core 88 / 2288, objectql 392 / 7739, runtime 345 / 4878 (+19 skipped);
    • spec migrations 3 / 200, cli unit 275 / 4059, and the 4 touched cli integration files 4 / 103.
  • Gates at ae87d67c8d: 125 derived families. 124 exit 0, and check-empty-changeset is red by design (the declared 22307 correction).
    • --ran: 125 derived, 125 run, 0 NOT-MEASURED, 0 UNRUN.
    • The 48 artifact-roster families all exit 0, check:error-code-provenance included. The 3 PR-context ones were run with PR_NUMBER=22523.
  • Changed lines after the merge: +1801 / −65 = 1,866 across 23 files, under the 3,000 threshold.

Patch round 2 (head 7f961c71a9)

The contract review FAIL 6087652785 and seat order 6087686331.

  • The spec changeset line. .changeset/22371-security-catalog-overlays-step.md names "@objectstack/spec": minor. A bullet under 'New exports it is built on' names the D3 entry security-catalog-environment-overlay-refused and its upgrade-guide projection.
  • --preset / --dev, read the way serve reads them.
    • Three serve lines now call the shared rules: isDev = isDevelopmentBoot(flags.dev), plugins = stackBootPlugins(config, flags.dev), and the --preset options Object.keys(STACK_TIER_PRESETS). The truth table, the array identity and the option order are unchanged, so serve composes what it composed.
    • The step passes serveFlags through bootSchemaStack to buildSchemaMigrationPlugins. There --dev composes the host config's devPlugins for their declarations, and the gate reads them; --preset reaches resolveStackTiers.
    • The JSON gains serveFlags, and the text report prints Composed as: os serve --preset NAME [--dev].
  • The pin. With --preset minimal and a secret: securityPlugin is auth-tier-off, member_default is not listed, and the list equals the cold-boot conflicts[] of the same flags. With --dev and no secret: composed, and 4 rows listed, equal to that boot's conflicts.
    • Ablation: dropping the preset from serveFlags turned the minimal case red ({ composed: true }). The restore was proven by blob.
  • Wording. The cli.mdx 'Data migrations' row says to run with the flags and environment the deployment boots with. So, beyond the order, do the D3 entry's replacement and acceptance text (registry.ts, spec-changes.json and the upgrade guide regenerated, check:generated / check:spec-changes / check:upgrade-guide / check:migration-registry green) and one word-group of the 22307 note's upgrade-shape sentence. All four surfaces give the operator one instruction.
  • Verification at 7f961c71a9:
    • build 72/72;
    • typecheck: core and cli exit 0;
    • core stack-auth 11/11; cli unit 9 files / 177; the step's integration file 6/6; host-composition plus one-shot-family 97/97; spec migrations 203/203;
    • eslint over the 21 changed .ts files: 0 / 0.
    • Full core, objectql and runtime suites are declared to CI.
  • Gates at 7f961c71a9:
    • 125 derived: 124 exit 0, and check-empty-changeset is red by design. --ran: 125/125, 0 NOT-MEASURED.
    • The 48 artifact-roster families exit 0, the 3 PR-context ones with PR context.
    • check-changeset-no-major with the PR event reads Clause-②: yes (widening) and passes.
  • Changed lines: +2025 / −76 = 2,101 across 26 files, under 3,000.

Landing step A (head f59b85e44e)

Seat order 6088904280. origin/main faf6348508 was merged through os-regen-merge.sh as 4cbd442f11, and the regeneration is f59b85e44e.

  • main's storage-scope-public-retired entry and its flow-value-slot-template-dialect-refused edit join this PR's entry in spec-changes.json and the upgrade guide. registry.ts regenerates to its merged bytes unchanged.
  • Both sides survive, by quoted exact-name git grep on origin/main and the head. Against origin/main, the PR's migration surfaces differ by additions only, and all of them are this PR's entry.
  • Green at the head: check:migration-registry, check:spec-changes, check:upgrade-guide, check:generated, check:authorable-surface, spec src/migrations (203), and the cli migrate-meta-engine-guidance integration file. The re-derived 125 families give 124 exit 0, with check-empty-changeset red by design, and the 48 roster families all pass.
  • The PR's own diff is unchanged: +2025 / −76 over the same 26 files.
  • The hop is not a pure regeneration (serve.ts, normalized-call-sites.test.ts and registry.ts moved on both sides), so a fresh contract review is owed on this head.

Gates

At ed5af305c8:

  • The 100 derived families (dispatch-gates.mjs --commands): 99 exit 0, and 1 is red by design. --ran reconciliation: 100 derived, 100 run, 0 NOT-MEASURED, 0 UNRUN.
  • check-empty-changeset.mjs --base origin/main is red by design. This PR deliberately corrects a pending release note: .changeset/22307-cold-boot-catalog-refusal.md, written by PR feat(objectql)!: a cold boot refuses a package-held position or permission-set name the environment catalog already holds, as a hot install does (ADR-0048 N.3) #22365 and not yet released. What changed under it: this PR adds the offline step its upgrade shape now prescribes. The corrections are the ADR-0087 marker, the upgrade shape, the after-upgrade bullets, and the sentence "No os command deletes a sys_metadata row offline". The gate's own text says this class takes a confirmation on the PR, not a restore from base. Requested here.
  • The 49 artifact-roster families: 46 exit 0, check:error-code-provenance among them. The step stamps no registered error code, and no owner-key row is owed. The other 3 need PR context: check-partof-closing-keyword.mjs with this body exits 0, and the two that need a PR number were run after this PR opened (see the report on the card).

Acceptance notes

  • content/docs/deployment/cli.mdx gains the step's rows in this PR (ae87d67c8d): one in the "Data migrations" table and one in the "If the database is in use" table, where --apply refuses a database a live process holds. The carrier noted in round 1 is discharged here.
  • A canonical row whose package item carries _lock: 'full' or 'no-delete' is refused by deleteMetaItem's lock gate (ITEM_LOCKED). The step reports that row failed with the reason and exits 1; the SQL in the changeset remains the statement of what to delete. Not measured on a real package: no shipped package declares a lock on a permission set or position.
  • serve.ts keeps a second spelling of isDev for port auto-shift: portAutoShiftAllowed = flags.dev || process.env.NODE_ENV === 'development', earlier in run() than isDev. It decides the port policy, not the composition, so it was left as is: outside this card's surface, no carrier.

Generated by Claude Code

claude added 8 commits October 9, 2026 15:16
@github-actions github-actions Bot added size/xl documentation Improvements or additions to documentation tests tooling labels Oct 9, 2026
@github-actions

github-actions Bot commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

40 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 ce78ff7bcd850f440b2bacc25a35cedbcc847c56.

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

What this run could not see
  • 3 changed file(s) yielded no anchor (packages/core/src/index.ts, packages/objectql/src/index.ts, packages/spec/spec-changes.json) — pages documenting those are invisible to this run
  • 1 anchor(s) matched too much of the corpus to be a work list: os serve (command, 31 pages)
  • 18 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 — 153 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 ce78ff7bcd850f440b2bacc25a35cedbcc847c56 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 7eb97d19fc0b7c7237abf2363533a2db662ad347 — the merge of head 9b0c250a70732c33a0b460cbe6e058e4be75983a into base ce78ff7bcd850f440b2bacc25a35cedbcc847c56, 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 7eb97d19fc0b7c7237abf2363533a2db662ad347 && git checkout 7eb97d19fc0b7c7237abf2363533a2db662ad347
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin ce78ff7bcd850f440b2bacc25a35cedbcc847c56 9b0c250a70732c33a0b460cbe6e058e4be75983a && git checkout -B drift-repro ce78ff7bcd850f440b2bacc25a35cedbcc847c56 && git merge --no-ff 9b0c250a70732c33a0b460cbe6e058e4be75983a

node scripts/docs-audit/affected-docs.mjs --json ce78ff7bcd850f440b2bacc25a35cedbcc847c56

⚠️ 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 ce78ff7bcd850f440b2bacc25a35cedbcc847c56 → 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

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: ae87d67c8d35119ca0438878e8454ffbbc45044a
Local-runs: none

Inputs, and nothing else: card #22371 (body and all 14 comments, rulings 6073500921 A′ and 6074838935 B included), PR #22523 (body, the 23-file list, the net diff of the branch against its merge-base with origin/main, da989bbb24, since the branch carries a merge of main at 16e58dc320), and the check-runs on the head. The seven required contexts — Lint & Repo Gates, TypeScript Type Check, Test Core, Dogfood Regression Gate, Build Core, Temporal Conformance (live PG + MySQL), Governed Surface Queue Guard — all conclude success; Console Pin Gate is skipped (no pin moved); Check Changeset concludes failure on all three runs on this head, the declared deliberate correction judged in ① below. Governed surfaces in the file list: none. Size: +1801 / −65 = 1,866 changed lines, under 3,000. Head repo is the base repo.

① Derived judgments

  1. New public CLI command os migrate security-catalog-overlays (packages/cli/src/commands/migrate/security-catalog-overlays.ts, packages/cli/src/utils/security-catalog-overlays.ts): preview by default; --apply, --yes / -y, --force, --json, --database-url (env OS_DATABASE_URL); the SQLite occupancy gate probed before boot and before the prompt; exit 1 when the preview lists a row, 0 when it lists none; under --apply 0 when every listed row is gone, 1 when one failed; 1 when a host config exists and cannot be loaded, before any row is read. Right. The population is type in the four stored spellings, organization_id null, state active, with no package_id filter, met with the engine's own holder reading — the ruling's letter B shape (composes what serve composes for phase 1, hydrates nothing, one engine reading, deletes through the audited write path). The integration pin runs the command in-process through oclif over one SQLite database and one compiled artifact and holds the list equal to the cold boot's conflicts[] in both auth postures, the three controls (an unheld environment-wide name, an organization-scoped row, a draft row) never listed, the preview writing nothing, and the cold boot coming up after --apply.
  2. The deletion path. A canonical row goes through protocol.deleteMetaItem (type, name, state: 'active', actor), repeated up to the group size and read back by id, so a name with two active rows (one bound to the package, one bound to none) loses both; a legacy-plural row goes through SysMetadataRepository.delete keyed by its stored type, name, package_id and checksum (parentVersion), intent: 'override-artifact'. Right. On an environment kernel deleteMetaItem admits the removal as the A legacy env overlay on an artifact-backed item of a rolled-back type can no longer be REMOVED through the ordinary delete path (403) — only via OS_METADATA_WRITABLE #6960 repair carve-out (permission / position merge overlays at read without allowOrgOverride); the repository's delete writes the sys_metadata_history tombstone for every active-row delete, under the stored spelling, with recorded_by = the actor; the audit row comes only through the protocol door, as the changeset says. No path writes managed_by or package_id; nothing renames. A row under an ITEM_LOCKED package item answers failed and exit 1 (③-8).
  3. @objectstack/objectql gains one export, findPackageHeldSecurityCatalogNames(registry), and the cold-boot check environmentHeldSecurityCatalogConflicts is now that list met with the bare slot. Right, and behaviour-preserving. The old reading walked bare-slot names and asked securityCatalogPackageHolders; the new one walks composite-slot names and install claims — exactly the domain securityCatalogPackageHolders answers from, so the held set is the same set — skips built-ins on the same predicate, checks the bare slot, and sorts type-then-name as before. The two new cases in protocol-boot-hydration-scoped.test.ts pin the list and pin the refusal's conflicts[] equal to that list met with the stored names. findEnvironmentHeldSecurityCatalogNames, declaredSecurityCatalogNames, BUILT_IN_SECURITY_CATALOG_NAMES and ENVIRONMENT_HELD_SECURITY_CATALOG_TYPES stay unexported: the ruling's "one new export" holds.
  4. The NAMESPACE_CONFLICT refusal text now names the step for the environment's rows. Right. The envelope (code, status: 422, conflicts[] with catalogType, name, incomingPackageId, existingHolder) is untouched; the runtime string carries an ADR cite and no tracker number.
  5. @objectstack/core gains stack-auth.ts, re-exported from the index: STACK_TIER_PRESETS, CAPABILITY_TO_TIER, resolveStackTiers, stackSuppliesAuthPlugin, isHostKernelComposition, DEV_AUTH_SECRET_FALLBACK, resolveAuthSecret, resolvePlatformAuthComposition, and the types PlatformAuthSkipReason, PlatformAuthComposition. Right. The ruling's first reading is answered in the open: materializeStackPlugin (97610a533) did not cover the auth-gated security plugin, so the gate is extracted as one rule with two readers (verify: the in-process handle boots a leaner stack than serve and has no door for eight things an app's tests need (requires[] capabilities, system/predicate update, the form door, user-less triggers, …), measured by hotcrm#2013 #22301 ruling A's one-composition principle). The rule's order is serve's: stack-supplies-auth, auth-tier-off, host-kernel, no-secret. DEV_AUTH_SECRET_FALLBACK is the literal serve already carried, now public and named "dev-only-insecure"; declared in the changeset. The unit test covers every clause.
  6. serve.ts reads the rule and composes what it composed. Right. TIER_PRESETS and CAPABILITY_TO_TIER become handles over the core declarations with the same members; tiers comes from resolveStackTiers({ declaredTiers, requires, preset }), which is the old precedence verbatim (declared tiers, else the preset or default, plus each requires token's tier); the gate line if (!hasAuthPlugin && tierEnabled('auth')) is kept with hasAuthPlugin from the rule's predicate; inside it, the host-kernel branch then the no-secret branch read authComposition, and secret is the rule's — the same OS_AUTH_SECRET / AUTH_SECRET / BETTER_AUTH_SECRET chain with the same development fallback. The rule's first two clauses cannot fire inside the block because the outer if excludes them, so the three warnings and the construction are unchanged.
  7. The step's composition (schema-migration-plugins.ts): authGatedSecurity is an opt-in only os migrate security-catalog-overlays passes; the early return for a config-less, artifact-less project is kept for every other caller; the security plugin is wrapped by composeForDeclarations (its start() suppressed, so no sys_permission_set seeding and no projector run) and spliced ahead of the host plugins where serve registers it; the gate's answer is reported as securityPlugin (composed, a skip reason, or config-unloadable). Right. Read, not re-measured: the requires fed to the gate (the artifact's as the standalone stack surfaced them for a non-host config, else the config's) — the touched schema-migrate.requires-providers pin and the new normalized-call-sites row cover that read. The step takes the default preset and NODE_ENV (③-2).
  8. @objectstack/runtime: createStandaloneStack accepts hydrateMetadataFromDb (Zod-declared, optional; only false changes what the plugin receives, so a serving boot gets exactly the options it always did). Right, pinned with a control over the same file. bootSchemaStack gains hydrateMetadata and composeAuthGatedSecurity (CLI-internal, default off). Right.
  9. packages/spec: the D3 semantic entry security-catalog-environment-overlay-refused (entries/semantic/18.*.ts, its generated region in registry.ts), with spec-changes.json (+14) and docs/protocol-upgrade-guide.md (+3) regenerated as additions in their own commit 7dc9788339 after an os-regen-merge.sh merge. Right in shape (id, a surface with no backticks, replacement, reason, acceptanceCriteria — the structured-TODO form ADR-0087 D3 names for a change no conversion can express) and in substance: the replacement prescribes the step before the first boot on this major and the --apply deletion through the write path, the "Done when" is the preview's exit 0, and the reason's "only where the boot composes it behind the auth gate (an auth secret set, or a development boot)" matches resolvePlatformAuthComposition. The check that gates this (check:spec-changes, check:upgrade-guide inside TypeScript Type Check) is green on the head.
  10. content/docs/deployment/cli.mdx, two rows. Right: preview by default, one tombstone per row, adopts nothing, exits 1 while a row is listed, and --apply refuses an occupied database (the occupancy gate above).
  11. Tests and the call-sites ledger. The new integration pin; the step registered as a caller in the one-shot-family pin (a preview mode and an --apply mode over a fixture with no held name); stack-auth.test.ts; the two objectql cases; the runtime hydrate-off case. normalized-call-sites.test.ts gains one top-level row for schema-migrate.ts :: stack.requires with its reason (the standalone stack's result, already resolved over package bodies) — a classification, not a baseline widening. Right.
  12. .changeset/22307-cold-boot-catalog-refusal.md — the declared deliberate correction, written by PR feat(objectql)!: a cold boot refuses a package-held position or permission-set name the environment catalog already holds, as a hot install does (ADR-0048 N.3) #22365 and unreleased (pre.json is in next pre mode). The diff rewrites seven units; each sentence judged against the code at this head and the rulings:
    • The marker, not-required (no-migration-prescription) … becomes registered security-catalog-environment-overlay-refused. Right. Ruling A′ item 1 orders exactly this flip ("to a migration prescription naming this step"); ADR-0087's closed vocabulary spells a prescription only as registered with an id that resolves and is new in the diff, and this one is both (item 9). The mechanical reading the PR body reports — the gate's findMigrationPrescription is null on both bodies, so the old marker would still have passed — makes the flip the ruling's, not a gate's, and that is the right reason to flip it.
    • The upgrade shape, the appended sentence "Run os migrate security-catalog-overlays before the first v18 boot, with the environment the deployment boots with: it lists exactly these rows, and with --apply deletes them." Right. "Exactly these rows" is the one-reading equality pinned in both auth postures (item 1); "with the environment the deployment boots with" is the qualifier the auth gate needs, since OS_AUTH_SECRET or NODE_ENV=development decides whether the platform security plugin's sets are held. The residue the qualifier does not cover is a CLI flag, not an environment variable (③-2).
    • The one-line fix: "before the first v18 boot, run os migrate security-catalog-overlays to list the rows, then os migrate security-catalog-overlays --apply to delete them; or rename the item in the package." Right. Dropping "rename or delete the environment's item, then restart" is correct for the refused deployment: the metadata API needs a server that does not boot, the step renames nothing and adopts nothing, and the new changeset says a row the environment needs is re-created under a name no package holds.
    • "After upgrading, for any refused name." Three sentences. "The same step: os migrate security-catalog-overlays, then --apply." — right. "It needs no server, so a deployment whose boot is refused can run it" — right: a one-shot kernel with hydration off, no HTTP; the public-door measurement and the pin boot the refused database. "It covers permission sets and positions, a name the platform security plugin declares, and a row stored under a legacy plural" — right: both catalog types, both spellings, and the security plugin's names whenever the deployment's own boot held them (a refused name was held, so the step run with the same environment holds it too).
    • "What the step deletes." Five sentences. The population sentence (organization_id IS NULL, state = 'active', whatever the package_id, under the type or its legacy plural) — right, it is the where the lister uses. The SQL sentence — right as a statement of the net effect: for one name the step removes every active environment-wide row under both spellings, through id-tracked protocol calls for the canonical rows and the repository for the plural rows (the SQL's name placeholder stands for the listed name; no SQL is executed by the step). "The step runs it through the metadata write path, so each deleted row leaves a sys_metadata_history tombstone" — right on both doors (item 2; the pin asserts four tombstones for four rows). "A draft row and an organization-scoped row are not loaded at boot and do not refuse it" — unchanged from the base and still true of the hydration filter.
    • The plural-row paragraph's last sentence: "… or by os migrate security-catalog-overlays --apply, for any name." (was "the SQL above after upgrading"). Right: the repository door reaches a plural row; the base's "after upgrading" is rightly dropped, since the step runs before or after.
    • The "one os command" paragraph. "os migrate security-catalog-overlays is the one os command that deletes these rows with no server running; os meta delete and os data delete call a running server." — right: no other command under packages/cli/src/commands/ deletes a sys_metadata row (meta --stored rewrites, recorded-by rewrites history, audit-metadata-bodies copies). "Nothing renames or removes either item automatically: the step deletes only under --apply, and adopts nothing." — right: the preview path never reaches a delete, and no write touches managed_by or package_id.
    • The engine seat's three wording points on the card (6073732548) are all applied: line 29 rewritten, the after-upgrade bullets pointed at the step with the SQL kept as the statement of what it does, and the "Before upgrading" bullet byte-identical.
      Every rewritten sentence is right; the Check Changeset red is the DELIBERATE CORRECTION class the gate names, confirmed as to its content. This record's verdict is FAIL on ② alone, so the red's landing confirmation waits for the record on the patched head; the sentence judgments above carry unchanged unless the note moves again.

② Semver level

  • The PR's Clause-②: yes (widening) line — right: a new public CLI command, one new objectql export, the core auth-gate exports, a new runtime config key; nothing is narrowed or renamed; no migration is owed by the widening itself. The 22307 note's own Clause-②: no is untouched and not re-judged here.
  • .changeset/22371-security-catalog-overlays-step.md: @objectstack/cli, @objectstack/objectql, @objectstack/core, @objectstack/runtime, all minor, with every new export named — right for those four, and minor is the floor Clause-②: yes takes.
  • Wrong — @objectstack/spec publishes and no changeset names it. The diff adds an entry to the migration ledger that @objectstack/spec exports at ./migrations (the ledger os migrate meta --from 17 imports) and regenerates spec-changes.json, which the tarball ships (files). Neither the 22371 changeset nor the 22307 note (now carrying the registered marker, frontmatter '@objectstack/objectql': major alone) names spec. The repo rule is a changeset for anything that publishes; the practice is uniform — all 17 pending changesets carrying a registered marker bump @objectstack/spec, and the consumed registration of the same shape (a D3 entry with no schema change: .changeset/21771-write-door-unreadable-is-not-found.md at 53021e3a63, registered by-id-write-unreadable-row-not-found) carried "@objectstack/spec": patch for it. Under the fixed group no version number moves, so the cost is confined to spec's released CHANGELOG.md saying nothing about the new entry — the one channel an upgrading agent greps inside the npm package, and a release deletes the input that would have fixed it. Remedy, one line and one bullet: add "@objectstack/spec": minor to the 22371 changeset's frontmatter (matching its four siblings; patch is the fix(plugin-security)!: on the write doors, a row the caller cannot read answers what a nonexistent id answers #21812 precedent's level and also acceptable) and a bullet under "New exports it is built on" naming the ledger entry security-catalog-environment-overlay-refused. Placing the bump on the 22307 note instead is the other admissible spelling; the PR's own changeset is the cleaner one. Nothing else in the diff moves for this.
  • The marker on the 22307 note, registered security-catalog-environment-overlay-refused — right (①-12).

③ Boundary flags

  1. open_questions (round 1): keep the D3 registration, option A. Answered: right, as the seat ruled. The ruling orders a prescription marker; the closed vocabulary admits one only as registered with an entry; D3 is where a step a human executes belongs; the entry's shape and substance are judged right in ①-9. The two spec files outside the claim's declared surface are the spec seat's home lane.
  2. No --preset and no --dev on the step (dev flag, "still owed"). Escalated, not blocking: the seat files a follow-up card in the domain:cli lane for flag parity with os serve, carried from this record. The one divergent posture: a deployment served with --preset minimal (auth tier off, so serve composes no security plugin and its eight shipped names are not held) while an auth secret is set in the environment — there the step reads the default preset, composes the plugin, lists an environment-wide row over one of those names and, on --apply, deletes a row that deployment's own boot does not refuse. Not blocking because the preview is the default and exits 1, it prints the gate's answer (securityPlugin) and each row's holder before any --apply, the deletion leaves a restorable tombstone, and a config that declares tiers is read exactly as serve reads it. The --dev half matters only for a boot run with --dev and without NODE_ENV=development, the same direction.
  3. The NAMESPACE_CONFLICT message rewrite (dev flag) — right (①-4); no test pinned the old text, and the envelope is unchanged.
  4. The normalized-call-sites row (dev flag) — right (①-11).
  5. serve.ts keeps its literal gate line (dev flag) — right (①-6); the two contract pins anchored on it stay as they are, and the composition is unchanged.
  6. AuthPlugin, the organizations plugin and the audit plugin stay serve-only; @objectstack/verify's harness composes auth and security ungated (dev flags) — right: none of the three registers a catalog name (the PR's measurement; security-plugin.ts is the only manifest registration among them), and the harness is verify: the in-process handle boots a leaner stack than serve and has no door for eight things an app's tests need (requires[] capabilities, system/predicate update, the form door, user-less triggers, …), measured by hotcrm#2013 #22301's open territory.
  7. Premise 5 — history and audit rows from a kernel that hydrated nothing (ruling B's confidence gap, carried as a premise) — closed: measured by the dev in a scratch run and, independently of that report, pinned by the integration test's trail assertions (four delete tombstones with the actor, two allowed audit rows for the two canonical rows).
  8. A canonical row under a package item declaring _lock full or no-delete (carried, not filed): deleteMetaItem's lock gate answers ITEM_LOCKED, the step reports that row failed and exits 1, and the note's SQL remains the statement of what is to be deleted. No shipped package declares such a lock on a permission set or position. Accepted as carried; no card is owed until a package does.
  9. Docs, cross-lane declarations, the merge and the regeneration: cli.mdx discharged in the patch round; packages/cli and packages/runtime (domain:cli), packages/objectql and packages/core (domain:engine), content/docs (domain:devx) declared by the seat; the merge through os-regen-merge.sh with the regeneration in its own commit — process facts, consistent with the diff.
  10. Ruling B's other confidence gaps: PR feat(core,cli,verify): bootStack composes what serve composes — item 1 stage 2 of #22301 (HELD at stop conditions) #22381's hold was lifted and it landed (97610a533); the core composition rule did not cover the auth gate, and the extraction is done and reported (①-5). Closed.
  11. The sibling-not-mode decision (the ruling asked the dev to report if meta --stored was not the nearest landing): reported in the PR body with three reasons — different operation, different boot, different exit contract. Consistent with the code: --stored --apply rewrites through saveMetaItem, the step deletes one population; --stored boots without the host config and hydrates, the step composes the host stack and does not.

Implemented-by: claude/issue-22371-overlay-cleanup-step
Reviewed-by: session_01KNKBCRDJCu5tGy3TEbvtrF

VERDICT: FAIL

On ② alone: @objectstack/spec publishes a new ledger entry and no changeset names it. Patch: one frontmatter line and one bullet on .changeset/22371-security-catalog-overlays-step.md. Every ① judgment, every rewritten sentence of the 22307 note, and every ③ answer above is right and carries to the patched head.


Generated by Claude Code

…eset and --dev, read through serve's own rules

isDevelopmentBoot (core) is serve's isDev; stackBootPlugins (cli) is serve's
devPlugins merge; the preset options are STACK_TIER_PRESETS' keys in both
commands. The 22371 changeset names @objectstack/spec and its D3 entry; the
entry, the 22307 note and the cli docs say to run the step with the flags and
environment the deployment boots with.

Claude-Session: https://claude.ai/code/session_01KNKBCRDJCu5tGy3TEbvtrF
Co-authored-by: Claude <[email protected]>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 7f961c71a9953b97f283a437e37f4e90cdf206f0
Local-runs: none

Re-review after the FAIL record 6087652785 at ae87d67c8d; the patch round is ae87d67c8d..7f961c71a9 (17 files, +267 / −54), and this verdict covers the whole net diff at this head. Inputs, and nothing else: card #22371 (body and all 16 comments — rulings 6073500921 A′ and 6074838935 B, the engine seat's wording points 6073732548, the three dev reports, the seat's reviews, the ACCEPT and the FAIL-adoption order 6087686331), PR #22523 (body, both comments, the 26-file list, and the net diff of the branch against its merge-base with origin/main, re-derived as da989bbb24), and the 42 check-runs on the head. The seven required contexts — Lint & Repo Gates, TypeScript Type Check, Test Core, Dogfood Regression Gate, Build Core, Temporal Conformance (live PG + MySQL), Governed Surface Queue Guard — all conclude success; Check Changeset concludes failure on both runs on this head (the declared deliberate correction, judged in ①-12 and ②); Console Pin Gate is skipped (no pin moved). Governed surfaces in the file list: none. Size: +2025 / −76 = 2,101 changed lines across 26 files, under 3,000. Head repo is the base repo; the PR is a draft awaiting this record.

① Derived judgments

Carried from the earlier record and re-read against this head, every item the round touched re-judged on the new code:

  1. New public CLI command os migrate security-catalog-overlays (packages/cli/src/commands/migrate/security-catalog-overlays.ts, packages/cli/src/utils/security-catalog-overlays.ts): preview by default; --apply, --yes / -y, --force, --json, --database-url (env OS_DATABASE_URL); and now --preset (options Object.keys(STACK_TIER_PRESETS): minimal, default, full) and --dev. The SQLite occupancy gate is probed before boot and before the prompt; exit 1 when the preview lists a row, 0 when none; under --apply 0 when every listed row is gone, 1 when one failed; 1 when a host config exists and cannot be loaded, before any row is read. Right. The population is the four stored spellings (permission, permissions, position, positions) with organization_id null and state active and no package_id filter, met with the engine's own holder reading — ruling B's shape. The text report prints Composed as: os serve --preset NAME [--dev] and the gate's answer, and the JSON carries serveFlags beside securityPlugin.
  2. The deletion path. A canonical row goes through protocol.deleteMetaItem (type, name, state: 'active', actor), repeated up to the group size and read back by id; a legacy-plural row through SysMetadataRepository.delete keyed by its stored type, name, package_id and checksum as parentVersion, intent: 'override-artifact'. Right. On an environment kernel deleteMetaItem admits the removal under the 2026-08-10 A legacy env overlay on an artifact-backed item of a rolled-back type can no longer be REMOVED through the ordinary delete path (403) — only via OS_METADATA_WRITABLE #6960 carve-out (the rolled-back overlayable tier, permission / position), then assertLockAllowsDelete (ADR-0010 L3) — a locked package item answers ITEM_LOCKED, which the step reports as failed (③-9). The repository's delete runs its own A legacy env overlay on an artifact-backed item of a rolled-back type can no longer be REMOVED through the ordinary delete path (403) — only via OS_METADATA_WRITABLE #6960 gate (assertDeleteAllowed), takes the lock through lockAccepts — a null stored checksum against a null parentVersion is accepted, which is what the seeded rows carry — removes the row and, for an active row, inserts the sys_metadata_history tombstone (operation_type: 'delete', recorded_by = the actor) in the same transaction. The audit row comes only through the protocol door, as the changeset says. No path writes managed_by or package_id; nothing renames; the preview path never reaches the deleter.
  3. @objectstack/objectql gains one export, findPackageHeldSecurityCatalogNames(registry), and the cold-boot check environmentHeldSecurityCatalogConflicts is now that list met with the bare slot. Right, and behaviour-preserving: the old reading walked bare-slot names and asked securityCatalogPackageHolders; the new one walks composite-slot names and install claims — the only two sources that predicate answers from — asks the same predicate, skips built-ins on the same set, keeps only names the bare slot holds, and sorts type-then-name as before. Same set, one reading. The two cases added to protocol-boot-hydration-scoped.test.ts pin the list and pin the refusal's conflicts[] equal to that list met with the stored names. The environment half, declaredSecurityCatalogNames, BUILT_IN_SECURITY_CATALOG_NAMES and ENVIRONMENT_HELD_SECURITY_CATALOG_TYPES stay off the package entries: the ruling's "one new export" holds.
  4. The NAMESPACE_CONFLICT refusal text names the step for the environment's rows. Right. The envelope is untouched (code, status: 422, conflicts[] with catalogType, name, incomingPackageId, existingHolder); the runtime string cites ADR-0048 and no tracker number.
  5. @objectstack/core gains stack-auth.ts, re-exported from the index: STACK_TIER_PRESETS, CAPABILITY_TO_TIER, resolveStackTiers, stackSuppliesAuthPlugin, isHostKernelComposition, DEV_AUTH_SECRET_FALLBACK, isDevelopmentBoot (new this round), resolveAuthSecret, resolvePlatformAuthComposition, and the types PlatformAuthSkipReason, PlatformAuthComposition. Right. The rule's order is serve's (stack-supplies-auth, auth-tier-off, host-kernel, no-secret); isDevelopmentBoot(devFlag) is devFlag === true || NODE_ENV === 'development', the truth table of serve's old flags.dev || NODE_ENV === 'development'. The unit test covers every clause, the new one included, with NODE_ENV saved and restored.
  6. serve.ts reads the rules and composes what it composed. Right, re-read line by line at this head: TIER_PRESETS and CAPABILITY_TO_TIER are handles over the core declarations with the same members; the --preset options are the same three strings in the same order; isDev = isDevelopmentBoot(flags.dev); tiers = resolveStackTiers({ declaredTiers, requires, preset: presetName }) is the old precedence verbatim (presetName is always defined there); plugins = stackBootPlugins(config, flags.dev) is the old config.plugins || [] plus devPlugins under the flag, the same array reference when nothing merges; the gate line if (!hasAuthPlugin && tierEnabled('auth')) stays with hasAuthPlugin from the rule's predicate, and inside it the host-kernel branch, then the no-secret branch, read authComposition — the first two skip reasons cannot occur inside the block because the outer if uses the same predicate and the same tier set — so the three warnings and the construction are unchanged, and secret is the rule's (OS_AUTH_SECRET / AUTH_SECRET / BETTER_AUTH_SECRET, silent, then the development fallback). serve registers the security plugin (kernel.use, :4142) ahead of its config-plugin loop (:4242), the order the composition reproduces by splicing it at index 1 behind the write guard.
  7. The step's composition (schema-migration-plugins.ts): authGatedSecurity is an opt-in only the step passes; the early return for a config-less, artifact-less project is kept for every other caller; composeAuthGatedSecurity feeds the gate the base plugins plus the host plugins (now by stackBootPlugins under --dev), the config's declared tiers and the requires serve reads (the artifact's as the standalone stack surfaced them for a non-host config, else the config's), the caller's --preset through resolveStackTiers, and resolveAuthSecret({ isDev: isDevelopmentBoot(dev) }); the security plugin is wrapped by composeForDeclarations (its start() suppressed, so no sys_permission_set seeding and no projector run); the composition note now counts the devPlugins. Right. Read, not re-measured, and pinned: the unit case (--dev composes the config's devPlugins, and a dev-only com.objectstack.auth flips the gate to stack-supplies-auth) and the integration file's equality in four postures (item 11).
  8. @objectstack/runtime: createStandaloneStack accepts hydrateMetadataFromDb (Zod-declared, optional; only false changes what the plugin receives, so a serving boot gets the options it always did). Right, pinned with a control over the same file. bootSchemaStack gains hydrateMetadata, composeAuthGatedSecurity and serveFlags (CLI-internal, default off). Right; serveFlags reaches only the composition — the standalone stack gets no dev key, as the option says.
  9. packages/spec: the D3 semantic entry security-catalog-environment-overlay-refused (entries/semantic/18.*.ts, its generated region in registry.ts), with spec-changes.json and docs/protocol-upgrade-guide.md regenerated. This round moved its replacement ("with the flags and environment the deployment boots with (--preset and --dev mean what they mean to os serve)") and acceptanceCriteria ("with its boot flags and environment"), and the three projections follow byte-for-byte. Right in shape (surface carries no backticks; the five D3 fields the sibling entries carry) and in substance: the gate now moves with those two flags exactly as serve's does, so a "Done when" that omitted them would be met by a list the deployment's own boot does not refuse. check:spec-changes, check:upgrade-guide and check:migration-registry run inside the green TypeScript Type Check.
  10. content/docs/deployment/cli.mdx, two rows. The "Data migrations" row now says to run with the flags and environment the deployment boots with, names the auth-tier condition, and says the step takes serve's --preset and --dev with serve's meaning; the occupancy row is unchanged. Right against items 1 and 7.
  11. Tests and the call-sites ledger. The integration pin now runs four preview postures — auth off (no-secret), auth on (composed), --preset minimal with a secret (auth-tier-off, member_default not listed), --dev with no secret (composed, four rows) — each asserting serveFlags, the list equal to the cold boot's conflicts[] of the same composition with the same flags, exit 1, and no write; the two --apply cases are unchanged (four tombstones, two audit rows, the controls kept, the cold boot then up). stack-collections.test.ts (three stackBootPlugins cases), schema-migration-plugins.test.ts (the --dev case), stack-auth.test.ts, the one-shot-family registration, the two objectql cases, the runtime hydrate-off case, and the one top-level row in normalized-call-sites.test.ts for schema-migrate.ts :: stack.requires. Right. The dev's reported ablation (dropping the preset from serveFlags turned the minimal case red; restore proven by blob) is consistent with the pin's shape; not re-run here.
  12. .changeset/22307-cold-boot-catalog-refusal.md — the declared deliberate correction, written by PR feat(objectql)!: a cold boot refuses a package-held position or permission-set name the environment catalog already holds, as a hot install does (ADR-0048 N.3) #22365 and unreleased (pre.json is next pre mode). Every changed sentence at this head, judged against the code and the rulings:
    • The marker, from not-required (no-migration-prescription) … to registered security-catalog-environment-overlay-refused. Right. Ruling A′ item 1 orders the flip "to a migration prescription naming this step"; the gate's closed vocabulary spells a prescription only as registered with an id that resolves and is new in the diff — this one is both (item 9), so the registered-claims-a-registration-this-PR-made check holds; check-adr-0087-registration runs inside the green Lint & Repo Gates.
    • The upgrade shape's appended sentence: "Run os migrate security-catalog-overlays before the first v18 boot, with the flags and environment the deployment boots with: it lists exactly these rows, and with --apply deletes them." Right — and the word-group that moved this round, "the environment" to "the flags and environment", is the correction the round's flags make necessary: --preset minimal turns the auth tier off and --dev turns the development fallback on, so an operator who ran the step with the environment but not the flags would get a list that differs from that boot's refusal (pinned both ways, item 11). "Exactly these rows" is the four-posture equality.
    • The one-line fix: list, then --apply, or rename the item in the package. Right: preview-then-apply is the command's only shape; the step adopts and renames nothing, so the package-side rename stays the other remedy.
    • "After upgrading, for any refused name." Three sentences — the same step; "It needs no server, so a deployment whose boot is refused can run it" (a one-shot kernel with hydration off, no HTTP; the pin seeds and runs over a database the cold boot refuses); "it covers permission sets and positions, a name the platform security plugin declares, and a row stored under a legacy plural" (both types, both spellings, and the plugin's names whenever the same flags and environment compose it — a refused plugin name was refused by a boot that composed it). Right.
    • "What the step deletes." The population sentence is the lister's where; the SQL sentence is a statement of the net effect for one name (every active environment-wide row under both spellings; the step executes no SQL); "through the metadata write path, so each deleted row leaves a sys_metadata_history tombstone" holds on both doors (item 2); the draft/organization sentence is unchanged and still true of the hydration filter. Right.
    • The plural-row paragraph's last sentence, "… or by os migrate security-catalog-overlays --apply, for any name." Right: the repository door reaches a plural row (pinned on positions/m22371_old_lead and permissions/member_default).
    • The "one os command" paragraph. "os migrate security-catalog-overlays is the one os command that deletes these rows with no server running; os meta delete and os data delete call a running server." Right: under packages/cli/src the only non-test callers of deleteMetaItem or SysMetadataRepository are the step's own utils. "Nothing renames or removes either item automatically: the step deletes only under --apply, and adopts nothing." Right (item 2).
    • The engine seat's three wording points (6073732548) are applied, and the "Before upgrading" bullet is byte-identical to the base.
      Every rewritten sentence, the moved word-group included, is right. The Check Changeset red is the DELIBERATE CORRECTION class the gate names (its own remedy: do NOT restore it — say so on the PR and get it confirmed); this record is that confirmation, on content, at this head.

② Semver level

  • The PR's Clause-②: yes (widening) line — right: a new public CLI command with two more flags, one new objectql export, eleven core exports, a new runtime config key and a new spec ledger entry; nothing narrowed or renamed; no migration is owed by the widening itself. The 22307 note's own Clause-②: no is PR feat(objectql)!: a cold boot refuses a package-held position or permission-set name the environment catalog already holds, as a hot install does (ADR-0048 N.3) #22365's declaration about its own change and is not re-judged here.
  • .changeset/22371-security-catalog-overlays-step.md, each package it publishes in, against its own entry:
    • @objectstack/cli: minor — right (the command and its flags; the bootSchemaStack options and stackBootPlugins are internal).
    • @objectstack/objectql: minor — right; findPackageHeldSecurityCatalogNames named, the refusal-text change named, the envelope stated unchanged.
    • @objectstack/core: minor — right; all eleven stack-auth exports named, isDevelopmentBoot added this round; Serve.TIER_PRESETS / Serve.CAPABILITY_TO_TIER as handles stated.
    • @objectstack/runtime: minor — right; createStandaloneStack's hydrateMetadataFromDb: false named, the default stated.
    • @objectstack/spec: minor — right, and the earlier record's ② defect is discharged. The package publishes the new D3 entry through its ./migrations entry and ships spec-changes.json in its tarball (files); the frontmatter line and the bullet naming security-catalog-environment-overlay-refused, its registry and spec-changes.json records and its protocol-18 projection in the upgrade guide are now in the file. minor matches the four siblings and is the floor Clause-②: yes takes; the eighteen other pending notes carrying a registered marker all bump @objectstack/spec, so the practice holds.
    • The new sentence on --preset / --dev (read through the rules serve reads them by; run the step with the flags and environment the deployment boots with) — right (①-6, ①-7).
  • The 22307 note's marker registered security-catalog-environment-overlay-refused — right (①-12). The correction stands on content: every changed sentence of that file is right against the code at this head and rulings A′ and B, and the one word-group changed since the earlier record is right for the reason given there.
  • check-changeset-no-major reads Clause-②: yes (widening) and no patch on a package whose src moves (inside the green Lint & Repo Gates).

③ Boundary flags

  1. The earlier record's ③-2 escalation (no --preset / --dev; a follow-up card in domain:cli) — discharged in-PR by the seat's order 6087686331 and this round: the step takes both flags with serve's meaning through the shared rules, with no near-copy of either (the three serve lines now call the rules). The divergent posture that record named (--preset minimal with a secret set) is now the pinned auth-tier-off case. Closed; no card owed.
  2. Dev deviation: the wording reached the D3 entry's replacement / acceptanceCriteria and one word-group of the 22307 note, beyond the order — right, judged in ①-9 and ①-12: the four operator-facing surfaces (the changeset, the D3 entry and its upgrade-guide projection, cli.mdx) give one instruction, and the one that omitted the flags would have been wrong once the flags move the gate.
  3. Dev boundary, reported and not a stop: the flags move the composition only — NODE_ENV defaulting, the .env cascade and the standalone stack's dev key are not taken. Accepted as reported. serve loads .env* files through dotenv-flow (serve.ts :2126) and bootSchemaStack does not, so a secret that lives only in a .env file is unseen by the step: the gate answers no-secret, the plugin's row is not listed, and the next os serve refuses on that one name. The direction is under-listing, never a deletion the deployment's own boot would not have refused; the preview prints the gate's answer before any --apply; the refusal names the residue; and every operator-facing surface says to run with the environment the deployment boots with. isDevelopmentBoot answers identically in all four flag-by-NODE_ENV cases, and the standalone stack correctly gets no dev key (no schema self-heal on a one-shot boot). No card owed; if a carrier is ever wanted it is a docs line naming .env files explicitly.
  4. Dev deviation: serve.ts changed in three lines, behaviour unchanged — right (①-6).
  5. Dev finding, not filed: serve.ts keeps a second spelling of isDev for port auto-shift (portAutoShiftAllowed, :2036) — accepted as carried: it decides port policy, not composition, has the same truth table today, and no reader of it decides a held name. Outside this card's surface; a drift-class note, no carrier owed.
  6. The NAMESPACE_CONFLICT message rewrite, the normalized-call-sites row, serve.ts keeping its literal gate line (round-1 flags) — right (①-4, ①-11, ①-6), unchanged by the round.
  7. AuthPlugin, the organizations plugin and the audit plugin stay serve-only; @objectstack/verify's harness composes auth and security ungated — right: none of the three registers a catalog name (security-plugin.ts is the only manifest registration among them), and the harness is verify: the in-process handle boots a leaner stack than serve and has no door for eight things an app's tests need (requires[] capabilities, system/predicate update, the form door, user-less triggers, …), measured by hotcrm#2013 #22301's open territory.
  8. Premise 5 (history and audit rows from a kernel that hydrated nothing) — closed: the integration pin's trail assertions hold on this head (four delete tombstones with the actor, two allowed audit rows for the two canonical rows), independently of the dev's scratch measurement.
  9. A canonical row under a package item declaring _lock full or no-delete (carried, not filed) — assertLockAllowsDelete answers ITEM_LOCKED, the step reports failed and exits 1, the SQL stays the statement of what to delete. No shipped package declares such a lock on a permission set or position. Accepted as carried; no card owed until a package does.
  10. Round-1 open_questions: keep the D3 registration (option A) — answered right, as the seat ruled: the entry is the only spelling the closed vocabulary offers for the ruled prescription (①-12), and the ledger is where os migrate meta --from 17 reads it.
  11. Ruling B's confidence gaps — PR feat(core,cli,verify): bootStack composes what serve composes — item 1 stage 2 of #22301 (HELD at stop conditions) #22381 landed (97610a533); materializeStackPlugin did not cover the auth gate, and the extraction is done and reported (①-5); history and audit measured and pinned (item 8). Closed. The sibling-not-mode decision the ruling asked the dev to report is in the PR body with three reasons (operation, boot, exit contract), consistent with the code.
  12. Process facts, consistent with the diff: origin/main was not merged this round (optional, not taken; the queue rebuilds on current main); the cross-lane declarations (packages/cli and packages/runtime — domain:cli; packages/objectql and packages/core — domain:engine; content/docs — domain:devx) are the seat's; lock use split per suite as ordered. The round-3 report's open_questions is empty.

Implemented-by: claude/issue-22371-overlay-cleanup-step
Reviewed-by: session_01KNKBCRDJCu5tGy3TEbvtrF

VERDICT: PASS

The ② defect of the earlier record is fixed on this head (@objectstack/spec named at minor, with its bullet); every ① judgment and every rewritten sentence of the 22307 note, the moved word-group included, is right; every ③ flag is answered or closed. The Check Changeset red is confirmed by this record as a DELIBERATE CORRECTION on content; the queue enforces the seven required contexts, all green on this head.


Generated by Claude Code

claude added 2 commits October 9, 2026 20:42
…the merged migration registry

os-regen-merge step 4: main's storage-scope-public-retired entry and its
flow-value-slot-template-dialect-refused edit join this branch's
security-catalog-environment-overlay-refused. registry.ts regenerates to
the merged bytes unchanged.

Claude-Session: https://claude.ai/code/session_01KNKBCRDJCu5tGy3TEbvtrF
Co-authored-by: Claude <[email protected]>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: f59b85e44e3b01e6c909c08fe83e28d04e1172c0
Local-runs: none

Fresh review on the head that moved after the PASS record 6088873078 at 7f961c71a9: the hop 7f961c71a9..f59b85e44e is the merge of origin/main faf6348508 (4cbd442f11, through os-regen-merge.sh) and the regeneration commit f59b85e44e. Inputs, and nothing else: card #22371 (body and all 18 comments — rulings 6073500921 A′ and 6074838935 B, the engine seat's wording points 6073732548, the four dev reports, the seat's reviews and orders, the landing-step-A order 6088904280 and its report 6089930761), PR #22523 (body, all 3 comments including the FAIL record 6087652785 and the PASS record 6088873078, the 26-file list, and the net diff of refs/pm/pr-22523 against its merge-base with origin/main, re-derived as faf6348508 — the merge-base is origin/main itself, so the net diff is the branch's own content), and the 42 check-runs on the head. Of the seven required contexts, six conclude success (Lint & Repo Gates, TypeScript Type Check, Test Core, Dogfood Regression Gate, Build Core, Governed Surface Queue Guard) and one concludes failure (Temporal Conformance (live PG + MySQL), judged in ③-1: the job never checked out the repository); Check Changeset concludes failure on both runs on this head (the declared deliberate correction, judged in ①-12 and ②); Console Pin Gate is skipped (no pin moved). Governed surfaces in the file list: none. Size: +2025 / −76 = 2,101 changed lines across 26 files, under 3,000. Head repo is the base repo; the PR is a draft awaiting this record.

What the hop did to the diff, measured. The PR's own diff is byte-identical across the hop: git diff da989bbb24 7f961c71a9 and git diff faf6348508 f59b85e44e, with only the hunk-header line numbers normalised, compare IDENTICAL over all 26 files. The two changeset files carry the same blobs on both heads (370b80256f for the 22307 note, 4cdae48695 for the 22371 note), and main did not move the 22307 note in the hop (blob 627ed100d2 at both da989bbb24 and faf6348508). What the hop changed is the base under three files this PR also edits, read in their merged result at this head, not only in the PR's hunks:

  • packages/cli/src/commands/serve.ts — main's one hunk ([finding] cli(serve): a config boot of a multi-package composeStacks project serves none of its flat src/docs pages, and nothing warns #22405, placeCollectedDocs in the dev docs-collection block) sits at the merged file's docs region and reassigns config by spread, keeping plugins; the PR's eleven hunks (the core imports, stackBootPlugins, the --preset options, the two handles, isDev, resolveStackTiers, the plugins line, the auth gate) are all present and disjoint from it. The PR's stackBootPlugins(config, flags.dev) reads the same config.plugins the old config.plugins || [] read at the same point, after that block. No interaction.
  • packages/cli/test/normalized-call-sites.test.ts — main rewrote the why of the existing commands/serve.ts :: config.docs row; the PR adds the utils/schema-migrate.ts :: stack.requires row. Both rows are in the merged file, and the pin runs inside the green Test Core.
  • packages/spec/src/migrations/registry.ts — the merged step-18 semantic region carries this PR's security-catalog-environment-overlay-refused between screen-field-lookup-reference-required and send-template-input-org-retired, main's storage-scope-public-retired before strategy-context-aggregation-method-narrowed, and main's items[0] edit to flow-value-slot-template-dialect-refused — the id-sorted placement a regeneration produces (408 semantic ids), and check:migration-registry runs inside the green Lint & Repo Gates. spec-changes.json (+14) and docs/protocol-upgrade-guide.md (+3) differ from main by this PR's entry alone, so main's two records rode in through the merge and the regeneration added nothing but ours; check:spec-changes and check:upgrade-guide run inside the green Type Check · source gates.

The rest of main's hop near this PR's surfaces was read for reach: plugin-security/src/security-plugin.ts moved 347 lines (#22455) and none of them is a manifest registration, init(), defaultPermissionSets or appSecurityPluginOptions line, so the names the step holds behind the gate did not move; runtime/src moved action-execution.ts and domains/automation.ts, not standalone-stack.ts; metadata-protocol moved one test file only; nothing under packages/cli/src/commands/migrate/** or utils/security-catalog-overlays.ts moved. The integration pin runs in CI: the cli test script is vitest run over both projects (unit and integration), inside the green Test Core.

① Derived judgments

Every judgment of the PASS record re-read against the merged code at this head; each item names what is right or wrong.

  1. New public CLI command os migrate security-catalog-overlays (packages/cli/src/commands/migrate/security-catalog-overlays.ts, packages/cli/src/utils/security-catalog-overlays.ts): preview by default; --apply, --yes / -y, --force, --json, --database-url (env OS_DATABASE_URL), --preset (options Object.keys(STACK_TIER_PRESETS): minimal, default, full) and --dev. The SQLite occupancy gate is probed before boot and before the prompt; --apply without --yes refuses under --json or a non-TTY and prompts otherwise; exit 1 when the preview lists a row, 0 when none; under --apply 0 when every listed row is gone, 1 when one failed; 1 when a host config exists and cannot be loaded, refused before any row is read (describeUnloadableHostConfig). The preview boots read-only (deferSchemaDdl, readOnlyProbe), --apply boots plain. Right. The population is the four stored spellings (permission, permissions, position, positions) with organization_id null and state active and no package_id filter, met with the engine's own holder reading — ruling B's shape, every clause of A′ item 1 that B left standing included. The text report prints Composed as: os serve --preset NAME [--dev] and the gate's answer; the JSON carries serveFlags beside securityPlugin, and rows[] outcomes align with the lister's order because the deleter returns one outcome per input row in input order.
  2. The deletion path. A canonical row goes through protocol.deleteMetaItem (type, name, state: 'active', actor), repeated up to the group size and read back by id, so a name with two active canonical rows loses both and which row went is measured, not assumed; a refusal marks the rest of the group failed and the next name is still tried. A legacy-plural row goes through SysMetadataRepository.delete keyed by its stored type, name, package_id and checksum as parentVersion, intent: 'override-artifact', source and actor the step's name, with the same read-back. Right. The protocol door writes the sys_metadata_history tombstone and the ADR-0010 audit row; the repository door writes the tombstone under the stored spelling and no audit row, which is what the changeset says; a null stored checksum against a null parentVersion is accepted by the repository's lock. No path writes managed_by or package_id; nothing renames; the preview path never reaches the deleter. The pin asserts four tombstones and two audit rows, actor the step.
  3. @objectstack/objectql gains one export, findPackageHeldSecurityCatalogNames(registry), and the cold-boot check environmentHeldSecurityCatalogConflicts is now that list met with the bare slot. Right, and behaviour-preserving: the new private packageHeldSecurityCatalogNames takes its candidates from every composite slot's name (the part after the last colon) and every install claim of the two environment-holdable types, skips the empty name and the built-ins, asks the same securityCatalogPackageHolders predicate, and sorts type-then-name; the conflicts method keeps only the names the bare slot holds. Same set as the old bare-slot walk, one reading. The two cases added to protocol-boot-hydration-scoped.test.ts pin the list and pin the refusal's conflicts[] equal to that list met with the stored names. findEnvironmentHeldSecurityCatalogNames, declaredSecurityCatalogNames, BUILT_IN_SECURITY_CATALOG_NAMES and ENVIRONMENT_HELD_SECURITY_CATALOG_TYPES stay off the package entries: the ruling's "one new export" holds.
  4. The NAMESPACE_CONFLICT refusal text names the step for the environment's rows. Right. The envelope (code, status: 422, conflicts[] with catalogType, name, incomingPackageId, existingHolder) is untouched; the runtime string cites ADR-0048 and carries no tracker number.
  5. @objectstack/core gains stack-auth.ts, re-exported from the index: STACK_TIER_PRESETS, CAPABILITY_TO_TIER, resolveStackTiers, stackSuppliesAuthPlugin, isHostKernelComposition, DEV_AUTH_SECRET_FALLBACK, isDevelopmentBoot, resolveAuthSecret, resolvePlatformAuthComposition, and the types PlatformAuthSkipReason, PlatformAuthComposition. Right. The rule's order is serve's (stack-supplies-auth, auth-tier-off, host-kernel, no-secret, with an empty secret read as none); isDevelopmentBoot(devFlag) is devFlag === true || NODE_ENV === 'development', the truth table of serve's old line; resolveAuthSecret is the same OS_AUTH_SECRET / AUTH_SECRET / BETTER_AUTH_SECRET chain, silent, then the development fallback; the presets and the tier map are the literal tables serve carried. The unit test covers every clause, with NODE_ENV and the secrets saved and restored.
  6. serve.ts reads the rules and composes what it composed, re-read line by line in the merged file: TIER_PRESETS and CAPABILITY_TO_TIER are handles over the core declarations with the same members; the --preset options are the same three strings in the same order; isDev = isDevelopmentBoot(flags.dev); tiers = resolveStackTiers({ declaredTiers, requires, preset: presetName }) is the old precedence verbatim (presetName always defined there, the unknown-name fallback the same); plugins = stackBootPlugins(config, flags.dev) is the old config.plugins || [] plus devPlugins under the flag, the same array reference when nothing merges; the gate line if (!hasAuthPlugin && tierEnabled('auth')) stays with hasAuthPlugin from the rule's predicate, and inside it the host-kernel branch, then the no-secret branch, read authComposition — the first two skip reasons cannot occur inside the block because the outer if asks the same predicate and the same tier set — so the three warnings and the construction are unchanged, and secret is the rule's. Right. main's [finding] cli(serve): a config boot of a multi-package composeStacks project serves none of its flat src/docs pages, and nothing warns #22405 hunk is elsewhere in the file and reads none of this.
  7. The step's composition (schema-migration-plugins.ts): authGatedSecurity is an opt-in only the step passes; the early return for a config-less, artifact-less project is kept for every other caller; composeAuthGatedSecurity feeds the gate the base plugins plus the host plugins (by stackBootPlugins under --dev), the config's declared tiers, the requires serve reads (the artifact's as the standalone stack surfaced them for a non-host config, else the config's), the caller's --preset through resolveStackTiers, and resolveAuthSecret({ isDev: isDevelopmentBoot(dev) }); a host config that exists and did not load answers config-unloadable; the security plugin is wrapped by composeForDeclarations (its start() suppressed, so no sys_permission_set seeding and no projector run) and spliced at index 1 behind the write guard, ahead of the host plugins where serve registers it; the composition note counts the devPlugins. Right. Pinned by the unit case (--dev composes the config's devPlugins, and a dev-only com.objectstack.auth flips the gate to stack-supplies-auth) and the integration file's four-posture equality (item 11).
  8. @objectstack/runtime: createStandaloneStack accepts hydrateMetadataFromDb (Zod-declared, optional; cfg.hydrateMetadataFromDb !== false, so only false changes what the plugin receives and a serving boot gets the options it always did). Right, pinned with a control over the same file. bootSchemaStack gains hydrateMetadata, composeAuthGatedSecurity and serveFlags (CLI-internal, default off); serveFlags reaches only the composition and the standalone stack gets no dev key. Right.
  9. packages/spec: the D3 semantic entry security-catalog-environment-overlay-refused (entries/semantic/18.*.ts, its generated region in registry.ts), with spec-changes.json and docs/protocol-upgrade-guide.md regenerated over the merged registry at this head. Right in shape (surface carries no backticks; id, replacement, reason, acceptanceCriteria — the structured-TODO form ADR-0087 D3 names for a change no conversion can express) and in substance: the replacement prescribes the step before the first boot on this major with the flags and environment the deployment boots with, then --apply through the write path, or a rename in the package, nothing adopted; the reason's "only where the boot composes it behind the auth gate (an auth secret set, or a development boot)" matches resolvePlatformAuthComposition; the "Done when" is the preview's exit 0 with the deployment's boot flags and environment, and the next boot coming up. The three projections follow the entry byte-for-byte, and the three generated-artifact checks are green on the head (above).
  10. content/docs/deployment/cli.mdx, two rows. The "Data migrations" row: preview, --apply with one tombstone per row, run before the first v18 boot with the flags and environment the deployment boots with, the auth-tier condition, serve's --preset and --dev with serve's meaning, adopts nothing, exits 1 while it lists a row; the occupancy row: --apply refuses an occupied database. Right against items 1 and 7.
  11. Tests and the call-sites ledger. The integration pin runs four preview postures — auth off (no-secret), auth on (composed), --preset minimal with a secret (auth-tier-off, member_default not listed), --dev with no secret (composed, four rows) — each asserting serveFlags, the list equal to the cold boot's conflicts[] of the same composition with the same flags, exit 1 and no write; two --apply cases (four tombstones, two audit rows, the controls kept, the cold boot then up and a second preview at 0; auth off leaves member_default alone). stack-collections.test.ts (three stackBootPlugins cases), schema-migration-plugins.test.ts (the --dev case), stack-auth.test.ts, the one-shot-family registration (a preview mode and an --apply mode), the two objectql cases, the runtime hydrate-off case, and the one top-level row in normalized-call-sites.test.ts for schema-migrate.ts :: stack.requires (a classification with its reason, not a baseline widening). Right, and all of it ran inside the green Test Core on this head.
  12. .changeset/22307-cold-boot-catalog-refusal.md — the declared deliberate correction, written by PR feat(objectql)!: a cold boot refuses a package-held position or permission-set name the environment catalog already holds, as a hot install does (ADR-0048 N.3) #22365 and unreleased (pre.json at this head is mode: pre, tag: next, with no consumed list naming it). The file at this head is the blob the PASS record judged, and nothing it describes moved on main in the hop (above), so each changed sentence is re-confirmed on content, not carried:
    • The marker, not-required (no-migration-prescription) … to registered security-catalog-environment-overlay-refused. Right. Ruling A′ item 1 orders the flip "to a migration prescription naming this step"; the closed vocabulary spells a prescription only as registered with an id that resolves and is new in the diff — this one is both (item 9). Check Changeset's step "Require an ADR-0087 disposition on a declared-breaking changeset" concludes success on this head.
    • The upgrade shape's appended sentence: run the step before the first v18 boot, with the flags and environment the deployment boots with; it lists exactly these rows and with --apply deletes them. Right: "exactly these rows" is the four-posture equality (item 11); "flags and environment" is the qualifier the gate needs, since --preset minimal turns the auth tier off and --dev turns the development fallback on.
    • The one-line fix: list, then --apply, or rename the item in the package. Right: preview-then-apply is the command's only shape; the step adopts and renames nothing.
    • "After upgrading, for any refused name." The same step; it needs no server (a one-shot kernel with hydration off, no HTTP; the pin runs over a database the cold boot refuses); it covers permission sets and positions, a name the platform security plugin declares, and a row stored under a legacy plural (both types, both spellings, and the plugin's names whenever the same flags and environment compose it). Right.
    • "What the step deletes." The population sentence is the lister's where; the SQL sentence, with its name placeholder, is a statement of the net effect for one name (every active environment-wide row under both spellings; the step executes no SQL); "through the metadata write path, so each deleted row leaves a sys_metadata_history tombstone" holds on both doors (item 2); the draft/organization sentence is unchanged from the base and still true of the hydration filter. Right.
    • The plural-row paragraph's last sentence, "or by os migrate security-catalog-overlays --apply, for any name". Right: the repository door reaches a plural row (pinned on positions/m22371_old_lead and permissions/member_default).
    • The "one os command" paragraph. The step is the one os command that deletes these rows with no server running; os meta delete and os data delete call a running server; nothing renames or removes either item automatically, the step deletes only under --apply and adopts nothing. Right: under packages/cli/src the only non-test callers of deleteMetaItem or SysMetadataRepository are the step's own utils, and the preview path never reaches a delete.
    • The engine seat's three wording points (6073732548) are applied, and the "Before upgrading" bullet is byte-identical to the base.
      Every rewritten sentence is right at this head. The Check Changeset red is the DELIBERATE CORRECTION class the gate names (its log on this head: the file "exists on the merge base and was not added by this PR", with the gate's own remedy "do NOT restore it — say so on the PR and get it confirmed"; the empty-frontmatter scan itself passed, 2 declaring changesets added); this record is that confirmation, on content, at this head.
  13. The merge and the regeneration (4cbd442f11, f59b85e44e): the merge is its own commit and the regeneration is the next one; both sides of the three both-sides files survive (measured above, not taken from the report); the PR's own diff did not move. Right.

② Semver level

  • The PR's Clause-②: yes (widening) line — right: a new public CLI command with its flags, one new objectql export, eleven core exports, a new runtime config key and a new spec ledger entry; nothing narrowed or renamed; no migration is owed by the widening itself. Check Changeset's "Guard against accidental major bumps" step concludes success on this head, reading that line.
  • .changeset/22371-security-catalog-overlays-step.md, each package this diff publishes in, against its own entry — the five packages whose src or shipped files move are exactly the five named:
    • @objectstack/cli: minor — right (the command and its flags; bootSchemaStack's options and stackBootPlugins are internal).
    • @objectstack/objectql: minor — right; findPackageHeldSecurityCatalogNames named, the refusal-text change named, the envelope stated unchanged.
    • @objectstack/core: minor — right; all eleven stack-auth exports named; the handles stated.
    • @objectstack/runtime: minor — right; createStandaloneStack's hydrateMetadataFromDb: false named, the default stated.
    • @objectstack/spec: minor — right; the package publishes the new D3 entry through ./migrations and ships spec-changes.json in its tarball, and the bullet names the entry, its registry and spec-changes.json records and its protocol-18 projection in the upgrade guide. minor is the floor Clause-②: yes takes and matches the four siblings.
    • The sentence on --preset / --dev (read through the rules serve reads them by; run the step with the flags and environment the deployment boots with) — right (①-6, ①-7).
  • No other published package is in the diff: content/docs and docs/ are repository documentation, not a tarball, and the hop's twelve main changesets are not this PR's.
  • The .changeset/22307-cold-boot-catalog-refusal.md correction still stands on content at this head (①-12): its blob is the one the PASS record judged, main did not move the note or the code it describes in the hop, the note stays unreleased, and its marker registered security-catalog-environment-overlay-refused resolves to the entry this diff adds. Its own Clause-②: no and major frontmatter are PR feat(objectql)!: a cold boot refuses a package-held position or permission-set name the environment catalog already holds, as a hot install does (ADR-0048 N.3) #22365's declaration about its own change and are not re-judged here. The Check Changeset red on this head is therefore the confirmed DELIBERATE CORRECTION, not a collision and not a missing changeset.

③ Boundary flags

  1. Temporal Conformance (live PG + MySQL) concludes failure on this head. Read from its job log: step 2 "Initialize containers" failed — docker pull postgres:16 answered toomanyrequests: You have reached your unauthenticated pull rate limit three times with back-off, and the service container never started; "Checkout repository" and every build and test step after it are skipped. No byte of this repository was read by that job, so the failure does not reach this PR's change: it is an infrastructure refusal at the registry, the same job's steps never ran, and the driver-sql, temporal-backend, metadata-protocol and runtime suites it would have run are untouched by this diff's reach (the hop's main-side changes to those packages were read in the measurement above). It is a REQUIRED context (check-expected-skips.mjs names it so, and notes the queue build is where its verdict is taken), so the owning seat owes a re-run on this head before enqueue; this record does not re-run it, by its own read-only shape. Escalated to the seat as a landing step, not a content finding; no card owed.
  2. Landing step A's deviation: the step-3 commit and the regeneration are one commit (f59b85e44e). The order asked for the regeneration as its own commit after the merge; the merge is 4cbd442f11 and the regeneration is the next commit, which is that shape — the os-regen pre-commit hook refuses an ordinary commit while the two deferred artifacts are stale, as the script's header records, so a separate "step 3" commit was never available. Accepted as reported; consistent with the diff (the regeneration commit changes exactly spec-changes.json and the upgrade guide, +20 / −3, main's records only).
  3. Landing step A's deviation: two stray files (/gates.txt, /gates.err) at the filesystem root of the dev's container, written by a mis-scoped background command; removal refused by the harness. Outside the repository and outside this diff; the shared checkout was reported clean. Accepted as reported; a hygiene note for a person, no carrier owed.
  4. Lock use and the re-created worktree (landing step A) — process facts, consistent with the report; the whole-workspace build and the ordered suites are declared green there and the head's check-runs agree where they overlap.
  5. The earlier record's ③-2 escalation (no --preset / --dev) — discharged in-PR in patch round 2 and judged right in ①-1, ①-6, ①-7 and ①-11; the divergent posture it named (--preset minimal with a secret set) is the pinned auth-tier-off case. Closed; no card owed.
  6. Dev boundary, reported and not a stop: the flags move the composition only — NODE_ENV defaulting, the .env cascade and the standalone stack's dev key are not taken. Accepted as reported. A secret that lives only in a .env file is unseen by the step (bootSchemaStack loads none), so the gate answers no-secret, the plugin's row is not listed and the next os serve refuses on that one name: under-listing, never a deletion the deployment's own boot would not have refused; the preview prints the gate's answer before any --apply; every operator-facing surface says to run with the flags and environment the deployment boots with. No card owed.
  7. Dev deviation: serve.ts changed in three lines, behaviour unchanged — right (①-6). Dev finding, not filed: serve.ts keeps a second spelling of isDev for port auto-shift (portAutoShiftAllowed) — accepted as carried: it decides port policy, not composition, and no reader of it decides a held name; a drift-class note outside this card's surface.
  8. Dev deviation: the wording reached the D3 entry's replacement / acceptanceCriteria and one word-group of the 22307 note, beyond the round-2 order — right, judged in ①-9 and ①-12: the four operator-facing surfaces give one instruction.
  9. The NAMESPACE_CONFLICT message rewrite, the normalized-call-sites row, serve.ts keeping its literal gate line (round-1 flags) — right (①-4, ①-11, ①-6).
  10. AuthPlugin, the organizations plugin and the audit plugin stay serve-only; @objectstack/verify's harness composes auth and security ungated — right: none of the three registers a catalog name (security-plugin.ts is the only manifest registration among them, and main's hop did not add one), and the harness is verify: the in-process handle boots a leaner stack than serve and has no door for eight things an app's tests need (requires[] capabilities, system/predicate update, the form door, user-less triggers, …), measured by hotcrm#2013 #22301's open territory.
  11. Premise 5 (history and audit rows from a kernel that hydrated nothing) — closed: the integration pin's trail assertions hold on this head inside the green Test Core, independently of the dev's scratch measurement.
  12. A canonical row under a package item declaring _lock full or no-delete (carried, not filed) — deleteMetaItem's lock gate answers ITEM_LOCKED, the step reports failed and exits 1, the SQL stays the statement of what to delete. No shipped package declares such a lock on a permission set or position. Accepted as carried; no card owed until a package does.
  13. Round-1 open_questions: keep the D3 registration (option A) — answered right, as the seat ruled (①-12). Ruling B's confidence gaps — PR feat(core,cli,verify): bootStack composes what serve composes — item 1 stage 2 of #22301 (HELD at stop conditions) #22381 landed (97610a533); materializeStackPlugin did not cover the auth gate and the extraction is done (①-5); history and audit pinned (item 11). Closed. The sibling-not-mode decision is in the PR body with three reasons (operation, boot, exit contract), consistent with the code. The landing-step-A report's open_questions and out_of_scope_findings are empty; the cross-lane declarations (packages/cli and packages/runtime — domain:cli; packages/objectql and packages/core — domain:engine; content/docs — domain:devx) are the seat's.

Implemented-by: claude/issue-22371-overlay-cleanup-step
Reviewed-by: session_01KNKBCRDJCu5tGy3TEbvtrF

VERDICT: PASS

The PR's own diff is byte-identical to the one the PASS record at 7f961c71a9 judged, and the three files that moved on both sides are merged whole, read in full at this head. Every ① judgment holds on the merged code; every package this diff publishes in is named at minor under Clause-②: yes (widening), and the 22307 note's correction stands on content, so the Check Changeset red is the confirmed DELIBERATE CORRECTION. The Temporal Conformance red is a registry rate-limit at container start that read nothing of this repository and does not reach this change; its re-run on this head is the owning seat's step before the queue.


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Regen-provenance: 6090076525 · f59b85e44e → 9b0c250a70 · unexplainedPathsBetween(from, to, base: origin/main) → (empty)

domain:spec seat 3 (#18883) · zhuangjianguo · session session_01KNKBCRDJCu5tGy3TEbvtrF · 2026-10-09T22:35Z.

The head moved after the contract review PASS 6090076525 by one merge of origin/main ce78ff7bcd (PR #22545, the Temporal Conformance image mirror), and nothing else:

  • 9b0c250a70 (parents f59b85e44e, ce78ff7bcd);
  • git diff --name-only f59b85e44e 9b0c250a70 lists .github/workflows/ci.yml alone, a path this PR touches at neither head;
  • scripts/pm/record-recognisers.mjs's unexplainedPathsBetween, run on the committed trees with base origin/main, returns no path;
  • git diff origin/main 9b0c250a70 --stat is still the PR's 26 files, +2025 / −76.

The record carries to 9b0c250a70. The merge exists so the required Temporal Conformance runs on the mirror; a re-run would have reused the old merge ref.


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Landing pre-checks at 9b0c250a70, by the owning seat: queued with Check Changeset red by design

domain:spec seat 3 (#18883) · zhuangjianguo · session session_01KNKBCRDJCu5tGy3TEbvtrF · 2026-10-09T23:03Z.

  • The review: the contract review PASS 6090076525 at f59b85e44e carries to this head through Regen-provenance 6090398442. The hop is one merge of origin/main, and unexplainedPathsBetween returns no path.
  • CI: 32 success, 2 skipped and 1 failure. All seven required contexts are green, Temporal Conformance included, on the mirror. check-expected-skips --pr 22523 exits 0.
  • Governed: check-governed-merges.mjs --pr 22523 reads 0 of 26 paths on the register, NOT governed. The size is 2,101 changed lines, under 3,000.
  • Closing keywords: the body carries Fixes #22371 alone.
  • main drift: origin/main is now f782f17644 (PR fix(storage): a refused attach tombstones the uploader's never-attached file, outside the refused write's unit of work #22542, four storage paths). It touches no path of this PR and no os-regen artifact.

The one red check, Check Changeset, is red by design. All three conditions hold:

  1. Its source says so on the pushed branch. The refusal it prints for an edited, unreleased note reads "write the confirmation on the PR, naming the note and what changed under it, and leave this check red ([finding] the skip-changeset label suppresses the DELIBERATE-CORRECTION refusal whose own text says 「no label and no diff shape makes that safe」 — declared contract against enforced behaviour #18375)". The edited note is .changeset/22307-cold-boot-catalog-refusal.md.
  2. It does not run on merge_group. .github/workflows/pr-automation.yml triggers on pull_request only, and the job is not a required context.
  3. The PR records the gate and the reason:
    • the body's Gates section;
    • the contract review records 6088873078 and 6090076525, which confirm the correction on content, sentence by sentence;
    • this comment.

pr_ready and automerge_enable follow.


Generated by Claude Code

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 size/xl tests tooling

Projects

None yet

2 participants