@juangcarmona/lighthouse 0.6.0 → 0.8.0

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.
Files changed (40) hide show
  1. package/README.md +7 -4
  2. package/assets/command-aliases.json +11 -0
  3. package/assets/profiles/catalog.json +1019 -17
  4. package/assets/profiles/mobile-expo/README.md +19 -0
  5. package/assets/profiles/mobile-expo/SKILL.md +14 -0
  6. package/assets/profiles/mobile-expo/adoption.md +11 -0
  7. package/assets/profiles/mobile-expo/guardrails.md +14 -0
  8. package/assets/profiles/mobile-expo/testing.md +13 -0
  9. package/assets/profiles/mobile-maui/README.md +19 -0
  10. package/assets/profiles/mobile-maui/SKILL.md +14 -0
  11. package/assets/profiles/mobile-maui/adoption.md +11 -0
  12. package/assets/profiles/mobile-maui/guardrails.md +13 -0
  13. package/assets/profiles/mobile-maui/testing.md +13 -0
  14. package/assets/profiles/pwa-capability/README.md +19 -0
  15. package/assets/profiles/pwa-capability/SKILL.md +14 -0
  16. package/assets/profiles/pwa-capability/adoption.md +9 -0
  17. package/assets/profiles/pwa-capability/guardrails.md +13 -0
  18. package/assets/profiles/pwa-capability/offline.md +11 -0
  19. package/assets/profiles/pwa-capability/testing.md +15 -0
  20. package/assets/profiles/react-web-platform/README.md +24 -0
  21. package/assets/profiles/react-web-platform/SKILL.md +18 -0
  22. package/assets/profiles/react-web-platform/adoption.md +17 -0
  23. package/assets/profiles/react-web-platform/guardrails.md +17 -0
  24. package/assets/profiles/react-web-platform/structure.md +36 -0
  25. package/assets/profiles/react-web-platform/testing.md +15 -0
  26. package/assets/profiles/web-bff/README.md +22 -0
  27. package/assets/profiles/web-bff/SKILL.md +16 -0
  28. package/assets/profiles/web-bff/adoption.md +9 -0
  29. package/assets/profiles/web-bff/security.md +16 -0
  30. package/assets/profiles/web-bff/testing.md +14 -0
  31. package/assets/schemas/command-aliases.schema.json +35 -0
  32. package/assets/schemas/init-plan.schema.json +40 -0
  33. package/assets/schemas/profile-catalog.schema.json +286 -20
  34. package/assets/skills/init/SKILL.md +30 -0
  35. package/package.json +5 -3
  36. package/src/aliases.mjs +56 -0
  37. package/src/create-dotnet.mjs +5 -1
  38. package/src/init-plan.mjs +62 -0
  39. package/src/lighthouse.mjs +443 -14
  40. package/src/profiles.mjs +78 -5
@@ -7,9 +7,12 @@ import { basename, dirname, resolve } from 'node:path';
7
7
  import { execFileSync, spawnSync } from 'node:child_process';
8
8
  import { evaluateGuardrails, readGuardrailCatalog } from './guardrails.mjs';
9
9
  import { composeProfiles, normalizeApplication, profileCompositionResult, readProfileCatalog } from './profiles.mjs';
10
+ import { hasUnresolvedQuestions, manifestFromInitPlan, validateInitPlan } from './init-plan.mjs';
11
+ import { readCommandAliases, verifyCommandAliases } from './aliases.mjs';
10
12
  let sdlcCapabilities;
11
13
  let sdlcReconcile;
12
14
  let sdlcGithub;
15
+ let sdlcAliases;
13
16
  try {
14
17
  [sdlcCapabilities, sdlcReconcile, sdlcGithub] = await Promise.all([
15
18
  import('@juangcarmona/lighthouse-sdlc/capabilities'),
@@ -17,6 +20,7 @@ try {
17
20
  import('@juangcarmona/lighthouse-sdlc/github'),
18
21
  ]);
19
22
  } catch { /* Standalone generated starters can run without the optional SDLC package. */ }
23
+ try { sdlcAliases = await import('@juangcarmona/lighthouse-sdlc/aliases'); } catch { /* Older standalone installations may not publish aliases. */ }
20
24
 
21
25
  const root = process.cwd();
22
26
  const packageRequire = createRequire(import.meta.url);
@@ -41,19 +45,28 @@ const managedPaths = [
41
45
  '.husky/pre-push',
42
46
  '.github/workflows/lighthouse.yml',
43
47
  '.lighthouse/sdlc.json',
48
+ '.agents/skills/lighthouse-init/SKILL.md',
44
49
  upgradeStateRelative,
50
+ '.lighthouse/commands.json',
45
51
  ];
46
52
  const profileManagedPaths = Object.freeze({
47
53
  'visual-theme': ['docs/design/DESIGN.md', '.agents/skills/lighthouse-ui/SKILL.md'],
48
54
  'dotnet-backend': ['docs/profiles/dotnet/README.md', 'docs/profiles/dotnet/adoption.md', 'docs/profiles/dotnet/architecture.md', 'docs/profiles/dotnet/guardrails.md', 'docs/profiles/dotnet/practices.md', 'docs/profiles/dotnet/testing-and-development.md', 'docs/profiles/dotnet/use-cases.md', '.agents/skills/lighthouse-dotnet/SKILL.md'],
55
+ 'react-web-platform': ['docs/profiles/react-web-platform/README.md', 'docs/profiles/react-web-platform/adoption.md', 'docs/profiles/react-web-platform/structure.md', 'docs/profiles/react-web-platform/guardrails.md', 'docs/profiles/react-web-platform/testing.md', '.agents/skills/lighthouse-react-web/SKILL.md'],
56
+ 'web-bff': ['docs/profiles/web-bff/README.md', 'docs/profiles/web-bff/adoption.md', 'docs/profiles/web-bff/security.md', 'docs/profiles/web-bff/testing.md', '.agents/skills/lighthouse-web-bff/SKILL.md'],
57
+ 'pwa-capability': ['docs/profiles/pwa-capability/README.md', 'docs/profiles/pwa-capability/adoption.md', 'docs/profiles/pwa-capability/guardrails.md', 'docs/profiles/pwa-capability/offline.md', 'docs/profiles/pwa-capability/testing.md', '.agents/skills/lighthouse-pwa/SKILL.md'],
58
+ 'mobile-expo': ['docs/profiles/mobile-expo/README.md', 'docs/profiles/mobile-expo/adoption.md', 'docs/profiles/mobile-expo/guardrails.md', 'docs/profiles/mobile-expo/testing.md', '.agents/skills/lighthouse-mobile-expo/SKILL.md'],
59
+ 'mobile-maui': ['docs/profiles/mobile-maui/README.md', 'docs/profiles/mobile-maui/adoption.md', 'docs/profiles/mobile-maui/guardrails.md', 'docs/profiles/mobile-maui/testing.md', '.agents/skills/lighthouse-mobile-maui/SKILL.md'],
49
60
  });
50
61
  const previousManagedPaths = managedPaths.slice(0, -1);
51
62
  const legacyManagedPaths = [projection];
52
- const supportedProfiles = ['node-tooling', 'dotnet-backend', 'visual-theme'];
63
+ const preSemanticManagedPaths = [projection, 'scripts/lighthouse-guard.mjs', '.husky/commit-msg', '.husky/pre-push', '.github/workflows/lighthouse.yml', '.lighthouse/sdlc.json', upgradeStateRelative];
64
+ const preSemanticWithoutUpgradePaths = preSemanticManagedPaths.slice(0, -1);
65
+ const supportedProfiles = readProfileCatalog().profiles.map(({ id }) => id);
53
66
  const branchPattern = '^(feat|fix|docs|refactor|test|chore|build|ci|perf|revert)/[1-9]\\d*-[a-z0-9]+(?:-[a-z0-9]+)*$';
54
- const authorityVersions = { productShape: '0.22.0', arcShape: '0.1.3' };
67
+ const authorityVersions = { productShape: '0.22.0', arcShape: '0.1.4' };
55
68
  const upgradeSchema = './node_modules/@juangcarmona/lighthouse/assets/schemas/lighthouse-upgrade.schema.json';
56
- const usage = 'Usage: lighthouse <init|create-dotnet|profiles|authorities|reconcile|upgrade|capabilities|verify> [--name <product>] [--profile <profile>] [--scope <harness|sdlc|all>] [--format <text|json>] [--dry-run] [--apply]';
69
+ const usage = 'Usage: lighthouse <init|aliases|create-dotnet|profiles|authorities|reconcile|upgrade|capabilities|verify> [--name <product>] [--plan-file <path>] [--profile <profile>] [--scope <harness|sdlc|all>] [--format <text|json>] [--adopt-existing] [--dry-run] [--apply]';
57
70
 
58
71
  const defaultRepository = () => ({
59
72
  layout: [...layout],
@@ -68,8 +81,8 @@ function managedPathsFor(manifest) {
68
81
 
69
82
  function upgradeScopesFor(manifest) {
70
83
  return {
71
- harness: [projection, 'scripts/lighthouse-guard.mjs', '.husky/commit-msg', '.husky/pre-push', '.github/workflows/lighthouse.yml', ...((manifest?.profiles ?? []).flatMap((profile) => profileManagedPaths[profile] ?? []))],
72
- sdlc: ['.lighthouse/sdlc.json'],
84
+ harness: [projection, 'scripts/lighthouse-guard.mjs', '.husky/commit-msg', '.husky/pre-push', '.github/workflows/lighthouse.yml', '.agents/skills/lighthouse-init/SKILL.md', ...((manifest?.profiles ?? []).flatMap((profile) => profileManagedPaths[profile] ?? []))],
85
+ sdlc: ['.lighthouse/sdlc.json', '.lighthouse/commands.json'],
73
86
  };
74
87
  }
75
88
 
@@ -96,6 +109,46 @@ function profileAssets(manifest) {
96
109
  catch (error) { throw new Error(`Cannot read the installed Lighthouse .NET profile: ${error.message}`); }
97
110
  }
98
111
  }
112
+ if ((manifest?.profiles ?? []).includes('react-web-platform')) {
113
+ const files = ['README.md', 'adoption.md', 'structure.md', 'guardrails.md', 'testing.md', 'SKILL.md'];
114
+ for (const file of files) {
115
+ const target = file === 'SKILL.md' ? '.agents/skills/lighthouse-react-web/SKILL.md' : `docs/profiles/react-web-platform/${file}`;
116
+ try { assets[target] = readFileSync(new URL(`../assets/profiles/react-web-platform/${file}`, import.meta.url), 'utf8'); }
117
+ catch (error) { throw new Error(`Cannot read the installed Lighthouse React web profile: ${error.message}`); }
118
+ }
119
+ }
120
+ if ((manifest?.profiles ?? []).includes('web-bff')) {
121
+ const files = ['README.md', 'adoption.md', 'security.md', 'testing.md', 'SKILL.md'];
122
+ for (const file of files) {
123
+ const target = file === 'SKILL.md' ? '.agents/skills/lighthouse-web-bff/SKILL.md' : `docs/profiles/web-bff/${file}`;
124
+ try { assets[target] = readFileSync(new URL(`../assets/profiles/web-bff/${file}`, import.meta.url), 'utf8'); }
125
+ catch (error) { throw new Error(`Cannot read the installed Lighthouse web BFF profile: ${error.message}`); }
126
+ }
127
+ }
128
+ if ((manifest?.profiles ?? []).includes('pwa-capability')) {
129
+ const files = ['README.md', 'adoption.md', 'guardrails.md', 'offline.md', 'testing.md', 'SKILL.md'];
130
+ for (const file of files) {
131
+ const target = file === 'SKILL.md' ? '.agents/skills/lighthouse-pwa/SKILL.md' : `docs/profiles/pwa-capability/${file}`;
132
+ try { assets[target] = readFileSync(new URL(`../assets/profiles/pwa-capability/${file}`, import.meta.url), 'utf8'); }
133
+ catch (error) { throw new Error(`Cannot read the installed Lighthouse PWA capability: ${error.message}`); }
134
+ }
135
+ }
136
+ if ((manifest?.profiles ?? []).includes('mobile-expo')) {
137
+ const files = ['README.md', 'adoption.md', 'guardrails.md', 'testing.md', 'SKILL.md'];
138
+ for (const file of files) {
139
+ const target = file === 'SKILL.md' ? '.agents/skills/lighthouse-mobile-expo/SKILL.md' : `docs/profiles/mobile-expo/${file}`;
140
+ try { assets[target] = readFileSync(new URL(`../assets/profiles/mobile-expo/${file}`, import.meta.url), 'utf8'); }
141
+ catch (error) { throw new Error(`Cannot read the installed Lighthouse Expo mobile profile: ${error.message}`); }
142
+ }
143
+ }
144
+ if ((manifest?.profiles ?? []).includes('mobile-maui')) {
145
+ const files = ['README.md', 'adoption.md', 'guardrails.md', 'testing.md', 'SKILL.md'];
146
+ for (const file of files) {
147
+ const target = file === 'SKILL.md' ? '.agents/skills/lighthouse-mobile-maui/SKILL.md' : `docs/profiles/mobile-maui/${file}`;
148
+ try { assets[target] = readFileSync(new URL(`../assets/profiles/mobile-maui/${file}`, import.meta.url), 'utf8'); }
149
+ catch (error) { throw new Error(`Cannot read the installed Lighthouse .NET MAUI mobile profile: ${error.message}`); }
150
+ }
151
+ }
99
152
  return assets;
100
153
  }
101
154
 
@@ -225,7 +278,7 @@ function render(manifest) {
225
278
  const sdlcLine = selectedSdlc ? `- SDLC package: \`${selectedSdlc.name}@${selectedSdlc.version}\`\n` : '';
226
279
  const selectedUi = manifest.packages?.ui;
227
280
  const uiLine = selectedUi ? `- UI package: \`${selectedUi.name}@${selectedUi.version}\`\n` : '';
228
- return `<!-- Generated by Lighthouse. Edit lighthouse.json and regenerate this file. -->\n\n# ${manifest.product} repository baseline\n\n- Baseline: \`${manifest.baseline}\`\n- Product: \`${manifest.product}\`\n- Profiles: ${manifest.profiles.length ? manifest.profiles.map((x) => `\`${x}\``).join(', ') : 'none'}\n${sdlcLine}${uiLine}- ProductShape: ${manifest.integrations.productShape ?? 'not adopted'}\n- ArcShape: ${manifest.integrations.arcShape ?? 'not adopted'}\n- Public site: \`${publicSite}\`\n- Repository roots: ${repository.layout.map((x) => `\`${x}/\``).join(', ')}\n- Commit convention: \`${repository.conventions.commits}\`\n\n## Delivery contract\n\nProductShape remains the source of product intent and Product Changes. ArcShape remains the source of architecture documentation and decisions. Lighthouse installs the repository baseline and verifies the selected integrations without replacing either authority.\n\nThe repository keeps source code in \`src/\`, documentation in \`docs/\`, infrastructure in \`infra/\` and repository automation in \`scripts/\`. A project may record a narrow, owned exception, but an initializer never overwrites an existing project file.\n\nThe selected SDLC package owns lifecycle skills, commands, meta-commands, routes and gates. The \`.lighthouse/\` control plane records the selected package and host adapters without storing agent runtime data.\n\nWhen the public site is enabled, its publication is allowed only after ProductShape and ArcShape validate successfully. A missing or invalid authority stops the build before deployment and reports the command that can repair the missing input.\n\nThis file is managed. Put repository-specific policy in its own documentation and keep this projection generated from \`lighthouse.json\`.\n`;
281
+ return `<!-- Generated by Lighthouse. Edit lighthouse.json and regenerate this file. -->\n\n# ${manifest.product} repository baseline\n\n- Baseline: \`${manifest.baseline}\`\n- Product: \`${manifest.product}\`\n- Profiles: ${manifest.profiles.length ? manifest.profiles.map((x) => `\`${x}\``).join(', ') : 'none'}\n${sdlcLine}${uiLine}- ProductShape: ${manifest.integrations.productShape ?? 'not adopted'}\n- ArcShape: ${manifest.integrations.arcShape ?? 'not adopted'}\n- Public site: \`${publicSite}\`\n- Repository roots: ${repository.layout.map((x) => `\`${x}/\``).join(', ')}\n- Commit convention: \`${repository.conventions.commits}\`\n\n## Delivery contract\n\nProductShape remains the source of product intent and Product Changes. ArcShape remains the source of architecture documentation and decisions. Lighthouse installs the repository baseline and verifies the selected integrations without replacing either authority.\n\nThe repository keeps source code in \`src/\`, documentation in \`docs/\`, infrastructure in \`infra/\` and repository automation in \`scripts/\`. A project may record a narrow, owned exception, but an initializer never overwrites an existing project file.\n\nThe selected SDLC package owns lifecycle skills, commands, meta-commands, routes and gates. The \`.lighthouse/\` control plane records the selected package, command aliases and host adapters without storing agent runtime data. Use \`lighthouse aliases --format json\` or \`.lighthouse/commands.json\` to discover canonical and short agent commands.\n\nWhen the public site is enabled, its publication is allowed only after ProductShape and ArcShape validate successfully. A missing or invalid authority stops the build before deployment and reports the command that can repair the missing input.\n\nThis file is managed. Put repository-specific policy in its own documentation and keep this projection generated from \`lighthouse.json\`.\n`;
229
282
  }
230
283
 
231
284
  function guardrailScript(manifest) {
@@ -309,6 +362,59 @@ jobs:
309
362
  `;
310
363
  }
311
364
 
365
+ function readSdlcCommandAliases() {
366
+ try {
367
+ const path = packageRequire.resolve('@juangcarmona/lighthouse-sdlc/command-aliases.json');
368
+ return JSON.parse(readFileSync(path, 'utf8'));
369
+ } catch { return null; }
370
+ }
371
+
372
+ function commandAliasesProjection() {
373
+ const contracts = [{ contract: readCommandAliases(), packageVersion: cliPackage.version }];
374
+ const sdlc = readSdlcCommandAliases();
375
+ if (sdlc) contracts.push({ contract: sdlc, packageVersion: sdlcPackage.version });
376
+ const aliases = contracts.flatMap(({ contract, packageVersion }) => (contract.aliases ?? []).map((alias) => ({
377
+ ...alias,
378
+ owner: contract.package.name,
379
+ packageVersion,
380
+ })));
381
+ return {
382
+ schemaVersion: 1,
383
+ contractVersion: 1,
384
+ canonicalNamespace: '/lighthouse',
385
+ aliasNamespace: '/lh',
386
+ packages: contracts.map(({ contract, packageVersion }) => ({
387
+ name: contract.package.name,
388
+ version: packageVersion,
389
+ contractVersion: contract.contractVersion,
390
+ aliases: contract.aliases,
391
+ })),
392
+ aliases,
393
+ };
394
+ }
395
+
396
+ function commandAliasProjectionText(projection) {
397
+ return `${JSON.stringify(projection, null, 2)}\n`;
398
+ }
399
+
400
+ async function commandAliases(format = 'text') {
401
+ const harness = readCommandAliases();
402
+ const errors = await verifyCommandAliases(harness);
403
+ const sdlc = readSdlcCommandAliases();
404
+ if (!sdlc) errors.push({ code: 'aliases.sdlc-unavailable', message: 'The installed SDLC package does not publish command aliases; upgrade it before using the SDLC alias namespace.' });
405
+ else if (sdlcAliases) errors.push(...(await sdlcAliases.verifyCommandAliases(sdlc)));
406
+ else errors.push({ code: 'aliases.sdlc-validator-unavailable', message: 'The installed SDLC package does not publish its alias validator.' });
407
+ const projection = commandAliasesProjection();
408
+ const report = { ...projection, status: errors.length ? 'invalid' : 'valid', valid: errors.length === 0, errors };
409
+ if (format === 'json') console.log(JSON.stringify(report, null, 2));
410
+ else {
411
+ console.log(`Lighthouse command aliases: ${report.status}`);
412
+ for (const alias of report.aliases) console.log(` ${alias.alias} -> ${alias.canonical}`);
413
+ for (const error of report.errors) console.error(` ${error.code}: ${error.message}`);
414
+ }
415
+ if (errors.length) process.exitCode = 1;
416
+ }
417
+
312
418
  function managedFiles(manifest) {
313
419
  const files = {
314
420
  [projection]: render(manifest),
@@ -316,10 +422,12 @@ function managedFiles(manifest) {
316
422
  '.husky/commit-msg': commitHook(),
317
423
  '.husky/pre-push': prePushHook(),
318
424
  '.github/workflows/lighthouse.yml': workflow(),
425
+ '.agents/skills/lighthouse-init/SKILL.md': readFileSync(new URL('../assets/skills/init/SKILL.md', import.meta.url), 'utf8'),
319
426
  };
320
427
  Object.assign(files, profileAssets(manifest));
321
428
  const controlPlane = sdlcControlPlane(manifest);
322
429
  if (controlPlane) files['.lighthouse/sdlc.json'] = controlPlane;
430
+ files['.lighthouse/commands.json'] = commandAliasProjectionText(commandAliasesProjection());
323
431
  return files;
324
432
  }
325
433
 
@@ -541,7 +649,13 @@ function validate(manifest) {
541
649
  const previousCompleteManaged = Array.isArray(manifest.managed)
542
650
  && manifest.managed.length === previousManagedPaths.length
543
651
  && manifest.managed.every((entry, index) => entry === previousManagedPaths[index]);
544
- if (!legacyManaged && !profilelessManaged && !previousCompleteManaged && !completeManaged) {
652
+ const preSemanticManaged = Array.isArray(manifest.managed)
653
+ && manifest.managed.length === preSemanticManagedPaths.length
654
+ && manifest.managed.every((entry, index) => entry === preSemanticManagedPaths[index]);
655
+ const preSemanticWithoutUpgrade = Array.isArray(manifest.managed)
656
+ && manifest.managed.length === preSemanticWithoutUpgradePaths.length
657
+ && manifest.managed.every((entry, index) => entry === preSemanticWithoutUpgradePaths[index]);
658
+ if (!legacyManaged && !profilelessManaged && !previousCompleteManaged && !completeManaged && !preSemanticManaged && !preSemanticWithoutUpgrade) {
545
659
  problems.push(`managed must be ${legacyManagedPaths.join(', ')}, the previous Lighthouse guardrail set or the selected profile set`);
546
660
  }
547
661
  if ('repository' in manifest) {
@@ -592,6 +706,103 @@ function productNameFromArgs(args) {
592
706
  return value;
593
707
  }
594
708
 
709
+ function valueFromArgs(args, name) {
710
+ const index = args.indexOf(name);
711
+ if (index !== -1) return args[index + 1];
712
+ return args.find((arg) => arg.startsWith(`${name}=`))?.slice(name.length + 1);
713
+ }
714
+
715
+ async function readInitPlan(planFile) {
716
+ let plan;
717
+ try { plan = JSON.parse(await readFile(resolve(root, planFile), 'utf8')); }
718
+ catch (error) { throw new Error(`Cannot read initialization plan ${planFile}: ${error.message}`); }
719
+ const problems = validateInitPlan(plan, supportedProfiles);
720
+ if (problems.length) throw new Error(`Invalid initialization plan:\n${problems.map((problem) => ` ${problem}`).join('\n')}`);
721
+ return plan;
722
+ }
723
+
724
+ function printInitializationPlan(plan, result, format) {
725
+ const report = {
726
+ schemaVersion: 1,
727
+ intent: plan.intent,
728
+ product: plan.product,
729
+ application: plan.application,
730
+ profiles: plan.profiles,
731
+ reference: plan.reference ?? null,
732
+ openQuestions: plan.openQuestions,
733
+ status: result.plan.conflicts.length || plan.openQuestions.length ? 'blocked' : 'ready',
734
+ manifest: {
735
+ product: result.manifest.product,
736
+ application: result.manifest.application,
737
+ profiles: result.manifest.profiles,
738
+ },
739
+ operations: result.plan.operations.map(({ path, action }) => ({ path, action })),
740
+ conflicts: result.plan.conflicts,
741
+ apply: 'lighthouse init --plan-file <path> --apply',
742
+ };
743
+ if (format === 'json') {
744
+ console.log(JSON.stringify(report, null, 2));
745
+ return;
746
+ }
747
+ console.log('Lighthouse semantic initialization plan:');
748
+ console.log(` product ${plan.product}`);
749
+ console.log(` intent ${plan.intent}`);
750
+ console.log(` application ${plan.application.shape} (${plan.application.stacks.join(', ') || 'no stack'})`);
751
+ console.log(` profiles ${plan.profiles.join(', ') || 'none'}`);
752
+ console.log(` reference ${plan.reference?.kind ?? 'none'}`);
753
+ if (plan.openQuestions.length) for (const question of plan.openQuestions) console.error(` question ${question}`);
754
+ for (const operation of result.plan.operations) console.log(` ${operation.action} ${operation.path}`);
755
+ for (const conflict of result.plan.conflicts) console.error(` conflict ${conflict}`);
756
+ console.log(' default is dry-run; apply only after review with --apply');
757
+ }
758
+
759
+ async function initializeFromPlan(args, planFile) {
760
+ const plan = await readInitPlan(planFile);
761
+ const format = formatFromArgs(args);
762
+ const apply = args.includes('--apply');
763
+ const dryRun = args.includes('--dry-run') || !apply;
764
+ if (args.includes('--apply') && args.includes('--dry-run')) throw new Error('--apply and --dry-run cannot be used together.');
765
+ if (format === 'json' && apply) throw new Error('--format json is available for semantic initialization dry runs; omit it when applying.');
766
+ const existing = await readManifest();
767
+ let manifest;
768
+ if (existing) {
769
+ const problems = validate(existing);
770
+ if (problems.length) throw new Error(problems.map((problem) => `Invalid lighthouse.json: ${problem}`).join('\n'));
771
+ if (existing.product !== plan.product) throw new Error(`Initialization plan targets ${plan.product}, but lighthouse.json belongs to ${existing.product}.`);
772
+ if (JSON.stringify(existing.profiles ?? []) !== JSON.stringify(plan.profiles)) throw new Error('Initialization plan profiles differ from lighthouse.json; resolve the selection before applying.');
773
+ if (JSON.stringify(normalizeApplication(existing.application)) !== JSON.stringify(plan.application)) throw new Error('Initialization plan application differs from lighthouse.json; resolve the selection before applying.');
774
+ manifest = existing;
775
+ } else {
776
+ manifest = manifestFromInitPlan(plan, defaultManifest);
777
+ }
778
+ const problems = validate(manifest);
779
+ if (problems.length) throw new Error(problems.map((problem) => `Invalid lighthouse.json: ${problem}`).join('\n'));
780
+ const composition = composeProfiles(readProfileCatalog(), manifest);
781
+ if (composition.status === 'failed') throw new Error(`Initialization plan has incompatible profiles:\n${composition.selected.flatMap((profile) => profile.problems).map((problem) => ` ${problem}`).join('\n')}`);
782
+ const initialization = { manifest, plan: await planInitialization(manifest, !existing) };
783
+ printInitializationPlan(plan, initialization, format);
784
+ if (initialization.plan.conflicts.length) {
785
+ if (dryRun) process.exitCode = 1;
786
+ else throw new Error('Initialization stopped; resolve the reported conflicts and run the plan again.');
787
+ return;
788
+ }
789
+ if (hasUnresolvedQuestions(plan)) {
790
+ if (dryRun) process.exitCode = 1;
791
+ else throw new Error('Initialization stopped; answer the open questions before using --apply.');
792
+ return;
793
+ }
794
+ if (dryRun) return;
795
+ if (!existing) await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, { flag: 'wx' });
796
+ for (const operation of initialization.plan.operations.filter((item) => item.path.endsWith('/'))) await mkdir(resolve(root, operation.path), { recursive: true });
797
+ const files = managedFiles(manifest);
798
+ for (const [file, content] of Object.entries(files)) {
799
+ await writeFile(resolve(root, file), content, { flag: 'wx', mode: file.startsWith('.husky/') ? 0o755 : 0o644 });
800
+ if (file.startsWith('.husky/')) await chmod(resolve(root, file), 0o755);
801
+ }
802
+ await writeUpgradeState(manifest, null, files);
803
+ console.log(`Initialized ${manifest.product} from the semantic plan. Run lighthouse verify.`);
804
+ }
805
+
595
806
  async function planInitialization(manifest, manifestMissing) {
596
807
  const operations = [];
597
808
  const conflicts = [];
@@ -649,6 +860,8 @@ function printReconciliationPlan(plan) {
649
860
  }
650
861
 
651
862
  async function initialize(args) {
863
+ const planFile = valueFromArgs(args, '--plan-file');
864
+ if (planFile) return initializeFromPlan(args, planFile);
652
865
  let manifest = await readManifest();
653
866
  const manifestMissing = !manifest;
654
867
  const selectedProfiles = profileArguments(args);
@@ -809,6 +1022,25 @@ const authorityCommands = [
809
1022
  },
810
1023
  ];
811
1024
 
1025
+ const authorityAdoptionCommands = [
1026
+ {
1027
+ id: 'productShape',
1028
+ label: 'ProductShape',
1029
+ packageName: '@prodshape/cli',
1030
+ command: 'prodshape',
1031
+ path: 'docs/product/model',
1032
+ validateArgs: ['validate', '--format', 'json'],
1033
+ },
1034
+ {
1035
+ id: 'arcShape',
1036
+ label: 'ArcShape',
1037
+ packageName: '@arcshape/cli',
1038
+ command: 'arcshape',
1039
+ path: 'docs/architecture',
1040
+ validateArgs: ['validate', '--docs', 'docs/architecture', '--links', '--citations', '--format', 'json'],
1041
+ },
1042
+ ];
1043
+
812
1044
  function providerExecutable(authority) {
813
1045
  const packageJson = packageRequire.resolve(`${authority.packageName}/package.json`);
814
1046
  const metadata = JSON.parse(readFileSync(packageJson, 'utf8'));
@@ -842,6 +1074,132 @@ function runAuthorityCommand(authority, dryRun) {
842
1074
  };
843
1075
  }
844
1076
 
1077
+ function parseProviderReport(output) {
1078
+ const text = output.trim();
1079
+ try { return JSON.parse(text); } catch { /* The provider may prefix JSON with a diagnostic line. */ }
1080
+ const start = text.indexOf('{');
1081
+ const end = text.lastIndexOf('}');
1082
+ if (start === -1 || end <= start) return null;
1083
+ try { return JSON.parse(text.slice(start, end + 1)); } catch { return null; }
1084
+ }
1085
+
1086
+ function authorityStatus(present, validation, recordedVersion, providerVersion) {
1087
+ if (!present) return 'absent';
1088
+ if (validation.status === 'unavailable') return 'unavailable';
1089
+ if (!validation.valid) return 'invalid';
1090
+ if (recordedVersion && recordedVersion !== providerVersion) return 'drifted';
1091
+ return recordedVersion ? 'adopted' : 'ready';
1092
+ }
1093
+
1094
+ async function runAuthorityValidation(authority, manifest) {
1095
+ const providerVersion = installedPackageVersion(authority.packageName, 'package.json');
1096
+ const present = await isDirectory(resolve(root, authority.path));
1097
+ const command = `${authority.command} ${authority.validateArgs.join(' ')}`;
1098
+ if (!present) {
1099
+ return {
1100
+ id: authority.id,
1101
+ label: authority.label,
1102
+ provider: authority.packageName,
1103
+ version: providerVersion,
1104
+ path: authority.path,
1105
+ command,
1106
+ recordedVersion: manifest.integrations[authority.id],
1107
+ status: 'absent',
1108
+ valid: false,
1109
+ diagnostics: [`${authority.path} is missing`],
1110
+ nextAction: `Create the ${authority.label} authority through its supported initializer, then rerun lighthouse authorities --adopt-existing.`,
1111
+ };
1112
+ }
1113
+ if (!providerVersion) {
1114
+ return {
1115
+ id: authority.id,
1116
+ label: authority.label,
1117
+ provider: authority.packageName,
1118
+ version: null,
1119
+ path: authority.path,
1120
+ command,
1121
+ recordedVersion: manifest.integrations[authority.id],
1122
+ status: 'unavailable',
1123
+ valid: false,
1124
+ diagnostics: [`${authority.packageName} is not installed`],
1125
+ nextAction: `Install ${authority.packageName}, then rerun lighthouse authorities --adopt-existing.`,
1126
+ };
1127
+ }
1128
+ let result;
1129
+ try {
1130
+ result = spawnSync(process.execPath, [providerExecutable(authority), ...authority.validateArgs], {
1131
+ cwd: root,
1132
+ encoding: 'utf8',
1133
+ stdio: ['pipe', 'pipe', 'pipe'],
1134
+ });
1135
+ } catch (error) {
1136
+ return {
1137
+ id: authority.id,
1138
+ label: authority.label,
1139
+ provider: authority.packageName,
1140
+ version: providerVersion,
1141
+ path: authority.path,
1142
+ command,
1143
+ recordedVersion: manifest.integrations[authority.id],
1144
+ status: 'unavailable',
1145
+ valid: false,
1146
+ diagnostics: [error.message],
1147
+ nextAction: `Run ${command} directly, repair the provider and rerun lighthouse authorities --adopt-existing.`,
1148
+ };
1149
+ }
1150
+ const output = `${result.stdout ?? ''}${result.stderr ?? ''}`;
1151
+ const report = parseProviderReport(output);
1152
+ const valid = result.status === 0 && report?.valid !== false;
1153
+ const status = authorityStatus(present, { status: result.status === null ? 'unavailable' : 'completed', valid }, manifest.integrations[authority.id], providerVersion);
1154
+ return {
1155
+ id: authority.id,
1156
+ label: authority.label,
1157
+ provider: authority.packageName,
1158
+ version: providerVersion,
1159
+ path: authority.path,
1160
+ command,
1161
+ recordedVersion: manifest.integrations[authority.id],
1162
+ status,
1163
+ valid,
1164
+ diagnostics: report?.diagnostics ?? (valid ? [] : [output.trim().split('\n').filter(Boolean).slice(-3).join(' ') || `${authority.label} validation failed`]),
1165
+ nextAction: status === 'invalid' ? `Run ${command} directly, repair the reported authority and rerun lighthouse authorities --adopt-existing.` : null,
1166
+ };
1167
+ }
1168
+
1169
+ async function authorityAdoptionPlan(manifest) {
1170
+ const authorities = await Promise.all(authorityAdoptionCommands.map((authority) => runAuthorityValidation(authority, manifest)));
1171
+ const blocking = authorities.filter((authority) => ['absent', 'invalid', 'unavailable'].includes(authority.status));
1172
+ const changed = authorities.some((authority) => ['ready', 'drifted'].includes(authority.status));
1173
+ const status = blocking.length ? 'blocked' : changed ? 'ready' : 'adopted';
1174
+ return {
1175
+ schemaVersion: 1,
1176
+ command: 'lighthouse authorities --adopt-existing',
1177
+ status,
1178
+ remoteWrites: 'none',
1179
+ providerWrites: 'none',
1180
+ authorities,
1181
+ operations: changed ? [
1182
+ { action: 'record', path: 'lighthouse.json', values: Object.fromEntries(authorities.map((authority) => [authority.id, authority.version])) },
1183
+ { action: 'regenerate', path: 'LIGHTHOUSE.md' },
1184
+ ] : [],
1185
+ apply: 'lighthouse authorities --adopt-existing --apply',
1186
+ };
1187
+ }
1188
+
1189
+ function printAuthorityAdoptionPlan(plan) {
1190
+ console.log(`Lighthouse existing-authority adoption plan: ${plan.status}`);
1191
+ console.log(' ProductShape and ArcShape provider files remain untouched.');
1192
+ console.log(' Remote writes: none.');
1193
+ for (const authority of plan.authorities) {
1194
+ const version = authority.version ?? 'unavailable';
1195
+ console.log(` ${authority.label}: ${authority.status} (${version}) ${authority.path}`);
1196
+ for (const diagnostic of authority.diagnostics) console.error(` ${diagnostic}`);
1197
+ if (authority.nextAction) console.error(` next: ${authority.nextAction}`);
1198
+ }
1199
+ for (const operation of plan.operations) console.log(` ${operation.action} ${operation.path}`);
1200
+ console.log(' default is dry-run; apply only after review with --apply');
1201
+ }
1202
+
845
1203
  async function writeManifestAtomically(manifest) {
846
1204
  const temporary = `${manifestPath}.tmp-${process.pid}`;
847
1205
  await writeFile(temporary, `${JSON.stringify(manifest, null, 2)}\n`, { flag: 'wx' });
@@ -862,7 +1220,7 @@ async function ensureProjectionReady(manifest) {
862
1220
 
863
1221
  async function writeProjection(manifest) {
864
1222
  const target = resolve(root, projection);
865
- await writeFile(target, render(manifest), { flag: 'w' });
1223
+ await writeTextAtomically(target, render(manifest));
866
1224
  }
867
1225
 
868
1226
  async function authorities(args) {
@@ -900,6 +1258,37 @@ async function authorities(args) {
900
1258
  console.log(`Recorded ProductShape ${authorityVersions.productShape} and ArcShape ${authorityVersions.arcShape} in lighthouse.json.`);
901
1259
  }
902
1260
 
1261
+ async function adoptExistingAuthorities(args) {
1262
+ const manifest = await readManifest();
1263
+ if (!manifest) throw new Error('Cannot read lighthouse.json: file does not exist; run lighthouse init --name <product>.');
1264
+ const problems = validate(manifest);
1265
+ if (problems.length) throw new Error(problems.map((problem) => `Invalid lighthouse.json: ${problem}`).join('\n'));
1266
+ const apply = args.includes('--apply');
1267
+ const dryRun = args.includes('--dry-run') || !apply;
1268
+ const format = formatFromArgs(args);
1269
+ if (args.includes('--apply') && args.includes('--dry-run')) throw new Error('--apply and --dry-run cannot be used together.');
1270
+ if (format === 'json' && apply) throw new Error('--format json is available for existing-authority adoption dry runs; omit it when applying.');
1271
+ const plan = await authorityAdoptionPlan(manifest);
1272
+ if (format === 'json') console.log(JSON.stringify(plan, null, 2));
1273
+ else printAuthorityAdoptionPlan(plan);
1274
+ if (dryRun) {
1275
+ if (plan.status === 'blocked') process.exitCode = 1;
1276
+ return;
1277
+ }
1278
+ if (plan.status === 'blocked') throw new Error('Existing-authority adoption stopped; resolve the reported provider conditions and rerun the plan.');
1279
+ await ensureProjectionReady(manifest);
1280
+ const updated = {
1281
+ ...manifest,
1282
+ integrations: Object.fromEntries(plan.authorities.map((authority) => [authority.id, authority.version])),
1283
+ };
1284
+ if (JSON.stringify(updated) !== JSON.stringify(manifest)) await writeManifestAtomically(updated);
1285
+ if (!await exists(resolve(root, projection))) await writeProjection(updated);
1286
+ else if (await readFile(resolve(root, projection), 'utf8') !== render(updated)) await writeProjection(updated);
1287
+ const verified = await authorityAdoptionPlan(updated);
1288
+ if (verified.status !== 'adopted') throw new Error('Existing-authority adoption was written but did not verify; rerun lighthouse authorities --adopt-existing and inspect the provider report.');
1289
+ console.log('Recorded and verified the existing ProductShape and ArcShape authorities. Provider-owned files were not changed.');
1290
+ }
1291
+
903
1292
  function gitBranch() {
904
1293
  try {
905
1294
  return execFileSync('git', ['branch', '--show-current'], {
@@ -949,6 +1338,40 @@ async function verify(manifest, format = 'text') {
949
1338
  const profileResult = profileCompositionResult(profileComposition);
950
1339
  report.results.push(profileResult);
951
1340
  report.summary[profileResult.status] = (report.summary[profileResult.status] ?? 0) + 1;
1341
+ if (validManifest) {
1342
+ const recordedAuthorities = [manifest.integrations.productShape, manifest.integrations.arcShape];
1343
+ const anyRecorded = recordedAuthorities.some((version) => version !== null);
1344
+ const authorityPlan = anyRecorded ? await authorityAdoptionPlan(manifest) : null;
1345
+ const authorityStatuses = authorityPlan?.authorities.map((authority) => authority.status) ?? [];
1346
+ const authorityStatus = !anyRecorded ? 'unconfigured'
1347
+ : authorityStatuses.includes('unavailable') ? 'unavailable'
1348
+ : authorityStatuses.some((status) => ['absent', 'invalid'].includes(status)) ? 'failed'
1349
+ : authorityStatuses.includes('drifted') ? 'drift'
1350
+ : 'passed';
1351
+ const authorityProblems = !anyRecorded
1352
+ ? ['ProductShape and ArcShape are not recorded; run lighthouse authorities --adopt-existing after reviewing its dry-run plan.']
1353
+ : authorityPlan.authorities.filter((authority) => authority.status !== 'adopted').flatMap((authority) => [
1354
+ `${authority.label} is ${authority.status}${authority.diagnostics.length ? `: ${authority.diagnostics.join('; ')}` : ''}`,
1355
+ ...(authority.nextAction ? [authority.nextAction] : []),
1356
+ ]);
1357
+ const authorityResult = {
1358
+ id: 'foundation.authorities',
1359
+ owner: 'lighthouse',
1360
+ level: anyRecorded ? 'required' : 'recommended',
1361
+ kind: 'hard',
1362
+ verification: 'deterministic',
1363
+ status: authorityStatus,
1364
+ evidence: authorityPlan?.authorities.map((authority) => ({ id: authority.id, path: authority.path, version: authority.version, recordedVersion: authority.recordedVersion, command: authority.command })) ?? [],
1365
+ problems: authorityProblems,
1366
+ nextAction: authorityProblems.length ? 'lighthouse authorities --adopt-existing' : null,
1367
+ };
1368
+ report.results.push(authorityResult);
1369
+ report.summary[authorityStatus] = (report.summary[authorityStatus] ?? 0) + 1;
1370
+ if (authorityResult.level === 'required' && !['passed', 'not-applicable'].includes(authorityStatus)) {
1371
+ report.blocking.push(authorityResult.id);
1372
+ report.problems.push(...authorityProblems);
1373
+ }
1374
+ }
952
1375
  const adoption = Object.values(capabilityAdoption(await readUpgradeState()));
953
1376
  for (const item of adoption) {
954
1377
  if (item.status !== 'adopted' || !item.recordPath || await exists(resolve(root, item.recordPath))) continue;
@@ -1167,7 +1590,8 @@ for (let index = 0; index < commandArgs.length; index += 1) {
1167
1590
  const arg = commandArgs[index];
1168
1591
  if (command === 'capabilities' && index === 0 && ['select', 'decline', 'plan', 'apply'].includes(arg)) continue;
1169
1592
  if (arg === '--dry-run') continue;
1170
- if (arg === '--apply') { if (!['authorities', 'reconcile', 'upgrade'].includes(command)) invalidArgument = true; continue; }
1593
+ if (arg === '--apply') { if (!['init', 'authorities', 'reconcile', 'upgrade'].includes(command)) invalidArgument = true; continue; }
1594
+ if (arg === '--adopt-existing') { if (command !== 'authorities') invalidArgument = true; continue; }
1171
1595
  if (arg === '--name') { if (!commandArgs[index + 1]) invalidArgument = true; else index += 1; continue; }
1172
1596
  if (arg.startsWith('--name=') && arg.length > '--name='.length) continue;
1173
1597
  if (arg === '--profile') {
@@ -1180,12 +1604,12 @@ for (let index = 0; index < commandArgs.length; index += 1) {
1180
1604
  continue;
1181
1605
  }
1182
1606
  if (arg === '--format') {
1183
- if (!['profiles', 'verify', 'upgrade', 'capabilities'].includes(command) || !['text', 'json'].includes(commandArgs[index + 1])) invalidArgument = true;
1607
+ if (!['init', 'aliases', 'profiles', 'authorities', 'verify', 'upgrade', 'capabilities'].includes(command) || !['text', 'json'].includes(commandArgs[index + 1])) invalidArgument = true;
1184
1608
  else index += 1;
1185
1609
  continue;
1186
1610
  }
1187
1611
  if (arg.startsWith('--format=') && ['text', 'json'].includes(arg.slice('--format='.length))) {
1188
- if (!['profiles', 'verify', 'upgrade', 'capabilities'].includes(command)) invalidArgument = true;
1612
+ if (!['init', 'aliases', 'profiles', 'authorities', 'verify', 'upgrade', 'capabilities'].includes(command)) invalidArgument = true;
1189
1613
  continue;
1190
1614
  }
1191
1615
  if (arg === '--scope') {
@@ -1198,19 +1622,24 @@ for (let index = 0; index < commandArgs.length; index += 1) {
1198
1622
  continue;
1199
1623
  }
1200
1624
  if (['--id', '--provider', '--repo', '--input', '--plan-file', '--authorize'].includes(arg)) {
1201
- if (!commandArgs[index + 1]) invalidArgument = true;
1625
+ if (!commandArgs[index + 1] || (arg === '--plan-file' && !['init', 'capabilities'].includes(command))) invalidArgument = true;
1202
1626
  else index += 1;
1203
1627
  continue;
1204
1628
  }
1205
- if (['--id=', '--provider=', '--repo=', '--input=', '--plan-file=', '--authorize='].some((prefix) => arg.startsWith(prefix) && arg.length > prefix.length)) continue;
1629
+ if (['--id=', '--provider=', '--repo=', '--input=', '--plan-file=', '--authorize='].some((prefix) => arg.startsWith(prefix) && arg.length > prefix.length)) {
1630
+ if (arg.startsWith('--plan-file=') && !['init', 'capabilities'].includes(command)) invalidArgument = true;
1631
+ continue;
1632
+ }
1206
1633
  invalidArgument = true;
1207
1634
  }
1208
- if (!['init', 'profiles', 'authorities', 'reconcile', 'upgrade', 'capabilities', 'verify'].includes(command) || invalidArgument) {
1635
+ if (!['init', 'aliases', 'profiles', 'authorities', 'reconcile', 'upgrade', 'capabilities', 'verify'].includes(command) || invalidArgument) {
1209
1636
  console.error(usage);
1210
1637
  process.exit(2);
1211
1638
  }
1212
1639
  try {
1213
1640
  if (command === 'init') await initialize(args.slice(1));
1641
+ else if (command === 'aliases') await commandAliases(formatFromArgs(commandArgs));
1642
+ else if (command === 'authorities' && commandArgs.includes('--adopt-existing')) await adoptExistingAuthorities(args.slice(1));
1214
1643
  else if (command === 'authorities') await authorities(args.slice(1));
1215
1644
  else if (command === 'reconcile') await reconcile(args.slice(1));
1216
1645
  else if (command === 'upgrade') await upgrade(args.slice(1));