@juangcarmona/lighthouse 0.2.1 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -8,6 +8,8 @@ Install the exact Lighthouse package artifact selected for the release and run `
8
8
 
9
9
  Use `lighthouse reconcile` to preview missing Lighthouse-managed paths in an existing adoption. It does not write by default; add `--apply` only after reviewing the plan. Existing files whose content differs from the generated projection are conflicts and are never overwritten by reconciliation.
10
10
 
11
+ Use `lighthouse upgrade` to preview a versioned foundation update. The command is dry-run by default, accepts `--scope harness`, `--scope sdlc` or `--scope all`, preserves customized projections and stops when ownership evidence is missing. Add `--apply` only after reviewing the plan, then run `lighthouse verify`.
12
+
11
13
  The harness package owns the baseline command and verification contract. The SDLC package owns lifecycle skills, commands, metacommands, routes and gates, and `lighthouse init` records its selected version in `.lighthouse/sdlc.json`. The UI package owns visual tokens, native recipes and `DESIGN.md`. Stack profiles own their source templates, guidance and checks.
12
14
 
13
15
  ProductShape, ArcShape and OpenSpec keep their own files and commands. Add them through their supported integration routes when the project needs them, and keep their versions and managed paths visible in the project configuration.
@@ -36,10 +38,12 @@ Initialization reports missing ProductShape and ArcShape authority inputs as ado
36
38
 
37
39
  ## Current command scope
38
40
 
39
- The command supports `init`, `authorities`, `reconcile` and `verify`. It checks the manifest, repository boundaries, Conventional Commits metadata, branch naming, the selected SDLC package projection and generated content. Lifecycle stages, tracker operations, agent runs and CodeGraph checks require their own configured integrations.
41
+ The command supports `init`, `authorities`, `reconcile`, `upgrade` and `verify`. It checks the manifest, repository boundaries, Conventional Commits metadata, branch naming, the selected SDLC package projection and generated content. Lifecycle stages, tracker operations, agent runs and CodeGraph checks require their own configured integrations.
40
42
 
41
43
  Initialization installs `.husky/commit-msg`, `.husky/pre-push`, `scripts/lighthouse-guard.mjs` and `.github/workflows/lighthouse.yml` when those paths are free. Existing files remain project-owned and are reported as conflicts. Reconciliation can add missing managed paths after adoption, but it does not overwrite a path that has drifted or belongs to another tool.
42
44
 
45
+ The upgrade route records the ownership hashes in `.lighthouse/upgrade.json`. A missing or stale state file is an adoption gap; reconcile it first instead of allowing a full re-adoption. Upgrade plans report the source and target package and baseline, selected scopes, classifications, preserved paths, verification and recovery action.
46
+
43
47
  The package does not contain the repository gallery, provider-owned integrations or reference source. Those remain with the source repository or the profile that owns them.
44
48
 
45
49
  ## Guardrail catalog
@@ -16,7 +16,8 @@
16
16
  "managed": {"anyOf": [
17
17
  {"const": ["LIGHTHOUSE.md"]},
18
18
  {"const": ["LIGHTHOUSE.md", "scripts/lighthouse-guard.mjs", ".husky/commit-msg", ".husky/pre-push", ".github/workflows/lighthouse.yml"]},
19
- {"const": ["LIGHTHOUSE.md", "scripts/lighthouse-guard.mjs", ".husky/commit-msg", ".husky/pre-push", ".github/workflows/lighthouse.yml", ".lighthouse/sdlc.json"]}
19
+ {"const": ["LIGHTHOUSE.md", "scripts/lighthouse-guard.mjs", ".husky/commit-msg", ".husky/pre-push", ".github/workflows/lighthouse.yml", ".lighthouse/sdlc.json"]},
20
+ {"const": ["LIGHTHOUSE.md", "scripts/lighthouse-guard.mjs", ".husky/commit-msg", ".husky/pre-push", ".github/workflows/lighthouse.yml", ".lighthouse/sdlc.json", ".lighthouse/upgrade.json"]}
20
21
  ]},
21
22
  "repository": {
22
23
  "type": "object",
@@ -0,0 +1,35 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://github.com/juangcarmona/lighthouse/schemas/lighthouse-upgrade.schema.json",
4
+ "title": "Lighthouse upgrade ownership state",
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "required": ["schemaVersion", "package", "baseline", "capabilities", "projections", "lastAppliedMigration", "files"],
8
+ "properties": {
9
+ "$schema": {"type": "string"},
10
+ "schemaVersion": {"const": 1},
11
+ "package": {
12
+ "type": "object",
13
+ "additionalProperties": false,
14
+ "required": ["name", "version"],
15
+ "properties": {"name": {"const": "@juangcarmona/lighthouse"}, "version": {"type": "string", "pattern": "^(?:\\d+\\.\\d+\\.\\d+|[a-f0-9]{40})$"}}
16
+ },
17
+ "baseline": {"type": "string", "minLength": 1},
18
+ "capabilities": {"$ref": "#/$defs/versions"},
19
+ "projections": {"$ref": "#/$defs/versions"},
20
+ "lastAppliedMigration": {"type": "string", "minLength": 1},
21
+ "files": {
22
+ "type": "object",
23
+ "additionalProperties": {
24
+ "type": "object",
25
+ "additionalProperties": false,
26
+ "required": ["hash", "scope"],
27
+ "properties": {
28
+ "hash": {"type": "string", "pattern": "^[a-f0-9]{64}$"},
29
+ "scope": {"enum": ["harness", "sdlc"]}
30
+ }
31
+ }
32
+ }
33
+ },
34
+ "$defs": {"versions": {"type": "object", "additionalProperties": {"type": ["string", "null"]}}}
35
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juangcarmona/lighthouse",
3
- "version": "0.2.1",
3
+ "version": "0.3.1",
4
4
  "type": "module",
5
5
  "description": "Lighthouse adoption and verification CLI",
6
6
  "repository": {
@@ -22,13 +22,14 @@
22
22
  "dependencies": {
23
23
  "@arcshape/cli": "0.1.3",
24
24
  "@prodshape/cli": "0.22.0",
25
- "@juangcarmona/lighthouse-sdlc": "0.2.0"
25
+ "@juangcarmona/lighthouse-sdlc": "0.4.0"
26
26
  },
27
27
  "files": [
28
28
  "src/lighthouse.mjs",
29
29
  "src/guardrails.mjs",
30
30
  "assets",
31
- "README.md"
31
+ "README.md",
32
+ "upgrades"
32
33
  ],
33
34
  "bin": {
34
35
  "lighthouse": "./src/lighthouse.mjs"
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
- import { access, chmod, mkdir, readFile, rename, stat, writeFile } from 'node:fs/promises';
2
+ import { access, chmod, mkdir, readFile, rename, rm, stat, writeFile } from 'node:fs/promises';
3
3
  import { constants, readFileSync } from 'node:fs';
4
+ import { createHash } from 'node:crypto';
4
5
  import { createRequire } from 'node:module';
5
6
  import { basename, dirname, resolve } from 'node:path';
6
7
  import { execFileSync, spawnSync } from 'node:child_process';
@@ -8,10 +9,19 @@ import { evaluateGuardrails, readGuardrailCatalog } from './guardrails.mjs';
8
9
 
9
10
  const root = process.cwd();
10
11
  const packageRequire = createRequire(import.meta.url);
12
+ const cliPackage = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
13
+ function installedPackageVersion(name, entry) {
14
+ try {
15
+ const entryPath = packageRequire.resolve(`${name}/${entry}`);
16
+ return JSON.parse(readFileSync(resolve(dirname(entryPath), 'package.json'), 'utf8')).version;
17
+ } catch { return null; }
18
+ }
11
19
  const manifestPath = resolve(root, 'lighthouse.json');
12
20
  const projection = 'LIGHTHOUSE.md';
21
+ const upgradeStateRelative = '.lighthouse/upgrade.json';
22
+ const upgradeStatePath = resolve(root, upgradeStateRelative);
13
23
  const layout = ['src', 'docs', 'infra', 'scripts'];
14
- const sdlcPackage = { name: '@juangcarmona/lighthouse-sdlc', version: '0.1.0' };
24
+ const sdlcPackage = { name: '@juangcarmona/lighthouse-sdlc', version: installedPackageVersion('@juangcarmona/lighthouse-sdlc', 'contract.json') ?? '0.1.0' };
15
25
  const managedPaths = [
16
26
  projection,
17
27
  'scripts/lighthouse-guard.mjs',
@@ -19,11 +29,18 @@ const managedPaths = [
19
29
  '.husky/pre-push',
20
30
  '.github/workflows/lighthouse.yml',
21
31
  '.lighthouse/sdlc.json',
32
+ upgradeStateRelative,
22
33
  ];
34
+ const previousManagedPaths = managedPaths.slice(0, -1);
23
35
  const legacyManagedPaths = [projection];
24
36
  const branchPattern = '^(feat|fix|docs|refactor|test|chore|build|ci|perf|revert)/[1-9]\\d*-[a-z0-9]+(?:-[a-z0-9]+)*$';
25
37
  const authorityVersions = { productShape: '0.22.0', arcShape: '0.1.3' };
26
- const usage = 'Usage: lighthouse <init|authorities|reconcile|verify> [--name <product>] [--format <text|json>] [--dry-run] [--apply]';
38
+ const upgradeScopes = {
39
+ harness: [projection, 'scripts/lighthouse-guard.mjs', '.husky/commit-msg', '.husky/pre-push', '.github/workflows/lighthouse.yml'],
40
+ sdlc: ['.lighthouse/sdlc.json'],
41
+ };
42
+ const upgradeSchema = './node_modules/@juangcarmona/lighthouse/assets/schemas/lighthouse-upgrade.schema.json';
43
+ const usage = 'Usage: lighthouse <init|authorities|reconcile|upgrade|verify> [--name <product>] [--scope <harness|sdlc|all>] [--format <text|json>] [--dry-run] [--apply]';
27
44
 
28
45
  const defaultRepository = () => ({
29
46
  layout: [...layout],
@@ -198,6 +215,132 @@ function managedFiles(manifest) {
198
215
  return files;
199
216
  }
200
217
 
218
+ function contentHash(content) {
219
+ return createHash('sha256').update(content).digest('hex');
220
+ }
221
+
222
+ function upgradeState(manifest, files = managedFiles(manifest), previous = null) {
223
+ const entries = Object.fromEntries(Object.entries(files).map(([path, content]) => [path, {
224
+ hash: contentHash(content),
225
+ scope: Object.entries(upgradeScopes).find(([, paths]) => paths.includes(path))?.[0] ?? 'harness',
226
+ }]));
227
+ return {
228
+ $schema: upgradeSchema,
229
+ schemaVersion: 1,
230
+ package: { name: cliPackage.name, version: cliPackage.version },
231
+ baseline: manifest.baseline,
232
+ capabilities: { harness: '1', sdlc: files['.lighthouse/sdlc.json'] ? '1' : null },
233
+ projections: { harness: '1', sdlc: files['.lighthouse/sdlc.json'] ? '1' : null },
234
+ lastAppliedMigration: previous?.lastAppliedMigration ?? 'foundation-init-1',
235
+ files: entries,
236
+ };
237
+ }
238
+
239
+ async function readUpgradeState() {
240
+ try { return JSON.parse(await readFile(upgradeStatePath, 'utf8')); }
241
+ catch (error) {
242
+ if (error.code === 'ENOENT') return null;
243
+ throw new Error(`Cannot read ${upgradeStateRelative}: ${error.message}`);
244
+ }
245
+ }
246
+
247
+ async function writeUpgradeState(manifest, previous = null, files = managedFiles(manifest)) {
248
+ const state = upgradeState(manifest, files, previous);
249
+ await mkdir(dirname(upgradeStatePath), { recursive: true });
250
+ await writeFile(upgradeStatePath, `${JSON.stringify(state, null, 2)}\n`, { flag: 'w' });
251
+ }
252
+
253
+ function stateProblems(state) {
254
+ if (!state || typeof state !== 'object' || Array.isArray(state)) return [`${upgradeStateRelative} is missing; run lighthouse reconcile --apply to establish ownership evidence.`];
255
+ const problems = [];
256
+ if (state.schemaVersion !== 1) problems.push(`${upgradeStateRelative} has unsupported schemaVersion ${state.schemaVersion}.`);
257
+ if (!state.package || state.package.name !== cliPackage.name || typeof state.package.version !== 'string') problems.push(`${upgradeStateRelative} does not identify the installed Lighthouse package.`);
258
+ if (!state.files || typeof state.files !== 'object' || Array.isArray(state.files)) problems.push(`${upgradeStateRelative} does not contain file ownership evidence.`);
259
+ return problems;
260
+ }
261
+
262
+ function scopesFromArgs(args) {
263
+ const selected = new Set();
264
+ for (let index = 0; index < args.length; index += 1) {
265
+ if (args[index] !== '--scope' && !args[index].startsWith('--scope=')) continue;
266
+ const value = args[index] === '--scope' ? args[index + 1] : args[index].slice('--scope='.length);
267
+ if (args[index] === '--scope') index += 1;
268
+ if (!value) throw new Error('--scope requires harness, sdlc or all');
269
+ for (const scope of value.split(',')) {
270
+ if (scope === 'all') { selected.add('harness'); selected.add('sdlc'); }
271
+ else if (scope === 'harness' || scope === 'sdlc') selected.add(scope);
272
+ else throw new Error(`Unknown upgrade scope ${scope}; use harness, sdlc or all.`);
273
+ }
274
+ }
275
+ return selected.size ? selected : new Set(Object.keys(upgradeScopes));
276
+ }
277
+
278
+ function selectedPaths(scopes) {
279
+ return new Set([...scopes].flatMap((scope) => upgradeScopes[scope]));
280
+ }
281
+
282
+ function classifyUpgradeFile(path, current, expected, recordedHash) {
283
+ if (current === null) return { classification: 'additive', action: 'add' };
284
+ const currentHash = contentHash(current);
285
+ if (current === expected && currentHash === recordedHash) return { classification: 'unchanged', action: 'keep' };
286
+ if (current === expected && currentHash !== recordedHash) return { classification: 'adoptable', action: 'record' };
287
+ if (recordedHash && currentHash === recordedHash) return { classification: 'adoptable', action: 'update' };
288
+ if (recordedHash) return { classification: 'customized', action: 'preserve', reason: 'current content differs from the recorded projection' };
289
+ return { classification: 'conflicting', action: 'review', reason: 'ownership is not recorded for this path' };
290
+ }
291
+
292
+ async function planUpgrade(manifest, scopes) {
293
+ const state = await readUpgradeState();
294
+ const problems = stateProblems(state);
295
+ const files = managedFiles(manifest);
296
+ const paths = selectedPaths(scopes);
297
+ const changes = [];
298
+ for (const path of Object.keys(files).filter((candidate) => paths.has(candidate))) {
299
+ const target = resolve(root, path);
300
+ const current = await exists(target) ? await readFile(target, 'utf8') : null;
301
+ const expected = files[path];
302
+ const recordedHash = state?.files?.[path]?.hash;
303
+ const result = classifyUpgradeFile(path, current, expected, recordedHash);
304
+ changes.push({ path, scope: Object.entries(upgradeScopes).find(([, values]) => values.includes(path))?.[0], ...result });
305
+ }
306
+ if (state?.baseline && state.baseline !== manifest.baseline) {
307
+ changes.unshift({ path: 'baseline', scope: 'harness', classification: 'breaking', action: 'review', reason: `baseline ${state.baseline} is not compatible with ${manifest.baseline}` });
308
+ }
309
+ return {
310
+ source: {
311
+ package: state?.package ?? null,
312
+ baseline: state?.baseline ?? null,
313
+ capabilities: state?.capabilities ?? null,
314
+ projections: state?.projections ?? null,
315
+ lastAppliedMigration: state?.lastAppliedMigration ?? null,
316
+ },
317
+ target: {
318
+ package: { name: cliPackage.name, version: cliPackage.version },
319
+ baseline: manifest.baseline,
320
+ capabilities: { harness: '1', sdlc: files['.lighthouse/sdlc.json'] ? '1' : null },
321
+ projections: { harness: '1', sdlc: files['.lighthouse/sdlc.json'] ? '1' : null },
322
+ },
323
+ scopes: [...scopes],
324
+ prerequisites: problems.length ? [{ status: 'failed', message: problems.join(' ') }] : [{ status: 'passed', message: 'Ownership metadata is readable.' }],
325
+ changes,
326
+ verification: ['lighthouse verify'],
327
+ recovery: 'Restore the pre-update commit or rerun lighthouse upgrade after resolving the reported ownership conflict.',
328
+ problems,
329
+ };
330
+ }
331
+
332
+ function printUpgradePlan(plan) {
333
+ console.log('Lighthouse upgrade plan:');
334
+ console.log(` package ${plan.source.package?.version ?? 'unknown'} -> ${plan.target.package.version}`);
335
+ console.log(` baseline ${plan.source.baseline ?? 'unknown'} -> ${plan.target.baseline}`);
336
+ console.log(` scopes ${plan.scopes.join(', ')}`);
337
+ for (const prerequisite of plan.prerequisites) console.log(` prerequisite ${prerequisite.status}: ${prerequisite.message}`);
338
+ for (const change of plan.changes) console.log(` ${change.classification} ${change.action} ${change.path}${change.reason ? ` (${change.reason})` : ''}`);
339
+ if (plan.problems.length) for (const problem of plan.problems) console.error(` adoption gap ${problem}`);
340
+ console.log(` verify ${plan.verification[0]}`);
341
+ console.log(` recovery ${plan.recovery}`);
342
+ }
343
+
201
344
  function validate(manifest) {
202
345
  const problems = [];
203
346
  if (!manifest || typeof manifest !== 'object' || Array.isArray(manifest)) return ['manifest must be an object'];
@@ -244,8 +387,11 @@ function validate(manifest) {
244
387
  const completeManaged = Array.isArray(manifest.managed)
245
388
  && manifest.managed.length === managedPaths.length
246
389
  && manifest.managed.every((entry, index) => entry === managedPaths[index]);
247
- if (!legacyManaged && !completeManaged) {
248
- problems.push(`managed must be ${legacyManagedPaths.join(', ')} or the complete Lighthouse guardrail set`);
390
+ const previousCompleteManaged = Array.isArray(manifest.managed)
391
+ && manifest.managed.length === previousManagedPaths.length
392
+ && manifest.managed.every((entry, index) => entry === previousManagedPaths[index]);
393
+ if (!legacyManaged && !previousCompleteManaged && !completeManaged) {
394
+ problems.push(`managed must be ${legacyManagedPaths.join(', ')}, the previous Lighthouse guardrail set or the current complete set`);
249
395
  }
250
396
  if ('repository' in manifest) {
251
397
  const repository = manifest.repository;
@@ -317,6 +463,8 @@ async function planInitialization(manifest, manifestMissing) {
317
463
  if (await exists(resolve(root, file))) conflicts.push(`${file} already exists; refusing to overwrite it`);
318
464
  else operations.push({ path: file, action: 'create' });
319
465
  }
466
+ if (await exists(upgradeStatePath)) conflicts.push(`${upgradeStateRelative} already exists; refusing to overwrite it`);
467
+ else operations.push({ path: upgradeStateRelative, action: 'create' });
320
468
  return { operations, conflicts };
321
469
  }
322
470
 
@@ -337,6 +485,9 @@ async function planReconciliation(manifest) {
337
485
  conflicts.push(`${path} exists but is not a readable file; refusing to overwrite it`);
338
486
  }
339
487
  }
488
+ if (!(await exists(upgradeStatePath))) {
489
+ operations.push({ path: upgradeStateRelative, action: 'create', content: `${JSON.stringify(upgradeState(manifest), null, 2)}\n` });
490
+ }
340
491
  return { operations, conflicts };
341
492
  }
342
493
 
@@ -374,6 +525,7 @@ async function initialize(args) {
374
525
  await writeFile(resolve(root, file), content, { flag: 'wx', mode: file.startsWith('.husky/') ? 0o755 : 0o644 });
375
526
  if (file.startsWith('.husky/')) await chmod(resolve(root, file), 0o755);
376
527
  }
528
+ await writeUpgradeState(manifest, null, files);
377
529
  try {
378
530
  const configured = execFileSync('git', ['config', '--get', 'core.hooksPath'], { cwd: root, encoding: 'utf8' }).trim();
379
531
  if (!configured) execFileSync('git', ['config', 'core.hooksPath', '.husky'], { cwd: root, stdio: 'ignore' });
@@ -413,6 +565,76 @@ async function reconcile(args) {
413
565
  console.log('Reconciled missing Lighthouse-managed paths. Run lighthouse verify to check the complete projection.');
414
566
  }
415
567
 
568
+ async function writeTextAtomically(path, content) {
569
+ const temporary = `${path}.tmp-${process.pid}`;
570
+ await writeFile(temporary, content, { flag: 'wx' });
571
+ await rename(temporary, path);
572
+ }
573
+
574
+ async function applyUpgrade(manifest, plan) {
575
+ if (plan.problems.length) throw new Error(`Upgrade stopped; ${plan.problems.join(' ')}`);
576
+ const blocked = plan.changes.filter((change) => ['conflicting', 'breaking'].includes(change.classification));
577
+ if (blocked.length) throw new Error(`Upgrade stopped; resolve ownership conflicts for: ${blocked.map((change) => change.path).join(', ')}.`);
578
+ const files = managedFiles(manifest);
579
+ const previous = await readUpgradeState();
580
+ const snapshots = new Map();
581
+ const updates = plan.changes.filter((change) => ['add', 'update', 'record'].includes(change.action));
582
+ for (const change of updates) {
583
+ const target = resolve(root, change.path);
584
+ snapshots.set(change.path, await exists(target) ? await readFile(target, 'utf8') : null);
585
+ }
586
+ const stateSnapshot = await readFile(upgradeStatePath, 'utf8');
587
+ try {
588
+ for (const change of updates) {
589
+ const target = resolve(root, change.path);
590
+ await mkdir(dirname(target), { recursive: true });
591
+ await writeTextAtomically(target, files[change.path]);
592
+ }
593
+ const next = upgradeState(manifest, files, { ...previous, lastAppliedMigration: `upgrade-${cliPackage.version}` });
594
+ for (const path of Object.keys(next.files)) {
595
+ const selected = plan.changes.find((change) => change.path === path);
596
+ if (!selected || selected.action === 'preserve' || selected.action === 'review') {
597
+ if (previous.files?.[path]) next.files[path] = previous.files[path];
598
+ else delete next.files[path];
599
+ }
600
+ }
601
+ next.lastAppliedMigration = `upgrade-${cliPackage.version}`;
602
+ await writeTextAtomically(upgradeStatePath, `${JSON.stringify(next, null, 2)}\n`);
603
+ } catch (error) {
604
+ for (const [path, content] of snapshots) {
605
+ const target = resolve(root, path);
606
+ if (content === null) await rm(target, { force: true });
607
+ else await writeFile(target, content);
608
+ }
609
+ await writeFile(upgradeStatePath, stateSnapshot);
610
+ throw new Error(`Upgrade rolled back: ${error.message}`);
611
+ }
612
+ const preserved = plan.changes.filter((change) => change.classification === 'customized').map((change) => change.path);
613
+ console.log(`Applied Lighthouse upgrade ${cliPackage.version}. Run lighthouse verify to check the result.`);
614
+ if (preserved.length) console.log(`Preserved customized paths for review: ${preserved.join(', ')}`);
615
+ }
616
+
617
+ async function upgrade(args) {
618
+ const manifest = await readManifest();
619
+ if (!manifest) throw new Error('Cannot read lighthouse.json: file does not exist; run lighthouse init --name <product>.');
620
+ const problems = validate(manifest);
621
+ if (problems.length) throw new Error(problems.map((problem) => `Invalid lighthouse.json: ${problem}`).join('\n'));
622
+ const plan = await planUpgrade(manifest, scopesFromArgs(args));
623
+ const format = formatFromArgs(args);
624
+ const apply = args.includes('--apply');
625
+ const dryRun = args.includes('--dry-run') || !apply;
626
+ if (args.includes('--apply') && args.includes('--dry-run')) throw new Error('--apply and --dry-run cannot be used together.');
627
+ if (format === 'json' && apply) throw new Error('--format json is available for dry-run upgrade plans; use text when applying.');
628
+ if (format === 'json') console.log(JSON.stringify(plan, null, 2));
629
+ else printUpgradePlan(plan);
630
+ if (dryRun) {
631
+ if (format !== 'json') console.log('Dry run: nothing was changed. Re-run with --apply after reviewing the plan.');
632
+ if (plan.problems.length) process.exitCode = 1;
633
+ return;
634
+ }
635
+ await applyUpgrade(manifest, plan);
636
+ }
637
+
416
638
  const authorityCommands = [
417
639
  {
418
640
  label: 'ProductShape',
@@ -587,21 +809,30 @@ let invalidArgument = false;
587
809
  for (let index = 0; index < commandArgs.length; index += 1) {
588
810
  const arg = commandArgs[index];
589
811
  if (arg === '--dry-run') continue;
590
- if (arg === '--apply') { if (!['authorities', 'reconcile'].includes(command)) invalidArgument = true; continue; }
812
+ if (arg === '--apply') { if (!['authorities', 'reconcile', 'upgrade'].includes(command)) invalidArgument = true; continue; }
591
813
  if (arg === '--name') { if (!commandArgs[index + 1]) invalidArgument = true; else index += 1; continue; }
592
814
  if (arg.startsWith('--name=') && arg.length > '--name='.length) continue;
593
815
  if (arg === '--format') {
594
- if (command !== 'verify' || !['text', 'json'].includes(commandArgs[index + 1])) invalidArgument = true;
816
+ if (!['verify', 'upgrade'].includes(command) || !['text', 'json'].includes(commandArgs[index + 1])) invalidArgument = true;
595
817
  else index += 1;
596
818
  continue;
597
819
  }
598
820
  if (arg.startsWith('--format=') && ['text', 'json'].includes(arg.slice('--format='.length))) {
599
- if (command !== 'verify') invalidArgument = true;
821
+ if (!['verify', 'upgrade'].includes(command)) invalidArgument = true;
822
+ continue;
823
+ }
824
+ if (arg === '--scope') {
825
+ if (command !== 'upgrade' || !commandArgs[index + 1]) invalidArgument = true;
826
+ else index += 1;
827
+ continue;
828
+ }
829
+ if (arg.startsWith('--scope=') && arg.length > '--scope='.length) {
830
+ if (command !== 'upgrade') invalidArgument = true;
600
831
  continue;
601
832
  }
602
833
  invalidArgument = true;
603
834
  }
604
- if (!['init', 'authorities', 'reconcile', 'verify'].includes(command) || invalidArgument) {
835
+ if (!['init', 'authorities', 'reconcile', 'upgrade', 'verify'].includes(command) || invalidArgument) {
605
836
  console.error(usage);
606
837
  process.exit(2);
607
838
  }
@@ -609,6 +840,7 @@ try {
609
840
  if (command === 'init') await initialize(args.slice(1));
610
841
  else if (command === 'authorities') await authorities(args.slice(1));
611
842
  else if (command === 'reconcile') await reconcile(args.slice(1));
843
+ else if (command === 'upgrade') await upgrade(args.slice(1));
612
844
  else {
613
845
  const manifest = await readManifest();
614
846
  if (!manifest) throw new Error('Cannot read lighthouse.json: file does not exist; run lighthouse init --name <product>.');
@@ -0,0 +1,26 @@
1
+ # Lighthouse 0.3.0 upgrade guide
2
+
3
+ ## Prerequisites
4
+
5
+ - The repository has a valid `lighthouse.json`.
6
+ - The working tree is clean or the update is isolated in a reviewable branch.
7
+ - `.lighthouse/upgrade.json` exists and records the managed projection hashes.
8
+ - Node.js 22 or newer is available.
9
+
10
+ If `.lighthouse/upgrade.json` is missing, run `lighthouse reconcile --dry-run`, review the plan and then run `lighthouse reconcile --apply` to establish ownership evidence. Resolve any reported conflicts first.
11
+
12
+ ## Plan the update
13
+
14
+ Run `lighthouse upgrade` to preview all Lighthouse-owned scopes. Use `lighthouse upgrade --scope harness` for `LIGHTHOUSE.md`, hooks, the guard script and the repository workflow, or use `lighthouse upgrade --scope sdlc` for the SDLC control-plane projection.
15
+
16
+ The plan reports the installed package and baseline, the selected scopes, prerequisites, file classifications, preserved content, verification command and recovery action. `lighthouse upgrade --format json` produces the same plan for a review tool or CI job.
17
+
18
+ ## Apply the update
19
+
20
+ Review the plan and run `lighthouse upgrade --apply` with the same scope. Lighthouse writes only missing or provably managed projections, records the new ownership hashes and preserves customized files for review.
21
+
22
+ Run `lighthouse verify` after the update. A preserved customized projection may still report drift; resolve it through the normal repository change route before making the verification gate required.
23
+
24
+ ## Recovery
25
+
26
+ If the update stops, keep the plan output and resolve the stated ownership or prerequisite issue. If a write fails, Lighthouse restores the files changed by that attempt; restore the branch or rerun the plan after correcting the reported condition.