@juangcarmona/lighthouse 0.3.0 → 0.4.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.
- package/README.md +3 -1
- package/assets/schemas/lighthouse-upgrade.schema.json +18 -0
- package/package.json +2 -2
- package/src/lighthouse.mjs +251 -4
- package/upgrades/0.4.0.md +7 -0
package/README.md
CHANGED
|
@@ -38,12 +38,14 @@ Initialization reports missing ProductShape and ArcShape authority inputs as ado
|
|
|
38
38
|
|
|
39
39
|
## Current command scope
|
|
40
40
|
|
|
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.
|
|
41
|
+
The command supports `init`, `authorities`, `reconcile`, `upgrade`, `capabilities` 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.
|
|
42
42
|
|
|
43
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.
|
|
44
44
|
|
|
45
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
46
|
|
|
47
|
+
The installed SDLC package also publishes a versioned capability catalog. `selected-by-default` capabilities appear as pending until the adopter selects or declines them; `opt-in` capabilities remain available without blocking `upgrade` or `verify`. `lighthouse capabilities select` and `lighthouse capabilities decline` record an explicit local choice, and `lighthouse verify --format json` exposes the adoption status and next command. For the GitHub labels slice, generate a local plan with `lighthouse capabilities plan --id labels --provider github --repo <owner/repo> --plan-file .lighthouse/reconciliation/labels.plan.json`, then apply it only with the plan hash returned by that command.
|
|
48
|
+
|
|
47
49
|
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.
|
|
48
50
|
|
|
49
51
|
## Guardrail catalog
|
|
@@ -16,6 +16,24 @@
|
|
|
16
16
|
},
|
|
17
17
|
"baseline": {"type": "string", "minLength": 1},
|
|
18
18
|
"capabilities": {"$ref": "#/$defs/versions"},
|
|
19
|
+
"adoption": {
|
|
20
|
+
"type": "object",
|
|
21
|
+
"additionalProperties": {
|
|
22
|
+
"type": "object",
|
|
23
|
+
"additionalProperties": false,
|
|
24
|
+
"required": ["id", "status", "provider", "version", "catalogVersion", "nextAction"],
|
|
25
|
+
"properties": {
|
|
26
|
+
"id": {"type": "string"},
|
|
27
|
+
"status": {"enum": ["available", "pending-adoption", "adopted", "declined", "drifted", "blocked", "failed", "unavailable"]},
|
|
28
|
+
"selection": {"enum": ["selected-by-default", "opt-in"]},
|
|
29
|
+
"provider": {"type": "string"},
|
|
30
|
+
"version": {"type": "string"},
|
|
31
|
+
"catalogVersion": {"type": "string"},
|
|
32
|
+
"nextAction": {"type": ["string", "null"]},
|
|
33
|
+
"recordPath": {"type": "string"}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
},
|
|
19
37
|
"projections": {"$ref": "#/$defs/versions"},
|
|
20
38
|
"lastAppliedMigration": {"type": "string", "minLength": 1},
|
|
21
39
|
"files": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@juangcarmona/lighthouse",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Lighthouse adoption and verification CLI",
|
|
6
6
|
"repository": {
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"@arcshape/cli": "0.1.3",
|
|
24
24
|
"@prodshape/cli": "0.22.0",
|
|
25
|
-
"@juangcarmona/lighthouse-sdlc": "0.
|
|
25
|
+
"@juangcarmona/lighthouse-sdlc": "0.5.0"
|
|
26
26
|
},
|
|
27
27
|
"files": [
|
|
28
28
|
"src/lighthouse.mjs",
|
package/src/lighthouse.mjs
CHANGED
|
@@ -6,6 +6,16 @@ import { createRequire } from 'node:module';
|
|
|
6
6
|
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
|
+
let sdlcCapabilities;
|
|
10
|
+
let sdlcReconcile;
|
|
11
|
+
let sdlcGithub;
|
|
12
|
+
try {
|
|
13
|
+
[sdlcCapabilities, sdlcReconcile, sdlcGithub] = await Promise.all([
|
|
14
|
+
import('@juangcarmona/lighthouse-sdlc/capabilities'),
|
|
15
|
+
import('@juangcarmona/lighthouse-sdlc/reconcile'),
|
|
16
|
+
import('@juangcarmona/lighthouse-sdlc/github'),
|
|
17
|
+
]);
|
|
18
|
+
} catch { /* Standalone generated starters can run without the optional SDLC package. */ }
|
|
9
19
|
|
|
10
20
|
const root = process.cwd();
|
|
11
21
|
const packageRequire = createRequire(import.meta.url);
|
|
@@ -40,7 +50,7 @@ const upgradeScopes = {
|
|
|
40
50
|
sdlc: ['.lighthouse/sdlc.json'],
|
|
41
51
|
};
|
|
42
52
|
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]';
|
|
53
|
+
const usage = 'Usage: lighthouse <init|authorities|reconcile|upgrade|capabilities|verify> [--name <product>] [--scope <harness|sdlc|all>] [--format <text|json>] [--dry-run] [--apply]';
|
|
44
54
|
|
|
45
55
|
const defaultRepository = () => ({
|
|
46
56
|
layout: [...layout],
|
|
@@ -96,6 +106,45 @@ function sdlcContract() {
|
|
|
96
106
|
}
|
|
97
107
|
}
|
|
98
108
|
|
|
109
|
+
function sdlcCapabilityCatalog() {
|
|
110
|
+
if (!sdlcCapabilities) return null;
|
|
111
|
+
try {
|
|
112
|
+
const path = packageRequire.resolve('@juangcarmona/lighthouse-sdlc/capabilities.json');
|
|
113
|
+
const catalog = JSON.parse(readFileSync(path, 'utf8'));
|
|
114
|
+
const errors = sdlcCapabilities.validateCapabilityCatalog(catalog);
|
|
115
|
+
if (errors.length) throw new Error(errors.map((error) => error.message).join('; '));
|
|
116
|
+
return catalog;
|
|
117
|
+
} catch (error) {
|
|
118
|
+
throw new Error(`Cannot read the installed SDLC capability catalog: ${error.message}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function capabilityAdoption(previous = null) {
|
|
123
|
+
const catalog = sdlcCapabilityCatalog();
|
|
124
|
+
if (!catalog) return {};
|
|
125
|
+
return Object.fromEntries(catalog.capabilities.map((capability) => {
|
|
126
|
+
const current = previous?.adoption?.[capability.id];
|
|
127
|
+
const status = current?.catalogVersion === catalog.package.version
|
|
128
|
+
? current.status
|
|
129
|
+
: sdlcCapabilities.initialAdoptionStatus?.(capability) ?? 'pending-adoption';
|
|
130
|
+
const nextAction = status === 'available'
|
|
131
|
+
? `lighthouse capabilities select --id ${capability.id} --provider ${capability.provider}`
|
|
132
|
+
: ['adopted', 'declined'].includes(status)
|
|
133
|
+
? null
|
|
134
|
+
: `lighthouse capabilities plan --id ${capability.id} --provider ${capability.provider} --repo <owner/repo> --plan-file .lighthouse/reconciliation/${capability.id}.plan.json`;
|
|
135
|
+
return [capability.id, {
|
|
136
|
+
id: capability.id,
|
|
137
|
+
status,
|
|
138
|
+
selection: capability.selection,
|
|
139
|
+
provider: capability.provider,
|
|
140
|
+
version: capability.version,
|
|
141
|
+
catalogVersion: catalog.package.version,
|
|
142
|
+
nextAction,
|
|
143
|
+
...(current?.recordPath ? { recordPath: current.recordPath } : {}),
|
|
144
|
+
}];
|
|
145
|
+
}));
|
|
146
|
+
}
|
|
147
|
+
|
|
99
148
|
function sdlcControlPlane(manifest) {
|
|
100
149
|
const selected = normalizeSdlcPackage(manifest);
|
|
101
150
|
if (!selected) return null;
|
|
@@ -230,6 +279,7 @@ function upgradeState(manifest, files = managedFiles(manifest), previous = null)
|
|
|
230
279
|
package: { name: cliPackage.name, version: cliPackage.version },
|
|
231
280
|
baseline: manifest.baseline,
|
|
232
281
|
capabilities: { harness: '1', sdlc: files['.lighthouse/sdlc.json'] ? '1' : null },
|
|
282
|
+
adoption: capabilityAdoption(previous),
|
|
233
283
|
projections: { harness: '1', sdlc: files['.lighthouse/sdlc.json'] ? '1' : null },
|
|
234
284
|
lastAppliedMigration: previous?.lastAppliedMigration ?? 'foundation-init-1',
|
|
235
285
|
files: entries,
|
|
@@ -311,6 +361,7 @@ async function planUpgrade(manifest, scopes) {
|
|
|
311
361
|
package: state?.package ?? null,
|
|
312
362
|
baseline: state?.baseline ?? null,
|
|
313
363
|
capabilities: state?.capabilities ?? null,
|
|
364
|
+
adoption: state?.adoption ?? null,
|
|
314
365
|
projections: state?.projections ?? null,
|
|
315
366
|
lastAppliedMigration: state?.lastAppliedMigration ?? null,
|
|
316
367
|
},
|
|
@@ -318,6 +369,7 @@ async function planUpgrade(manifest, scopes) {
|
|
|
318
369
|
package: { name: cliPackage.name, version: cliPackage.version },
|
|
319
370
|
baseline: manifest.baseline,
|
|
320
371
|
capabilities: { harness: '1', sdlc: files['.lighthouse/sdlc.json'] ? '1' : null },
|
|
372
|
+
adoption: capabilityAdoption(state),
|
|
321
373
|
projections: { harness: '1', sdlc: files['.lighthouse/sdlc.json'] ? '1' : null },
|
|
322
374
|
},
|
|
323
375
|
scopes: [...scopes],
|
|
@@ -325,6 +377,11 @@ async function planUpgrade(manifest, scopes) {
|
|
|
325
377
|
changes,
|
|
326
378
|
verification: ['lighthouse verify'],
|
|
327
379
|
recovery: 'Restore the pre-update commit or rerun lighthouse upgrade after resolving the reported ownership conflict.',
|
|
380
|
+
adoption: {
|
|
381
|
+
status: Object.values(capabilityAdoption(state)).some((item) => !['adopted', 'declined', 'available'].includes(item.status)) ? 'pending' : 'complete',
|
|
382
|
+
capabilities: capabilityAdoption(state),
|
|
383
|
+
externalWrites: 'none',
|
|
384
|
+
},
|
|
328
385
|
problems,
|
|
329
386
|
};
|
|
330
387
|
}
|
|
@@ -336,6 +393,12 @@ function printUpgradePlan(plan) {
|
|
|
336
393
|
console.log(` scopes ${plan.scopes.join(', ')}`);
|
|
337
394
|
for (const prerequisite of plan.prerequisites) console.log(` prerequisite ${prerequisite.status}: ${prerequisite.message}`);
|
|
338
395
|
for (const change of plan.changes) console.log(` ${change.classification} ${change.action} ${change.path}${change.reason ? ` (${change.reason})` : ''}`);
|
|
396
|
+
if (plan.adoption) {
|
|
397
|
+
console.log(` capability adoption ${plan.adoption.status} (external writes: ${plan.adoption.externalWrites})`);
|
|
398
|
+
for (const capability of Object.values(plan.adoption.capabilities)) {
|
|
399
|
+
console.log(` capability ${capability.id ?? capability.provider} ${capability.status}${capability.nextAction ? `; next: ${capability.nextAction}` : ''}`);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
339
402
|
if (plan.problems.length) for (const problem of plan.problems) console.error(` adoption gap ${problem}`);
|
|
340
403
|
console.log(` verify ${plan.verification[0]}`);
|
|
341
404
|
console.log(` recovery ${plan.recovery}`);
|
|
@@ -787,6 +850,30 @@ async function verify(manifest, format = 'text') {
|
|
|
787
850
|
validateGitConventions,
|
|
788
851
|
branch: gitBranch(),
|
|
789
852
|
});
|
|
853
|
+
const adoption = Object.values(capabilityAdoption(await readUpgradeState()));
|
|
854
|
+
for (const item of adoption) {
|
|
855
|
+
if (item.status !== 'adopted' || !item.recordPath || await exists(resolve(root, item.recordPath))) continue;
|
|
856
|
+
item.status = 'failed';
|
|
857
|
+
item.nextAction = `lighthouse capabilities plan --id ${item.id} --provider ${item.provider} --repo <owner/repo> --plan-file .lighthouse/reconciliation/${item.id}.plan.json`;
|
|
858
|
+
}
|
|
859
|
+
if (adoption.length) {
|
|
860
|
+
const pending = adoption.filter((item) => !['adopted', 'declined', 'available'].includes(item.status));
|
|
861
|
+
const available = adoption.some((item) => item.status === 'available');
|
|
862
|
+
const declined = adoption.some((item) => item.status === 'declined');
|
|
863
|
+
report.results.push({
|
|
864
|
+
id: 'foundation.capability-adoption',
|
|
865
|
+
owner: '@juangcarmona/lighthouse-sdlc',
|
|
866
|
+
level: 'recommended',
|
|
867
|
+
kind: 'hard',
|
|
868
|
+
verification: 'deterministic',
|
|
869
|
+
status: pending.length ? 'unconfigured' : available || declined ? 'not-applicable' : 'passed',
|
|
870
|
+
adoptionStatus: pending.length ? pending[0].status : available ? 'available' : declined ? 'declined' : 'adopted',
|
|
871
|
+
evidence: adoption.map((item) => ({ capability: item.id, catalogVersion: item.catalogVersion })),
|
|
872
|
+
problems: pending.map((item) => `${item.id} is ${item.status}; run ${item.nextAction}`),
|
|
873
|
+
nextAction: pending[0]?.nextAction ?? null,
|
|
874
|
+
});
|
|
875
|
+
report.summary.unconfigured = (report.summary.unconfigured ?? 0) + (pending.length ? 1 : 0);
|
|
876
|
+
}
|
|
790
877
|
if (format === 'json') {
|
|
791
878
|
console.log(JSON.stringify(report, null, 2));
|
|
792
879
|
if (report.problems.length) process.exitCode = 1;
|
|
@@ -796,6 +883,158 @@ async function verify(manifest, format = 'text') {
|
|
|
796
883
|
console.log('Lighthouse manifest, repository boundaries and managed projection are valid.');
|
|
797
884
|
}
|
|
798
885
|
|
|
886
|
+
function planHash(plan) {
|
|
887
|
+
const { authorization, ...payload } = plan;
|
|
888
|
+
return contentHash(JSON.stringify(payload));
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
function capabilityPlanText(plan) {
|
|
892
|
+
const lines = [
|
|
893
|
+
`Capability adoption: ${plan.status}`,
|
|
894
|
+
`Capability: ${plan.capability.id}@${plan.capability.version} (${plan.capability.provider})`,
|
|
895
|
+
`Repository: ${plan.repository.repo}`,
|
|
896
|
+
`Remote writes: none`,
|
|
897
|
+
`Authorization: ${plan.authorization.planHash}`,
|
|
898
|
+
];
|
|
899
|
+
for (const change of plan.reconciliation.changes) lines.push(` ${change.status} ${change.name}`);
|
|
900
|
+
if (plan.nextAction) lines.push(`Next: ${plan.nextAction}`);
|
|
901
|
+
return `${lines.join('\n')}\n`;
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
async function writeCapabilityPlan(path, plan) {
|
|
905
|
+
const target = resolve(root, path);
|
|
906
|
+
await mkdir(dirname(target), { recursive: true });
|
|
907
|
+
await writeTextAtomically(target, `${JSON.stringify(plan, null, 2)}\n`);
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
async function writeCapabilityEvidence(plan, status, extra = {}) {
|
|
911
|
+
const path = resolve(root, '.lighthouse/reconciliation', `${plan.capability.id}.json`);
|
|
912
|
+
await mkdir(dirname(path), { recursive: true });
|
|
913
|
+
const evidence = {
|
|
914
|
+
schemaVersion: 1,
|
|
915
|
+
capability: plan.capability,
|
|
916
|
+
catalogVersion: plan.catalogVersion,
|
|
917
|
+
provider: plan.repository,
|
|
918
|
+
desiredHash: plan.reconciliation.desiredHash,
|
|
919
|
+
observedHash: extra.observedHash ?? plan.reconciliation.observedHash,
|
|
920
|
+
targetHash: extra.targetHash ?? plan.reconciliation.targetHash,
|
|
921
|
+
operations: extra.operations ?? plan.reconciliation.operations,
|
|
922
|
+
operationPolicy: plan.reconciliation.operationPolicy ?? plan.operationPolicy ?? null,
|
|
923
|
+
blockedOperations: extra.blockedOperations ?? plan.reconciliation.blockedOperations ?? [],
|
|
924
|
+
conflicts: extra.conflicts ?? plan.reconciliation.changes.filter((change) => change.status === 'conflict'),
|
|
925
|
+
status,
|
|
926
|
+
verified: status === 'adopted',
|
|
927
|
+
};
|
|
928
|
+
await writeTextAtomically(path, `${JSON.stringify(evidence, null, 2)}\n`);
|
|
929
|
+
return path;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
async function capabilities(args) {
|
|
933
|
+
const action = args[0];
|
|
934
|
+
const id = args.includes('--id') ? args[args.indexOf('--id') + 1] : args.find((arg) => arg.startsWith('--id='))?.slice(5) ?? 'labels';
|
|
935
|
+
const provider = args.includes('--provider') ? args[args.indexOf('--provider') + 1] : args.find((arg) => arg.startsWith('--provider='))?.slice(11) ?? 'github';
|
|
936
|
+
const repo = args.includes('--repo') ? args[args.indexOf('--repo') + 1] : args.find((arg) => arg.startsWith('--repo='))?.slice(7);
|
|
937
|
+
const inputPath = args.includes('--input') ? args[args.indexOf('--input') + 1] : args.find((arg) => arg.startsWith('--input='))?.slice(8);
|
|
938
|
+
const format = formatFromArgs(args);
|
|
939
|
+
const planFile = args.includes('--plan-file') ? args[args.indexOf('--plan-file') + 1] : args.find((arg) => arg.startsWith('--plan-file='))?.slice(12);
|
|
940
|
+
if (['select', 'decline'].includes(action)) {
|
|
941
|
+
const catalog = sdlcCapabilityCatalog();
|
|
942
|
+
if (!catalog) throw new Error('The installed SDLC package does not publish the capability adoption contract; upgrade it before selecting capabilities.');
|
|
943
|
+
const capability = catalog.capabilities.find((item) => item.id === id && item.provider === provider);
|
|
944
|
+
if (!capability) throw new Error(`Capability ${id} for provider ${provider} is not declared by the installed SDLC package.`);
|
|
945
|
+
const state = await readUpgradeState();
|
|
946
|
+
if (!state) throw new Error('Cannot select a capability before Lighthouse ownership evidence exists; run lighthouse init or reconcile --apply.');
|
|
947
|
+
state.adoption = state.adoption ?? {};
|
|
948
|
+
const status = action === 'decline' ? 'declined' : 'pending-adoption';
|
|
949
|
+
state.adoption[id] = {
|
|
950
|
+
id,
|
|
951
|
+
status,
|
|
952
|
+
selection: capability.selection,
|
|
953
|
+
provider: capability.provider,
|
|
954
|
+
version: capability.version,
|
|
955
|
+
catalogVersion: catalog.package.version,
|
|
956
|
+
nextAction: status === 'declined' ? null : `lighthouse capabilities plan --id ${id} --provider ${provider} --repo <owner/repo> --plan-file .lighthouse/reconciliation/${id}.plan.json`,
|
|
957
|
+
};
|
|
958
|
+
await writeTextAtomically(upgradeStatePath, `${JSON.stringify(state, null, 2)}\n`);
|
|
959
|
+
const result = { schemaVersion: 1, capability: id, provider, status, applied: true };
|
|
960
|
+
if (format === 'json') console.log(JSON.stringify(result, null, 2)); else console.log(`Capability ${id}: ${status}.`);
|
|
961
|
+
return;
|
|
962
|
+
}
|
|
963
|
+
if (action === 'plan') {
|
|
964
|
+
if (!repo || !planFile) throw new Error('Usage: lighthouse capabilities plan --id <capability> --provider github --repo <owner/repo> --plan-file <path> [--input <capabilities.json>] [--format <text|json>]');
|
|
965
|
+
const catalog = sdlcCapabilityCatalog();
|
|
966
|
+
if (!catalog || !sdlcCapabilities || !sdlcReconcile || !sdlcGithub) throw new Error('The installed SDLC package does not publish the capability adoption contract; upgrade it before planning adoption.');
|
|
967
|
+
const capability = catalog.capabilities.find((item) => item.id === id && item.provider === provider);
|
|
968
|
+
if (!capability) throw new Error(`Capability ${id} for provider ${provider} is not declared by the installed SDLC package.`);
|
|
969
|
+
const declaration = inputPath ? JSON.parse(await readFile(resolve(root, inputPath), 'utf8')) : sdlcCapabilities.declarationFromCatalog(catalog, id);
|
|
970
|
+
const labels = await sdlcGithub.readGithubLabels(repo);
|
|
971
|
+
const reconciliation = sdlcReconcile.buildLabelReconciliationPlan(declaration, labels, await (async () => {
|
|
972
|
+
const path = resolve(root, '.lighthouse/reconciliation', `${id}.json`);
|
|
973
|
+
try { return JSON.parse(await readFile(path, 'utf8')); } catch { return null; }
|
|
974
|
+
})());
|
|
975
|
+
const plan = {
|
|
976
|
+
schemaVersion: 1,
|
|
977
|
+
status: reconciliation.status,
|
|
978
|
+
capability: { id: capability.id, provider: capability.provider, selection: capability.selection, version: capability.version },
|
|
979
|
+
catalogVersion: catalog.package.version,
|
|
980
|
+
operationPolicy: capability.operations,
|
|
981
|
+
repository: { provider, repo },
|
|
982
|
+
declaration,
|
|
983
|
+
reconciliation,
|
|
984
|
+
authorization: { required: true, planHash: '' },
|
|
985
|
+
nextAction: reconciliation.canApply ? `lighthouse capabilities apply --plan-file ${planFile} --authorize <plan-hash>` : 'Resolve the reported conflicts before authorizing this plan.',
|
|
986
|
+
};
|
|
987
|
+
plan.authorization.planHash = planHash(plan);
|
|
988
|
+
await writeCapabilityPlan(planFile, plan);
|
|
989
|
+
if (format === 'json') console.log(JSON.stringify(plan, null, 2)); else process.stdout.write(capabilityPlanText(plan));
|
|
990
|
+
if (!reconciliation.valid || !reconciliation.canApply) process.exitCode = 1;
|
|
991
|
+
return;
|
|
992
|
+
}
|
|
993
|
+
if (action === 'apply') {
|
|
994
|
+
const authorization = args.includes('--authorize') ? args[args.indexOf('--authorize') + 1] : args.find((arg) => arg.startsWith('--authorize='))?.slice(12);
|
|
995
|
+
if (!planFile || !authorization) throw new Error('Usage: lighthouse capabilities apply --plan-file <path> --authorize <plan-hash> [--format <text|json>]');
|
|
996
|
+
const plan = JSON.parse(await readFile(resolve(root, planFile), 'utf8'));
|
|
997
|
+
if (authorization !== plan.authorization?.planHash || planHash(plan) !== authorization) throw new Error('Capability plan authorization does not match the plan file; generate a new plan.');
|
|
998
|
+
if (!plan.reconciliation?.canApply) throw new Error('Capability plan is blocked; resolve its conflicts and generate a new plan.');
|
|
999
|
+
if (!sdlcReconcile || !sdlcGithub) throw new Error('The installed SDLC package does not publish the capability adoption contract; upgrade it before applying adoption.');
|
|
1000
|
+
const current = await sdlcGithub.readGithubLabels(plan.repository.repo);
|
|
1001
|
+
let reconciliation = sdlcReconcile.buildLabelReconciliationPlan(plan.declaration, current, plan.reconciliation.record);
|
|
1002
|
+
const originalOperations = new Set(plan.reconciliation.operations.map((operation) => operation.name));
|
|
1003
|
+
const plannedTarget = new Map((plan.reconciliation.target ?? []).map((label) => [label.name, JSON.stringify(label)]));
|
|
1004
|
+
const outsidePlanDrift = current.some((label) => !plannedTarget.has(label.name) || (plannedTarget.has(label.name) && !originalOperations.has(label.name) && JSON.stringify(label) !== plannedTarget.get(label.name)));
|
|
1005
|
+
if (reconciliation.observedHash !== plan.reconciliation.observedHash && (outsidePlanDrift || reconciliation.operations.some((operation) => !originalOperations.has(operation.name)))) {
|
|
1006
|
+
throw new Error('GitHub labels changed outside this plan; refusing to apply. Generate a new plan.');
|
|
1007
|
+
}
|
|
1008
|
+
const completed = [];
|
|
1009
|
+
try {
|
|
1010
|
+
for (const operation of reconciliation.operations) {
|
|
1011
|
+
await sdlcGithub.createGithubLabel(plan.repository.repo, operation, { dryRun: false });
|
|
1012
|
+
completed.push(operation);
|
|
1013
|
+
}
|
|
1014
|
+
} catch (error) {
|
|
1015
|
+
await writeCapabilityEvidence(plan, 'failed', { operations: completed, observedHash: reconciliation.observedHash });
|
|
1016
|
+
throw new Error(`Capability adoption stopped after ${completed.length} operation(s): ${error.message}`);
|
|
1017
|
+
}
|
|
1018
|
+
const finalLabels = await sdlcGithub.readGithubLabels(plan.repository.repo);
|
|
1019
|
+
const final = sdlcReconcile.buildLabelReconciliationPlan(plan.declaration, finalLabels, plan.reconciliation.record);
|
|
1020
|
+
if (!final.valid || final.operations.length || final.changes.some((change) => change.status === 'conflict')) {
|
|
1021
|
+
await writeCapabilityEvidence(plan, 'blocked', { operations: completed, observedHash: final.observedHash, targetHash: final.targetHash, conflicts: final.changes.filter((change) => change.status === 'conflict') });
|
|
1022
|
+
throw new Error('Capability adoption did not reach the declared state; inspect the evidence and generate a new plan.');
|
|
1023
|
+
}
|
|
1024
|
+
const evidencePath = await writeCapabilityEvidence(plan, 'adopted', { operations: completed, observedHash: final.observedHash, targetHash: final.targetHash });
|
|
1025
|
+
const state = await readUpgradeState();
|
|
1026
|
+
if (state) {
|
|
1027
|
+
state.adoption = state.adoption ?? {};
|
|
1028
|
+
state.adoption[id] = { id, status: 'adopted', selection: plan.capability.selection, provider: plan.capability.provider, version: plan.capability.version, catalogVersion: plan.catalogVersion, nextAction: null, recordPath: '.lighthouse/reconciliation/labels.json' };
|
|
1029
|
+
await writeTextAtomically(upgradeStatePath, `${JSON.stringify(state, null, 2)}\n`);
|
|
1030
|
+
}
|
|
1031
|
+
const result = { ...plan, status: 'adopted', evidencePath, reconciliation: final, applied: true };
|
|
1032
|
+
if (format === 'json') console.log(JSON.stringify(result, null, 2)); else process.stdout.write(capabilityPlanText(result));
|
|
1033
|
+
return;
|
|
1034
|
+
}
|
|
1035
|
+
throw new Error('Usage: lighthouse capabilities <select|decline|plan|apply> ...');
|
|
1036
|
+
}
|
|
1037
|
+
|
|
799
1038
|
function formatFromArgs(args) {
|
|
800
1039
|
const index = args.indexOf('--format');
|
|
801
1040
|
if (index !== -1) return args[index + 1];
|
|
@@ -808,17 +1047,18 @@ const commandArgs = args.slice(1);
|
|
|
808
1047
|
let invalidArgument = false;
|
|
809
1048
|
for (let index = 0; index < commandArgs.length; index += 1) {
|
|
810
1049
|
const arg = commandArgs[index];
|
|
1050
|
+
if (command === 'capabilities' && index === 0 && ['select', 'decline', 'plan', 'apply'].includes(arg)) continue;
|
|
811
1051
|
if (arg === '--dry-run') continue;
|
|
812
1052
|
if (arg === '--apply') { if (!['authorities', 'reconcile', 'upgrade'].includes(command)) invalidArgument = true; continue; }
|
|
813
1053
|
if (arg === '--name') { if (!commandArgs[index + 1]) invalidArgument = true; else index += 1; continue; }
|
|
814
1054
|
if (arg.startsWith('--name=') && arg.length > '--name='.length) continue;
|
|
815
1055
|
if (arg === '--format') {
|
|
816
|
-
if (!['verify', 'upgrade'].includes(command) || !['text', 'json'].includes(commandArgs[index + 1])) invalidArgument = true;
|
|
1056
|
+
if (!['verify', 'upgrade', 'capabilities'].includes(command) || !['text', 'json'].includes(commandArgs[index + 1])) invalidArgument = true;
|
|
817
1057
|
else index += 1;
|
|
818
1058
|
continue;
|
|
819
1059
|
}
|
|
820
1060
|
if (arg.startsWith('--format=') && ['text', 'json'].includes(arg.slice('--format='.length))) {
|
|
821
|
-
if (!['verify', 'upgrade'].includes(command)) invalidArgument = true;
|
|
1061
|
+
if (!['verify', 'upgrade', 'capabilities'].includes(command)) invalidArgument = true;
|
|
822
1062
|
continue;
|
|
823
1063
|
}
|
|
824
1064
|
if (arg === '--scope') {
|
|
@@ -830,9 +1070,15 @@ for (let index = 0; index < commandArgs.length; index += 1) {
|
|
|
830
1070
|
if (command !== 'upgrade') invalidArgument = true;
|
|
831
1071
|
continue;
|
|
832
1072
|
}
|
|
1073
|
+
if (['--id', '--provider', '--repo', '--input', '--plan-file', '--authorize'].includes(arg)) {
|
|
1074
|
+
if (!commandArgs[index + 1]) invalidArgument = true;
|
|
1075
|
+
else index += 1;
|
|
1076
|
+
continue;
|
|
1077
|
+
}
|
|
1078
|
+
if (['--id=', '--provider=', '--repo=', '--input=', '--plan-file=', '--authorize='].some((prefix) => arg.startsWith(prefix) && arg.length > prefix.length)) continue;
|
|
833
1079
|
invalidArgument = true;
|
|
834
1080
|
}
|
|
835
|
-
if (!['init', 'authorities', 'reconcile', 'upgrade', 'verify'].includes(command) || invalidArgument) {
|
|
1081
|
+
if (!['init', 'authorities', 'reconcile', 'upgrade', 'capabilities', 'verify'].includes(command) || invalidArgument) {
|
|
836
1082
|
console.error(usage);
|
|
837
1083
|
process.exit(2);
|
|
838
1084
|
}
|
|
@@ -841,6 +1087,7 @@ try {
|
|
|
841
1087
|
else if (command === 'authorities') await authorities(args.slice(1));
|
|
842
1088
|
else if (command === 'reconcile') await reconcile(args.slice(1));
|
|
843
1089
|
else if (command === 'upgrade') await upgrade(args.slice(1));
|
|
1090
|
+
else if (command === 'capabilities') await capabilities(args.slice(1));
|
|
844
1091
|
else {
|
|
845
1092
|
const manifest = await readManifest();
|
|
846
1093
|
if (!manifest) throw new Error('Cannot read lighthouse.json: file does not exist; run lighthouse init --name <product>.');
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Lighthouse 0.4.0
|
|
2
|
+
|
|
3
|
+
This release discovers the installed SDLC capability catalog during `lighthouse upgrade` and records pending adoption in `.lighthouse/upgrade.json`.
|
|
4
|
+
|
|
5
|
+
The upgrade remains local and dry-run by default. Review the plan, generate a provider plan with `lighthouse capabilities plan`, then authorize its application explicitly with the returned plan hash.
|
|
6
|
+
|
|
7
|
+
No package installation, local upgrade or implicit hook writes to GitHub.
|