Files
suaveplan/openspec/changes/_superseded-narrow-deferrals-2026-05-17/add-installer-upgrade/tasks.md
2026-05-27 23:29:53 +01:00

6.4 KiB

Tasks: add-installer-upgrade

1. Error codes

  • 1.1 Append UPGRADE_NOT_INSTALLED, UPGRADE_VERSION_NOT_FOUND, UPGRADE_SAME_VERSION, UPGRADE_DIRTY_TREE, UPGRADE_STATE_INVALID, UPGRADE_MIGRATION_FAILED, UPGRADE_LOCKFILE_MISSING to the defineCodes("INSTALLER", [...]) tuple in packages/tooling/installer/src/error-codes.ts.
  • 1.2 Extend src/error-codes.spec-conformance.test.ts with an INSTALLER-UPGRADE-09 block asserting the codes are present, frozen, and prefixed INSTALLER..

2. State file (src/state/)

  • 2.1 src/state/state.types.ts — InstallerState, InstalledPlugin, FileHash types.
  • 2.2 src/state/state.schemas.ts — Zod schemas for boundary validation.
  • 2.3 src/state/state-file.ts — loadState(fs, cwd), writeState(fs, cwd, state) (atomic write → fsync → rename), updatePlugin(state, entry). Failure → INSTALLER.UPGRADE_STATE_INVALID.
  • 2.4 src/state/hash-tree.ts — hashTree(fs, dir) → Record<path, hex> using BLAKE3 via @suaveplan/crypto-hash.
  • 2.5 src/state/state-file.spec-conformance.test.ts — INSTALLER-UPGRADE-08.
  • 2.6 .md siblings for every implementation file.

3. Pre-flight checks (src/host/upgrade/preflight.ts)

  • 3.1 detectLockfile(fs, cwd) returning "bun" | "npm" | "pnpm" | "yarn" or throwing INSTALLER.UPGRADE_LOCKFILE_MISSING.
  • 3.2 detectDirtyTree(state, currentHashes) returning the list of user-modified paths. Throws INSTALLER.UPGRADE_DIRTY_TREE unless --allow-dirty or --force was supplied.
  • 3.3 src/host/upgrade/preflight.spec-conformance.test.ts — INSTALLER-UPGRADE-04.
  • 3.4 src/host/upgrade/dirty-tree.spec-conformance.test.ts — INSTALLER-UPGRADE-03.

4. Diff engine (src/host/upgrade/diff.ts)

  • 4.1 computeDiff(fromOps, toOps) returning { added: FileOp[], removed: FileOp[], changed: FileOp[], unchanged: FileOp[] }.
  • 4.2 materialiseDiff(diff, options) translating the diff into the existing FileOp union: removals → { kind: "delete" }, additions → { kind: "write" }, changes → { kind: "write", conflict: "overwrite" } (or merge for patch-json ops).
  • 4.3 Sibling diff.md covering algorithm and tie-breaking rules.

5. Migration runner (src/host/upgrade/migrations.ts)

  • 5.1 MigrationScript, MigrationContext, MigrationTable types in src/host/upgrade/migrations.types.ts.
  • 5.2 loadMigrationTable(plugin) dynamic-imports the plugin's ./upgrade.js if declared in the manifest; absent table → no-op.
  • 5.3 selectMigrations(table, from, to) returns the ordered semver path.
  • 5.4 runMigrations(selected, ctx) runs each script inside an AbortSignal-propagating sandbox. Throw → INSTALLER.UPGRADE_MIGRATION_FAILED.
  • 5.5 src/host/upgrade/migrations.spec-conformance.test.ts — INSTALLER-UPGRADE-06, INSTALLER-UPGRADE-07.

6. Upgrade orchestration (src/host/upgrade/upgrade.ts)

  • 6.1 Host.upgrade(plugin, options) method: 1. Load state-file entry; throw UPGRADE_NOT_INSTALLED if absent. 2. Resolve target version; throw UPGRADE_VERSION_NOT_FOUND if node_modules resolution fails. 3. Short-circuit with UPGRADE_SAME_VERSION when installed === target and not --force. 4. Run preflight (lockfile + dirty-tree). 5. Hash current tree; compute snapshot for rollback. 6. Run plugin.plan for both versions (or load cached bundle manifests). 7. computeDiff → materialiseDiff → Host.applyOps (existing transactional path). 8. Run migrations between the same staged-tmp transaction window. 9. On success, writeState atomically.
  • 6.2 OTel span installer.upgrade wrapping the whole flow with plugin.id, plugin.version.from, plugin.version.to, dry_run.
  • 6.3 src/host/upgrade/upgrade.spec-conformance.test.ts — INSTALLER-UPGRADE-01, INSTALLER-UPGRADE-02, INSTALLER-UPGRADE-10, INSTALLER-UPGRADE-11.
  • 6.4 src/host/upgrade/apply-diff.spec-conformance.test.ts — INSTALLER-UPGRADE-05.

7. CLI wiring (src/bin/suaveplan-install.ts)

  • 7.1 Add app.command("upgrade <plugin>") with flags --dry-run, --force, --no-interactive, --answers <file>, --from <version>, --to <version>, --allow-dirty.
  • 7.2 runUpgrade(pluginId, flags) mirrors runAdd but calls host.upgrade(...).
  • 7.3 src/bin/suaveplan-install.spec-conformance.test.ts — INSTALLER-UPGRADE-12.

8. Testing helpers (src/testing/)

  • 8.1 src/testing/upgrade-plugin.ts — upgradePlugin(plugin, { fromState, toBundle, answers? }) mirroring the existing runPlugin harness for upgrade tests.
  • 8.2 src/testing/fixtures/upgrade-fixture-plugin.ts — two-version fixture plugin (1.0.0 + 2.0.0) with one migration script, used by every upgrade conformance test for end-to-end coverage at 100%.

9. Docs

  • 9.1 Sibling .md for every new implementation .ts per openspec/AGENTS.md §4 (Purpose, Features, Basic, Advanced, API, Notes).
  • 9.2 Update root packages/tooling/installer/README.md Quickstart to include the upgrade verb and link the new module docs.

10. Quality gates

  • 10.1 bunx turbo lint typecheck test --filter=@suaveplan/installer green.
  • 10.2 bunx turbo test:coverage --filter=@suaveplan/installer 100% istanbul on every new module.
  • 10.3 No banned patterns (openspec/AGENTS.md §2) in new files.
  • 10.4 No biome-ignore in new files (openspec/AGENTS.md §2a).
  • 10.5 File-size check: every src/host/upgrade/*.ts ≤ 300 lines, every *.test.ts ≤ 500 lines.
  • 10.6 Sibling .md audit returns zero gaps for new files.

11. Dependent sweep

  • 11.1 bunx turbo test --filter=...@suaveplan/installer green (validates @suaveplan/install-agent-skills against the new surface).
  • 11.2 Add empty upgrade.ts migration table export to @suaveplan/install-agent-skills and wire it through its manifest so the migration-loader path is exercised even with zero migrations.

12. Archive

  • 12.1 Fold specs/installer/spec.md delta into openspec/specs/installer/spec.md.
  • 12.2 Move openspec/changes/add-installer-upgrade/ to openspec/archive/<yyyy-mm-dd>-add-installer-upgrade/.