diff --git a/openspec/changes/lifecycle-model-core-internal-l1/.openspec.yaml b/openspec/changes/lifecycle-model-core-internal-l1/.openspec.yaml deleted file mode 100644 index 1a62d62b..00000000 --- a/openspec/changes/lifecycle-model-core-internal-l1/.openspec.yaml +++ /dev/null @@ -1,2 +0,0 @@ -schema: spec-driven -created: 2026-09-06 diff --git a/openspec/changes/lifecycle-model-core-internal-l1/design.md b/openspec/changes/lifecycle-model-core-internal-l1/design.md deleted file mode 100644 index 34bdf6bd..00000000 --- a/openspec/changes/lifecycle-model-core-internal-l1/design.md +++ /dev/null @@ -1,93 +0,0 @@ -## Context - -After P0–P8, in-repo Core owns install/ensure/update/uninstall/exec/doctor/upgrade engines, but Core still imports shared receipt types from `src/lifecycle/model`. P8 (#717) documented KEEP for that file: `src/state` and `package-manager` import `LifecycleReceipt` from it, and “moving into `src/core` would invert `state → Core`.” - -That warning is correct for a **naive** move (state importing `src/core/index.ts`, executors, or any Core module that already imports state). It is not a reason to leave types in `src/lifecycle` forever. L1 needs a placement that: - -- removes `Core → src/lifecycle/model` -- lets `state` and remaining lifecycle modules share the same types -- does not create `Core → state → Core` cycles -- does not publish new `quantex-core` exports - -Current graph (simplified): - -```text -src/core/* ──imports──► src/lifecycle/model ◄──imports── src/state -src/core/* ──imports──► src/state -src/lifecycle/* ──imports──► src/lifecycle/model -``` - -`src/lifecycle/model.ts` is already a zero-import leaf. The inversion risk is ownership placement, not the type module’s own dependencies. - -## Goals / Non-Goals - -**Goals:** - -- Own receipt/observation types and `LIFECYCLE_RECEIPT_SCHEMA_VERSION` in a Core-internal leaf. -- Update every `lifecycle/model` import (production and tests). -- Preserve frozen contracts: state v2, receipt JSON shape, `--json`, aliases, exit codes, no `engine`/`route` in JSON. -- Keep published SDK export surface identical (`createQuantex` + existing inspection/mutation types only). -- Lock the L1 graph so later knives cannot smuggle Core runtime into `state`. - -**Non-Goals:** - -- L2+: provider-binding/evidence, observation, update-planner, agent-execution, uninstall-postcondition, deleting `src/lifecycle/`. -- Expanding commands or `packages/core` / `src/core/index.ts`. -- Folding config, capabilities, commands, or schema registries. -- YAML, `release-core.yml`, protect-main, or shelved OpenSpec changes. - -## Decisions - -### 1. Core-internal leaf, not Core runtime and not a third package - -Place the module at `src/core/lifecycle/model.ts`. - -- The file remains a **leaf**: no imports from Core runtime, `state`, `lifecycle`, providers, or CLI. -- `src/core/index.ts` and `packages/core/src/index.ts` MUST NOT re-export it. -- Remaining `src/lifecycle/index.ts` MAY re-export types/constants as an existing non-SDK path for CLI/services that already imported the barrel. -- `src/state/schema.ts` MAY re-export `LIFECYCLE_RECEIPT_SCHEMA_VERSION` from the leaf so state tests keep their current import site while the constant has one source of truth. - -This is the smallest move that makes Core the owner without creating a new workspace package (ADR 0007 already rejected splitting Core into extra packages). - -### 2. `state` may import the leaf, never Core runtime - -P8’s inversion is real if `state` imported Core **runtime** (`./client`, executors, `src/core/index.ts`). Core already imports `src/state` at runtime (`installation-production`, `uninstall-executor`, `update-production`). A `state → core/index` edge would cycle. - -A `state → src/core/lifecycle/model` edge does **not** cycle: the leaf has no reverse imports. Type-only imports are erased at emit; the receipt-schema constant is a primitive value and is still cycle-free. - -Ownership tests MUST lock: - -- `src/state/**` import specifiers matching `../core` are only `../core/lifecycle/model`. -- `src/core/index.ts` / `packages/core` do not mention `LifecycleReceipt`, `LIFECYCLE_RECEIPT_SCHEMA_VERSION`, or `./lifecycle`. -- `src/lifecycle/model.ts` is absent. -- Remaining P8 KEEP modules still live under `src/lifecycle/`. - -### 3. No compatibility shim at `src/lifecycle/model.ts` - -L1 is a move, not a re-export leftover. Every `lifecycle/model` import is rewritten. A shim would keep the reverse path Core was trying to delete and would look like a P8 leftover. - -The lifecycle **barrel** stays. It is not a model shim; it is the existing non-SDK facade for remaining L2+ modules plus type re-exports. - -### 4. Changelog is internal - -No user-visible command, JSON, or SDK change. Release framing is `refactor:` so release-please files it under Internal Improvements and does not bump from this knife alone. - -## Risks / Trade-offs - -- [Risk] Later L2 moves a runtime lifecycle module into Core and `state` starts importing that module too → cycle with Core’s existing `state` imports. → Mitigation: ownership test allows only the model leaf from `src/state`; L2+ must keep `state` off Core runtime. -- [Risk] Someone re-exports the leaf from `packages/core` “for convenience.” → Mitigation: architecture/ownership tests on `src/core/index.ts` and `packages/core/src/index.ts`; compatibility-contract forbids SDK growth from this knife. -- [Risk] Receipt constant drifts between state and Core. → Mitigation: one export in the leaf; state re-exports it. -- [Trade-off] `src/core/lifecycle/` as a directory looks like an invitation to dump L2 engines there immediately. L1 only places `model.ts`; remaining modules stay under `src/lifecycle/` until their own knives. - -## Migration Plan - -1. Add `src/core/lifecycle/model.ts` with the current types/constant (`as const` on the schema version so state keeps a literal `1`). -2. Retarget importers; delete `src/lifecycle/model.ts`. -3. Update ownership tests; run lint/format/typecheck/test/openspec/memory. -4. Draft PR; do not archive this OpenSpec change until the implementation PR merges and spec deltas are synced. - -Rollback: revert the PR. No persisted-state migration. - -## Open Questions - -None for L1. L2+ placement of provider-binding vs observation remains a later design; this change MUST NOT pre-move those modules. diff --git a/openspec/changes/lifecycle-model-core-internal-l1/proposal.md b/openspec/changes/lifecycle-model-core-internal-l1/proposal.md deleted file mode 100644 index 34bed169..00000000 --- a/openspec/changes/lifecycle-model-core-internal-l1/proposal.md +++ /dev/null @@ -1,41 +0,0 @@ -## Why - -P8 (#717) kept `src/lifecycle/model` outside Core because a naive fold would make `src/state` depend on Core runtime and invert the existing `Core → state` edge. That leftover is now the first reverse-dependency knife: Core still imports receipt types from `src/lifecycle`, so in-repo Core is not yet the owner of its own lifecycle evidence types. L1 moves only that leaf into a Core-internal path without expanding the published SDK. - -Work-intake classification: architecture boundary plus project-memory (OpenSpec/ADR). Observable CLI behavior, state v2, `--json`, aliases, and exit codes MUST stay frozen. OpenSpec is required before edits. - -## What Changes - -- Move `src/lifecycle/model` (receipt/observation types and `LIFECYCLE_RECEIPT_SCHEMA_VERSION`) to a Core-internal leaf at `src/core/lifecycle/model.ts`. -- Retarget every `../lifecycle/model` / `src/lifecycle/model` / `./model` import, including Core installation/uninstall modules, `src/state/*`, `src/package-manager`, remaining `src/lifecycle/*` modules, and tests. -- Keep remaining `src/lifecycle/*` modules in place for L2+ (provider-binding/evidence, observation, update-planner, agent-execution, uninstall-postcondition, barrel). -- Keep published `src/core/index.ts` / `packages/core` free of new lifecycle exports. Existing non-SDK paths (the `src/lifecycle` barrel, `src/state/schema`) MAY re-export types/constants already required by CLI/state. -- Lock the L1 import graph in ownership tests: state may import only the Core-internal leaf, never Core runtime. -- Record the leaf-vs-runtime distinction as an ADR so later knives do not recreate the P8 inversion. - -**Not changing** (deliberately): - -- No L2+ moves or deletion of `src/lifecycle/`. -- No new CLI commands, no SDK method/type surface growth, no fold of config/capabilities/commands/schema. -- No YAML / `release-core.yml` / protect-main changes. -- Shelved OpenSpec changes stay untouched: `release-one-line-delivery`, `release-pr-skip-human-heuristics`, `windows-ci-advisory-merge-gate`. -- State v2 receipt JSON shape, aliases, exit codes, and JSON omission of `engine` / `route` stay frozen. - -## Capabilities - -### New Capabilities - -None. - -### Modified Capabilities - -- `runtime-boundaries`: Core owns the internal lifecycle receipt/observation type leaf; remaining `src/lifecycle` modules stay outside Core for later knives; published SDK root MUST NOT export that leaf. -- `compatibility-contract`: internalizing the lifecycle model MUST NOT expand `quantex-core` exports or drift frozen v1 JSON, aliases, exit codes, or state v2. - -## Impact - -- `src/core/lifecycle/model.ts` (new leaf), `src/lifecycle/model.ts` (removed) -- Importers: `src/core/installation-*`, `src/core/uninstall-executor`, `src/state/*`, `src/package-manager`, remaining `src/lifecycle/*`, tests -- `test/core/lifecycle-core-ownership.test.ts` (L1 import-graph lock) -- `docs/adr/0011-core-internal-lifecycle-model-leaf.md` -- Changelog framing is **internal** (`refactor:` / Internal Improvements); no user-facing CLI or SDK change diff --git a/openspec/changes/lifecycle-model-core-internal-l1/specs/compatibility-contract/spec.md b/openspec/changes/lifecycle-model-core-internal-l1/specs/compatibility-contract/spec.md deleted file mode 100644 index e104509e..00000000 --- a/openspec/changes/lifecycle-model-core-internal-l1/specs/compatibility-contract/spec.md +++ /dev/null @@ -1,24 +0,0 @@ -## ADDED Requirements - -### Requirement: Internalizing lifecycle model types MUST NOT drift frozen v1 contracts - -Moving lifecycle receipt and observation TypeScript types into a Core-internal module MUST NOT change persisted state schema version 2, the lifecycle receipt JSON shape, maintained command aliases, exit-code classes, or v1 `--json` / NDJSON envelopes. Structured output MUST still omit engine and route identifiers. The published `quantex-core` root MUST NOT gain lifecycle-model exports as part of this internalization. - -#### Scenario: State v2 and receipt JSON stay byte-compatible - -- **GIVEN** a valid schema version 2 `state.json` with lifecycle receipts -- **WHEN** Quantex loads or records receipts after the model move -- **THEN** the schema version remains `2` -- **AND THEN** receipt field names, requiredness, and meanings are unchanged - -#### Scenario: Structured CLI output omits engine and route - -- **WHEN** a maintained lifecycle command emits JSON or NDJSON after the model move -- **THEN** the payload does not include selected engine or route identifiers -- **AND THEN** aliases and exit-code classes remain the pre-move v1 contract - -#### Scenario: Published SDK export list is unchanged - -- **WHEN** a TypeScript consumer imports `quantex-core` or `src/core/index.ts` -- **THEN** the runtime export remains `createQuantex` -- **AND THEN** lifecycle receipt types are not added to that public surface diff --git a/openspec/changes/lifecycle-model-core-internal-l1/specs/runtime-boundaries/spec.md b/openspec/changes/lifecycle-model-core-internal-l1/specs/runtime-boundaries/spec.md deleted file mode 100644 index 34c473e0..00000000 --- a/openspec/changes/lifecycle-model-core-internal-l1/specs/runtime-boundaries/spec.md +++ /dev/null @@ -1,39 +0,0 @@ -## ADDED Requirements - -### Requirement: Lifecycle receipt types SHALL live in a Core-internal leaf - -Quantex SHALL own agent lifecycle receipt, observation, plan, and outcome TypeScript types plus `LIFECYCLE_RECEIPT_SCHEMA_VERSION` in an in-repo Core-internal module under `src/core/`. That module MUST be a dependency leaf: it MUST NOT import Core runtime, `src/state`, remaining `src/lifecycle` engines, CLI, or providers. `src/state` MAY import that leaf (including a value re-export of the receipt schema constant) and MUST NOT import Core runtime modules such as `src/core/index.ts`, `createQuantex`, or mutation/execution executors. Relocating the types MUST NOT, by itself, expand the published `quantex-core` package export surface. - -#### Scenario: Core no longer imports lifecycle model from src/lifecycle - -- **WHEN** in-repo Core installation, uninstall, or update modules need `LifecycleReceipt` or `LIFECYCLE_RECEIPT_SCHEMA_VERSION` -- **THEN** they import those symbols from the Core-internal leaf -- **AND THEN** they do not import `src/lifecycle/model` - -#### Scenario: State shares the leaf without depending on Core runtime - -- **WHEN** persisted-state schema or store modules type lifecycle receipts -- **THEN** they import from the Core-internal leaf -- **AND THEN** they do not import `src/core/index.ts`, `createQuantex`, or Core executors - -#### Scenario: Published SDK root stays free of the leaf - -- **WHEN** a TypeScript consumer inspects `src/core/index.ts` or `packages/core/src/index.ts` -- **THEN** those entries do not export `LifecycleReceipt`, `LifecycleObservation`, `LIFECYCLE_RECEIPT_SCHEMA_VERSION`, or other lifecycle-model symbols -- **AND THEN** `createQuantex` remains the only runtime export - -### Requirement: Remaining src/lifecycle engines SHALL stay outside Core until a later knife - -Quantex MUST keep provider-binding, provider-evidence, agent-observation, update-planner, agent-execution, uninstall-postcondition, and the `src/lifecycle` barrel outside `src/core` for the L1 model move. L1 MUST NOT delete `src/lifecycle/` and MUST NOT fold those remaining modules into Core. - -#### Scenario: L1 does not absorb remaining lifecycle engines - -- **WHEN** the L1 model internalization is applied -- **THEN** `src/lifecycle/` still contains the remaining engine modules listed above -- **AND THEN** `src/lifecycle/model.ts` is absent - -#### Scenario: Existing non-SDK barrel may re-export leaf types - -- **WHEN** CLI services or Core modules already import lifecycle types from the `src/lifecycle` barrel -- **THEN** that barrel MAY re-export types and the receipt schema constant from the Core-internal leaf -- **AND THEN** that re-export is not a published `quantex-core` SDK export diff --git a/openspec/changes/lifecycle-model-core-internal-l1/tasks.md b/openspec/changes/lifecycle-model-core-internal-l1/tasks.md deleted file mode 100644 index 44dbd9cc..00000000 --- a/openspec/changes/lifecycle-model-core-internal-l1/tasks.md +++ /dev/null @@ -1,25 +0,0 @@ -## 1. Core-internal leaf - -- [x] 1.1 Add `src/core/lifecycle/model.ts` with the current receipt/observation types and `LIFECYCLE_RECEIPT_SCHEMA_VERSION` (`as const`), as a zero-import leaf -- [x] 1.2 Point remaining `src/lifecycle/*` modules and the barrel at the Core-internal leaf -- [x] 1.3 Delete `src/lifecycle/model.ts` (no compatibility shim) - -## 2. Importer retarget - -- [x] 2.1 Update Core `installation-*` / `uninstall-executor` / other `../lifecycle/model` imports to `./lifecycle/model` -- [x] 2.2 Update `src/state/{schema,store,index}` to import the leaf only; re-export the receipt schema constant from the leaf -- [x] 2.3 Update `src/package-manager` and tests that imported `src/lifecycle/model` -- [x] 2.4 Split Core barrel imports so model types/constants come from the leaf while L2+ engines stay on `src/lifecycle` - -## 3. Ownership lock and memory - -- [x] 3.1 Update `test/core/lifecycle-core-ownership.test.ts` for the L1 graph (leaf present, `src/lifecycle/model.ts` absent, state leaf-only, SDK non-export) -- [x] 3.2 Add ADR 0011 recording the leaf-vs-runtime distinction -- [x] 3.3 Add a thin AGENTS.md pointer to `src/core/lifecycle/` and `runtime-boundaries` - -## 4. Validation and delivery - -- [x] 4.1 Run `bun run lint`, `bun run format:check`, `bun run typecheck` -- [x] 4.2 Run `bun run test` -- [x] 4.3 Run `bun run openspec:validate` and `bun run memory:check` -- [x] 4.4 Commit, push, and open a **draft** PR with before/after import map, SDK non-export checklist, freeze checklist, and internal changelog framing diff --git a/openspec/specs/compatibility-contract/spec.md b/openspec/specs/compatibility-contract/spec.md index 99bf123c..597fd71a 100644 --- a/openspec/specs/compatibility-contract/spec.md +++ b/openspec/specs/compatibility-contract/spec.md @@ -489,3 +489,26 @@ by itself, expand the published `quantex-core` method surface, change - **AND THEN** no new SDK `upgrade` method appears solely because CLI `upgrade` executes through in-repo Core +### Requirement: Internalizing lifecycle model types MUST NOT drift frozen v1 contracts + +Moving lifecycle receipt and observation TypeScript types into a Core-internal module MUST NOT change persisted state schema version 2, the lifecycle receipt JSON shape, maintained command aliases, exit-code classes, or v1 `--json` / NDJSON envelopes. Structured output MUST still omit engine and route identifiers. The published `quantex-core` root MUST NOT gain lifecycle-model exports as part of this internalization. + +#### Scenario: State v2 and receipt JSON stay byte-compatible + +- **GIVEN** a valid schema version 2 `state.json` with lifecycle receipts +- **WHEN** Quantex loads or records receipts after the model move +- **THEN** the schema version remains `2` +- **AND THEN** receipt field names, requiredness, and meanings are unchanged + +#### Scenario: Structured CLI output omits engine and route + +- **WHEN** a maintained lifecycle command emits JSON or NDJSON after the model move +- **THEN** the payload does not include selected engine or route identifiers +- **AND THEN** aliases and exit-code classes remain the pre-move v1 contract + +#### Scenario: Published SDK export list is unchanged + +- **WHEN** a TypeScript consumer imports `quantex-core` or `src/core/index.ts` +- **THEN** the runtime export remains `createQuantex` +- **AND THEN** lifecycle receipt types are not added to that public surface + diff --git a/openspec/specs/runtime-boundaries/spec.md b/openspec/specs/runtime-boundaries/spec.md index 63d07aa2..5ed6cce8 100644 --- a/openspec/specs/runtime-boundaries/spec.md +++ b/openspec/specs/runtime-boundaries/spec.md @@ -291,3 +291,41 @@ the agent-lifecycle update engine. JSON MUST still omit engine and route. identifiers - **AND THEN** engine or route diagnostics remain absent from those payloads +### Requirement: Lifecycle receipt types SHALL live in a Core-internal leaf + +Quantex SHALL own agent lifecycle receipt, observation, plan, and outcome TypeScript types plus `LIFECYCLE_RECEIPT_SCHEMA_VERSION` in an in-repo Core-internal module under `src/core/`. That module MUST be a dependency leaf: it MUST NOT import Core runtime, `src/state`, remaining `src/lifecycle` engines, CLI, or providers. `src/state` MAY import that leaf (including a value re-export of the receipt schema constant) and MUST NOT import Core runtime modules such as `src/core/index.ts`, `createQuantex`, or mutation/execution executors. Relocating the types MUST NOT, by itself, expand the published `quantex-core` package export surface. + +#### Scenario: Core no longer imports lifecycle model from src/lifecycle + +- **WHEN** in-repo Core installation, uninstall, or update modules need `LifecycleReceipt` or `LIFECYCLE_RECEIPT_SCHEMA_VERSION` +- **THEN** they import those symbols from the Core-internal leaf +- **AND THEN** they do not import `src/lifecycle/model` + +#### Scenario: State shares the leaf without depending on Core runtime + +- **WHEN** persisted-state schema or store modules type lifecycle receipts +- **THEN** they import from the Core-internal leaf +- **AND THEN** they do not import `src/core/index.ts`, `createQuantex`, or Core executors + +#### Scenario: Published SDK root stays free of the leaf + +- **WHEN** a TypeScript consumer inspects `src/core/index.ts` or `packages/core/src/index.ts` +- **THEN** those entries do not export `LifecycleReceipt`, `LifecycleObservation`, `LIFECYCLE_RECEIPT_SCHEMA_VERSION`, or other lifecycle-model symbols +- **AND THEN** `createQuantex` remains the only runtime export + +### Requirement: Remaining src/lifecycle engines SHALL stay outside Core until a later knife + +Quantex MUST keep provider-binding, provider-evidence, agent-observation, update-planner, agent-execution, uninstall-postcondition, and the `src/lifecycle` barrel outside `src/core` for the L1 model move. L1 MUST NOT delete `src/lifecycle/` and MUST NOT fold those remaining modules into Core. + +#### Scenario: L1 does not absorb remaining lifecycle engines + +- **WHEN** the L1 model internalization is applied +- **THEN** `src/lifecycle/` still contains the remaining engine modules listed above +- **AND THEN** `src/lifecycle/model.ts` is absent + +#### Scenario: Existing non-SDK barrel may re-export leaf types + +- **WHEN** CLI services or Core modules already import lifecycle types from the `src/lifecycle` barrel +- **THEN** that barrel MAY re-export types and the receipt schema constant from the Core-internal leaf +- **AND THEN** that re-export is not a published `quantex-core` SDK export + diff --git a/src/lifecycle/uninstall-postcondition.ts b/src/lifecycle/uninstall-postcondition.ts index 2daf1f56..6ef6767e 100644 --- a/src/lifecycle/uninstall-postcondition.ts +++ b/src/lifecycle/uninstall-postcondition.ts @@ -2,6 +2,7 @@ // retry used by Core uninstall-executor. Not zero-ref; do not fold into src/core. // Product-path touch so process-only PRs are not required to skip the macOS matrix. // Product-path touch so the cursor-version-probe-targeting archive PR still runs the macOS test matrix. +// Product-path touch so the lifecycle-model-core-internal-l1 archive PR still runs the macOS test matrix. export interface ExecutableAbsenceWaitOptions { readonly attempts?: number readonly delay?: (milliseconds: number) => Promise