@juangcarmona/lighthouse 0.3.1 → 0.5.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 +5 -3
- package/assets/guardrails/catalog.json +10 -0
- package/assets/schemas/lighthouse-manifest.schema.json +3 -2
- package/assets/schemas/lighthouse-upgrade.schema.json +18 -0
- package/package.json +3 -2
- package/src/guardrails.mjs +17 -2
- package/src/lighthouse.mjs +352 -23
- package/upgrades/0.4.0.md +7 -0
package/README.md
CHANGED
|
@@ -4,13 +4,13 @@ This package is the core Lighthouse harness. It provides the `lighthouse` comman
|
|
|
4
4
|
|
|
5
5
|
## Initialize a repository
|
|
6
6
|
|
|
7
|
-
Install the exact Lighthouse package artifact selected for the release and run `lighthouse init --name <product>` in a new repository. Lighthouse creates the manifest, the `src/`, `docs/`, `infra/` and `scripts/` boundaries, the repository guardrails and the generated `LIGHTHOUSE.md`. Inspect the plan and projection, then run `lighthouse verify` before adding project-specific policy or exceptions.
|
|
7
|
+
Install the exact Lighthouse package artifact selected for the release and run `lighthouse init --name <product>` in a new repository. Add `--profile visual-theme` when the project wants the Lighthouse UI contract. Lighthouse creates the manifest, the `src/`, `docs/`, `infra/` and `scripts/` boundaries, the repository guardrails and the generated `LIGHTHOUSE.md`. Inspect the plan and projection, then run `lighthouse verify` before adding project-specific policy or exceptions.
|
|
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
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
12
|
|
|
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
|
|
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, `DESIGN.md` and its agent skill. Selecting `visual-theme` projects the guide and skill into the adopter and enables its deterministic guardrail. Stack profiles own their source templates, guidance and checks.
|
|
14
14
|
|
|
15
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.
|
|
16
16
|
|
|
@@ -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
|
|
@@ -41,6 +41,16 @@
|
|
|
41
41
|
"verification": "deterministic",
|
|
42
42
|
"check": "branch",
|
|
43
43
|
"failureHint": "Use the branch pattern declared in lighthouse.json."
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"id": "visual.design-contract",
|
|
47
|
+
"owner": "@juangcarmona/lighthouse-ui",
|
|
48
|
+
"appliesTo": { "baseline": false, "profiles": ["visual-theme"] },
|
|
49
|
+
"level": "required",
|
|
50
|
+
"kind": "hard",
|
|
51
|
+
"verification": "deterministic",
|
|
52
|
+
"check": "ui-design",
|
|
53
|
+
"failureHint": "Run lighthouse reconcile after reviewing the visual-theme profile plan, then run lighthouse verify again."
|
|
44
54
|
}
|
|
45
55
|
]
|
|
46
56
|
}
|
|
@@ -12,12 +12,13 @@
|
|
|
12
12
|
"product": {"type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9 ._-]{0,63}$"},
|
|
13
13
|
"profiles": {"type": "array", "items": {"enum": ["node-tooling", "dotnet-backend", "visual-theme"]}, "uniqueItems": true},
|
|
14
14
|
"integrations": {"type": "object", "additionalProperties": false, "required": ["productShape", "arcShape"], "properties": {"productShape": {"$ref": "#/$defs/pin"}, "arcShape": {"$ref": "#/$defs/pin"}}},
|
|
15
|
-
"packages": {"type": "object", "additionalProperties": false, "required": ["sdlc"], "properties": {"sdlc": {"type": "object", "additionalProperties": false, "required": ["name", "version"], "properties": {"name": {"const": "@juangcarmona/lighthouse-sdlc"}, "version": {"$ref": "#/$defs/pin"}}}}},
|
|
15
|
+
"packages": {"type": "object", "additionalProperties": false, "required": ["sdlc"], "properties": {"sdlc": {"type": "object", "additionalProperties": false, "required": ["name", "version"], "properties": {"name": {"const": "@juangcarmona/lighthouse-sdlc"}, "version": {"$ref": "#/$defs/pin"}}}, "ui": {"type": "object", "additionalProperties": false, "required": ["name", "version"], "properties": {"name": {"const": "@juangcarmona/lighthouse-ui"}, "version": {"$ref": "#/$defs/pin"}}}}},
|
|
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
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
|
+
{"const": ["LIGHTHOUSE.md", "scripts/lighthouse-guard.mjs", ".husky/commit-msg", ".husky/pre-push", ".github/workflows/lighthouse.yml", ".lighthouse/sdlc.json", ".lighthouse/upgrade.json"]},
|
|
21
|
+
{"const": ["LIGHTHOUSE.md", "scripts/lighthouse-guard.mjs", ".husky/commit-msg", ".husky/pre-push", ".github/workflows/lighthouse.yml", ".lighthouse/sdlc.json", ".lighthouse/upgrade.json", "docs/design/DESIGN.md", ".agents/skills/lighthouse-ui/SKILL.md"]}
|
|
21
22
|
]},
|
|
22
23
|
"repository": {
|
|
23
24
|
"type": "object",
|
|
@@ -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.5.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Lighthouse adoption and verification CLI",
|
|
6
6
|
"repository": {
|
|
@@ -21,8 +21,9 @@
|
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"@arcshape/cli": "0.1.3",
|
|
24
|
+
"@juangcarmona/lighthouse-ui": "0.2.0",
|
|
24
25
|
"@prodshape/cli": "0.22.0",
|
|
25
|
-
"@juangcarmona/lighthouse-sdlc": "0.
|
|
26
|
+
"@juangcarmona/lighthouse-sdlc": "0.5.0"
|
|
26
27
|
},
|
|
27
28
|
"files": [
|
|
28
29
|
"src/lighthouse.mjs",
|
package/src/guardrails.mjs
CHANGED
|
@@ -7,6 +7,7 @@ const allowedLevels = new Set(['required', 'recommended', 'optional']);
|
|
|
7
7
|
const allowedKinds = new Set(['hard', 'soft']);
|
|
8
8
|
const allowedVerification = new Set(['deterministic', 'semantic', 'human']);
|
|
9
9
|
const allowedStatuses = new Set(['passed', 'failed', 'drift', 'unavailable', 'unconfigured', 'pending-review', 'not-applicable']);
|
|
10
|
+
const requiredVisualTokens = ['--lh-canvas', '--lh-surface', '--lh-text', '--lh-action', '--lh-border', '--lh-focus'];
|
|
10
11
|
|
|
11
12
|
function descriptorProblems(descriptor) {
|
|
12
13
|
const problems = [];
|
|
@@ -102,16 +103,30 @@ async function runDeterministicCheck(descriptor, context) {
|
|
|
102
103
|
const problems = context.validateGitConventions(context.repository);
|
|
103
104
|
return result(descriptor, problems.length ? 'failed' : 'passed', [{ value: context.branch ?? '' }, { pattern: context.repository.conventions.branchPattern }], problems);
|
|
104
105
|
}
|
|
106
|
+
if (descriptor.check === 'ui-design') {
|
|
107
|
+
const expected = ['docs/design/DESIGN.md', '.agents/skills/lighthouse-ui/SKILL.md'];
|
|
108
|
+
const missing = [];
|
|
109
|
+
const drifted = [];
|
|
110
|
+
for (const path of expected) {
|
|
111
|
+
const target = context.resolve(path);
|
|
112
|
+
if (!context.managedFiles[path] || !(await context.exists(target))) missing.push(path);
|
|
113
|
+
else if (await context.readFile(target, 'utf8') !== context.managedFiles[path]) drifted.push(`${path} has drifted; regenerate it from the visual-theme profile.`);
|
|
114
|
+
}
|
|
115
|
+
const tokenProblems = requiredVisualTokens.filter((token) => !context.uiTokens?.includes(token)).map((token) => `${token} is missing from the installed Lighthouse UI token source.`);
|
|
116
|
+
const problems = [...missing.map((path) => `${path} is missing; run lighthouse reconcile after reviewing the visual-theme profile plan.`), ...drifted, ...tokenProblems];
|
|
117
|
+
const status = drifted.length ? 'drift' : problems.length ? 'failed' : 'passed';
|
|
118
|
+
return result(descriptor, status, [...expected.map((path) => ({ path })), ...requiredVisualTokens.map((token) => ({ token }))], problems);
|
|
119
|
+
}
|
|
105
120
|
return result(descriptor, 'unavailable', [], [`No deterministic checker is registered for ${descriptor.id}.`]);
|
|
106
121
|
}
|
|
107
122
|
|
|
108
|
-
export async function evaluateGuardrails({ catalog, manifest, manifestProblems, repository, managedFiles, resolve, exists, isDirectory, readFile, validateGitConventions, branch }) {
|
|
123
|
+
export async function evaluateGuardrails({ catalog, manifest, manifestProblems, repository, managedFiles, resolve, exists, isDirectory, readFile, validateGitConventions, branch, uiTokens }) {
|
|
109
124
|
if (manifestProblems.length) {
|
|
110
125
|
const descriptor = catalog.guardrails.find((item) => item.check === 'manifest') ?? catalog.guardrails[0];
|
|
111
126
|
const failed = result(descriptor, 'failed', [{ path: 'lighthouse.json' }], manifestProblems.map((problem) => `Invalid lighthouse.json: ${problem}`));
|
|
112
127
|
return report([failed]);
|
|
113
128
|
}
|
|
114
|
-
const context = { manifest, repository, managedFiles, resolve, exists, isDirectory, readFile, validateGitConventions, branch };
|
|
129
|
+
const context = { manifest, repository, managedFiles, resolve, exists, isDirectory, readFile, validateGitConventions, branch, uiTokens };
|
|
115
130
|
const results = [];
|
|
116
131
|
for (const descriptor of catalog.guardrails) {
|
|
117
132
|
if (!appliesTo(descriptor, manifest)) {
|
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);
|
|
@@ -22,6 +32,7 @@ const upgradeStateRelative = '.lighthouse/upgrade.json';
|
|
|
22
32
|
const upgradeStatePath = resolve(root, upgradeStateRelative);
|
|
23
33
|
const layout = ['src', 'docs', 'infra', 'scripts'];
|
|
24
34
|
const sdlcPackage = { name: '@juangcarmona/lighthouse-sdlc', version: installedPackageVersion('@juangcarmona/lighthouse-sdlc', 'contract.json') ?? '0.1.0' };
|
|
35
|
+
const uiPackage = { name: '@juangcarmona/lighthouse-ui', version: installedPackageVersion('@juangcarmona/lighthouse-ui', 'DESIGN.md') ?? '0.1.0' };
|
|
25
36
|
const managedPaths = [
|
|
26
37
|
projection,
|
|
27
38
|
'scripts/lighthouse-guard.mjs',
|
|
@@ -31,16 +42,16 @@ const managedPaths = [
|
|
|
31
42
|
'.lighthouse/sdlc.json',
|
|
32
43
|
upgradeStateRelative,
|
|
33
44
|
];
|
|
45
|
+
const profileManagedPaths = Object.freeze({
|
|
46
|
+
'visual-theme': ['docs/design/DESIGN.md', '.agents/skills/lighthouse-ui/SKILL.md'],
|
|
47
|
+
});
|
|
34
48
|
const previousManagedPaths = managedPaths.slice(0, -1);
|
|
35
49
|
const legacyManagedPaths = [projection];
|
|
50
|
+
const supportedProfiles = ['node-tooling', 'dotnet-backend', 'visual-theme'];
|
|
36
51
|
const branchPattern = '^(feat|fix|docs|refactor|test|chore|build|ci|perf|revert)/[1-9]\\d*-[a-z0-9]+(?:-[a-z0-9]+)*$';
|
|
37
52
|
const authorityVersions = { productShape: '0.22.0', arcShape: '0.1.3' };
|
|
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
53
|
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]';
|
|
54
|
+
const usage = 'Usage: lighthouse <init|authorities|reconcile|upgrade|capabilities|verify> [--name <product>] [--profile <profile>] [--scope <harness|sdlc|all>] [--format <text|json>] [--dry-run] [--apply]';
|
|
44
55
|
|
|
45
56
|
const defaultRepository = () => ({
|
|
46
57
|
layout: [...layout],
|
|
@@ -48,16 +59,59 @@ const defaultRepository = () => ({
|
|
|
48
59
|
conventions: { commits: 'conventional', branchPattern },
|
|
49
60
|
});
|
|
50
61
|
|
|
51
|
-
function
|
|
62
|
+
function managedPathsFor(manifest) {
|
|
63
|
+
const profiles = manifest?.profiles ?? [];
|
|
64
|
+
return [...managedPaths, ...profiles.flatMap((profile) => profileManagedPaths[profile] ?? [])];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function upgradeScopesFor(manifest) {
|
|
68
|
+
return {
|
|
69
|
+
harness: [projection, 'scripts/lighthouse-guard.mjs', '.husky/commit-msg', '.husky/pre-push', '.github/workflows/lighthouse.yml', ...((manifest?.profiles ?? []).flatMap((profile) => profileManagedPaths[profile] ?? []))],
|
|
70
|
+
sdlc: ['.lighthouse/sdlc.json'],
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function packageAsset(packageName, entry) {
|
|
75
|
+
const path = packageRequire.resolve(`${packageName}/${entry}`);
|
|
76
|
+
return readFileSync(path, 'utf8');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function profileAssets(manifest) {
|
|
80
|
+
if (!(manifest?.profiles ?? []).includes('visual-theme')) return {};
|
|
81
|
+
try {
|
|
82
|
+
return {
|
|
83
|
+
'docs/design/DESIGN.md': packageAsset('@juangcarmona/lighthouse-ui', 'DESIGN.md'),
|
|
84
|
+
'.agents/skills/lighthouse-ui/SKILL.md': packageAsset('@juangcarmona/lighthouse-ui', 'skills/ui-design/SKILL.md'),
|
|
85
|
+
};
|
|
86
|
+
} catch (error) {
|
|
87
|
+
throw new Error(`Cannot read the installed Lighthouse UI profile: ${error.message}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function profileArguments(args) {
|
|
92
|
+
const selected = [];
|
|
93
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
94
|
+
if (args[index] !== '--profile' && !args[index].startsWith('--profile=')) continue;
|
|
95
|
+
const value = args[index] === '--profile' ? args[++index] : args[index].slice('--profile='.length);
|
|
96
|
+
if (!value || !supportedProfiles.includes(value)) throw new Error(`Unknown profile ${value ?? ''}; use ${supportedProfiles.join(', ')}.`);
|
|
97
|
+
if (!selected.includes(value)) selected.push(value);
|
|
98
|
+
}
|
|
99
|
+
return selected;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function defaultManifest(product, profiles = []) {
|
|
103
|
+
const draft = { profiles: [...profiles] };
|
|
104
|
+
const packages = { sdlc: { ...sdlcPackage } };
|
|
105
|
+
if (profiles.includes('visual-theme')) packages.ui = { ...uiPackage };
|
|
52
106
|
return {
|
|
53
107
|
$schema: './node_modules/@juangcarmona/lighthouse/assets/schemas/lighthouse-manifest.schema.json',
|
|
54
108
|
schemaVersion: 1,
|
|
55
109
|
baseline: '0.1.0',
|
|
56
110
|
product,
|
|
57
|
-
profiles: [],
|
|
111
|
+
profiles: [...profiles],
|
|
58
112
|
integrations: { productShape: null, arcShape: null },
|
|
59
|
-
packages
|
|
60
|
-
managed:
|
|
113
|
+
packages,
|
|
114
|
+
managed: managedPathsFor(draft),
|
|
61
115
|
repository: defaultRepository(),
|
|
62
116
|
};
|
|
63
117
|
}
|
|
@@ -96,6 +150,45 @@ function sdlcContract() {
|
|
|
96
150
|
}
|
|
97
151
|
}
|
|
98
152
|
|
|
153
|
+
function sdlcCapabilityCatalog() {
|
|
154
|
+
if (!sdlcCapabilities) return null;
|
|
155
|
+
try {
|
|
156
|
+
const path = packageRequire.resolve('@juangcarmona/lighthouse-sdlc/capabilities.json');
|
|
157
|
+
const catalog = JSON.parse(readFileSync(path, 'utf8'));
|
|
158
|
+
const errors = sdlcCapabilities.validateCapabilityCatalog(catalog);
|
|
159
|
+
if (errors.length) throw new Error(errors.map((error) => error.message).join('; '));
|
|
160
|
+
return catalog;
|
|
161
|
+
} catch (error) {
|
|
162
|
+
throw new Error(`Cannot read the installed SDLC capability catalog: ${error.message}`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function capabilityAdoption(previous = null) {
|
|
167
|
+
const catalog = sdlcCapabilityCatalog();
|
|
168
|
+
if (!catalog) return {};
|
|
169
|
+
return Object.fromEntries(catalog.capabilities.map((capability) => {
|
|
170
|
+
const current = previous?.adoption?.[capability.id];
|
|
171
|
+
const status = current?.catalogVersion === catalog.package.version
|
|
172
|
+
? current.status
|
|
173
|
+
: sdlcCapabilities.initialAdoptionStatus?.(capability) ?? 'pending-adoption';
|
|
174
|
+
const nextAction = status === 'available'
|
|
175
|
+
? `lighthouse capabilities select --id ${capability.id} --provider ${capability.provider}`
|
|
176
|
+
: ['adopted', 'declined'].includes(status)
|
|
177
|
+
? null
|
|
178
|
+
: `lighthouse capabilities plan --id ${capability.id} --provider ${capability.provider} --repo <owner/repo> --plan-file .lighthouse/reconciliation/${capability.id}.plan.json`;
|
|
179
|
+
return [capability.id, {
|
|
180
|
+
id: capability.id,
|
|
181
|
+
status,
|
|
182
|
+
selection: capability.selection,
|
|
183
|
+
provider: capability.provider,
|
|
184
|
+
version: capability.version,
|
|
185
|
+
catalogVersion: catalog.package.version,
|
|
186
|
+
nextAction,
|
|
187
|
+
...(current?.recordPath ? { recordPath: current.recordPath } : {}),
|
|
188
|
+
}];
|
|
189
|
+
}));
|
|
190
|
+
}
|
|
191
|
+
|
|
99
192
|
function sdlcControlPlane(manifest) {
|
|
100
193
|
const selected = normalizeSdlcPackage(manifest);
|
|
101
194
|
if (!selected) return null;
|
|
@@ -118,7 +211,9 @@ function render(manifest) {
|
|
|
118
211
|
const publicSite = repository.publicSite.enabled ? 'enabled' : 'disabled';
|
|
119
212
|
const selectedSdlc = normalizeSdlcPackage(manifest);
|
|
120
213
|
const sdlcLine = selectedSdlc ? `- SDLC package: \`${selectedSdlc.name}@${selectedSdlc.version}\`\n` : '';
|
|
121
|
-
|
|
214
|
+
const selectedUi = manifest.packages?.ui;
|
|
215
|
+
const uiLine = selectedUi ? `- UI package: \`${selectedUi.name}@${selectedUi.version}\`\n` : '';
|
|
216
|
+
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`;
|
|
122
217
|
}
|
|
123
218
|
|
|
124
219
|
function guardrailScript(manifest) {
|
|
@@ -210,6 +305,7 @@ function managedFiles(manifest) {
|
|
|
210
305
|
'.husky/pre-push': prePushHook(),
|
|
211
306
|
'.github/workflows/lighthouse.yml': workflow(),
|
|
212
307
|
};
|
|
308
|
+
Object.assign(files, profileAssets(manifest));
|
|
213
309
|
const controlPlane = sdlcControlPlane(manifest);
|
|
214
310
|
if (controlPlane) files['.lighthouse/sdlc.json'] = controlPlane;
|
|
215
311
|
return files;
|
|
@@ -220,9 +316,10 @@ function contentHash(content) {
|
|
|
220
316
|
}
|
|
221
317
|
|
|
222
318
|
function upgradeState(manifest, files = managedFiles(manifest), previous = null) {
|
|
319
|
+
const scopes = upgradeScopesFor(manifest);
|
|
223
320
|
const entries = Object.fromEntries(Object.entries(files).map(([path, content]) => [path, {
|
|
224
321
|
hash: contentHash(content),
|
|
225
|
-
scope: Object.entries(
|
|
322
|
+
scope: Object.entries(scopes).find(([, paths]) => paths.includes(path))?.[0] ?? 'harness',
|
|
226
323
|
}]));
|
|
227
324
|
return {
|
|
228
325
|
$schema: upgradeSchema,
|
|
@@ -230,6 +327,7 @@ function upgradeState(manifest, files = managedFiles(manifest), previous = null)
|
|
|
230
327
|
package: { name: cliPackage.name, version: cliPackage.version },
|
|
231
328
|
baseline: manifest.baseline,
|
|
232
329
|
capabilities: { harness: '1', sdlc: files['.lighthouse/sdlc.json'] ? '1' : null },
|
|
330
|
+
adoption: capabilityAdoption(previous),
|
|
233
331
|
projections: { harness: '1', sdlc: files['.lighthouse/sdlc.json'] ? '1' : null },
|
|
234
332
|
lastAppliedMigration: previous?.lastAppliedMigration ?? 'foundation-init-1',
|
|
235
333
|
files: entries,
|
|
@@ -259,7 +357,8 @@ function stateProblems(state) {
|
|
|
259
357
|
return problems;
|
|
260
358
|
}
|
|
261
359
|
|
|
262
|
-
function scopesFromArgs(args) {
|
|
360
|
+
function scopesFromArgs(args, manifest) {
|
|
361
|
+
const upgradeScopes = upgradeScopesFor(manifest);
|
|
263
362
|
const selected = new Set();
|
|
264
363
|
for (let index = 0; index < args.length; index += 1) {
|
|
265
364
|
if (args[index] !== '--scope' && !args[index].startsWith('--scope=')) continue;
|
|
@@ -275,7 +374,8 @@ function scopesFromArgs(args) {
|
|
|
275
374
|
return selected.size ? selected : new Set(Object.keys(upgradeScopes));
|
|
276
375
|
}
|
|
277
376
|
|
|
278
|
-
function selectedPaths(scopes) {
|
|
377
|
+
function selectedPaths(scopes, manifest) {
|
|
378
|
+
const upgradeScopes = upgradeScopesFor(manifest);
|
|
279
379
|
return new Set([...scopes].flatMap((scope) => upgradeScopes[scope]));
|
|
280
380
|
}
|
|
281
381
|
|
|
@@ -293,7 +393,8 @@ async function planUpgrade(manifest, scopes) {
|
|
|
293
393
|
const state = await readUpgradeState();
|
|
294
394
|
const problems = stateProblems(state);
|
|
295
395
|
const files = managedFiles(manifest);
|
|
296
|
-
const paths = selectedPaths(scopes);
|
|
396
|
+
const paths = selectedPaths(scopes, manifest);
|
|
397
|
+
const upgradeScopes = upgradeScopesFor(manifest);
|
|
297
398
|
const changes = [];
|
|
298
399
|
for (const path of Object.keys(files).filter((candidate) => paths.has(candidate))) {
|
|
299
400
|
const target = resolve(root, path);
|
|
@@ -311,6 +412,7 @@ async function planUpgrade(manifest, scopes) {
|
|
|
311
412
|
package: state?.package ?? null,
|
|
312
413
|
baseline: state?.baseline ?? null,
|
|
313
414
|
capabilities: state?.capabilities ?? null,
|
|
415
|
+
adoption: state?.adoption ?? null,
|
|
314
416
|
projections: state?.projections ?? null,
|
|
315
417
|
lastAppliedMigration: state?.lastAppliedMigration ?? null,
|
|
316
418
|
},
|
|
@@ -318,6 +420,7 @@ async function planUpgrade(manifest, scopes) {
|
|
|
318
420
|
package: { name: cliPackage.name, version: cliPackage.version },
|
|
319
421
|
baseline: manifest.baseline,
|
|
320
422
|
capabilities: { harness: '1', sdlc: files['.lighthouse/sdlc.json'] ? '1' : null },
|
|
423
|
+
adoption: capabilityAdoption(state),
|
|
321
424
|
projections: { harness: '1', sdlc: files['.lighthouse/sdlc.json'] ? '1' : null },
|
|
322
425
|
},
|
|
323
426
|
scopes: [...scopes],
|
|
@@ -325,6 +428,11 @@ async function planUpgrade(manifest, scopes) {
|
|
|
325
428
|
changes,
|
|
326
429
|
verification: ['lighthouse verify'],
|
|
327
430
|
recovery: 'Restore the pre-update commit or rerun lighthouse upgrade after resolving the reported ownership conflict.',
|
|
431
|
+
adoption: {
|
|
432
|
+
status: Object.values(capabilityAdoption(state)).some((item) => !['adopted', 'declined', 'available'].includes(item.status)) ? 'pending' : 'complete',
|
|
433
|
+
capabilities: capabilityAdoption(state),
|
|
434
|
+
externalWrites: 'none',
|
|
435
|
+
},
|
|
328
436
|
problems,
|
|
329
437
|
};
|
|
330
438
|
}
|
|
@@ -336,6 +444,12 @@ function printUpgradePlan(plan) {
|
|
|
336
444
|
console.log(` scopes ${plan.scopes.join(', ')}`);
|
|
337
445
|
for (const prerequisite of plan.prerequisites) console.log(` prerequisite ${prerequisite.status}: ${prerequisite.message}`);
|
|
338
446
|
for (const change of plan.changes) console.log(` ${change.classification} ${change.action} ${change.path}${change.reason ? ` (${change.reason})` : ''}`);
|
|
447
|
+
if (plan.adoption) {
|
|
448
|
+
console.log(` capability adoption ${plan.adoption.status} (external writes: ${plan.adoption.externalWrites})`);
|
|
449
|
+
for (const capability of Object.values(plan.adoption.capabilities)) {
|
|
450
|
+
console.log(` capability ${capability.id ?? capability.provider} ${capability.status}${capability.nextAction ? `; next: ${capability.nextAction}` : ''}`);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
339
453
|
if (plan.problems.length) for (const problem of plan.problems) console.error(` adoption gap ${problem}`);
|
|
340
454
|
console.log(` verify ${plan.verification[0]}`);
|
|
341
455
|
console.log(` recovery ${plan.recovery}`);
|
|
@@ -355,7 +469,7 @@ function validate(manifest) {
|
|
|
355
469
|
if (typeof manifest.product !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9 ._-]{0,63}$/.test(manifest.product)) {
|
|
356
470
|
problems.push('product must be a readable name of 1–64 characters');
|
|
357
471
|
}
|
|
358
|
-
const profiles =
|
|
472
|
+
const profiles = supportedProfiles;
|
|
359
473
|
if (!Array.isArray(manifest.profiles) || manifest.profiles.some((p) => !profiles.includes(p))
|
|
360
474
|
|| new Set(manifest.profiles).size !== manifest.profiles.length) {
|
|
361
475
|
problems.push('profiles must be unique supported profile names');
|
|
@@ -372,26 +486,42 @@ function validate(manifest) {
|
|
|
372
486
|
if ('packages' in manifest) {
|
|
373
487
|
const packages = manifest.packages;
|
|
374
488
|
const selected = packages?.sdlc;
|
|
489
|
+
const selectedUi = packages?.ui;
|
|
490
|
+
const hasVisualProfile = manifest.profiles?.includes('visual-theme');
|
|
375
491
|
if (!packages || typeof packages !== 'object' || Array.isArray(packages)
|
|
376
|
-
|| Object.keys(packages).
|
|
492
|
+
|| !Object.keys(packages).every((key) => ['sdlc', 'ui'].includes(key))
|
|
493
|
+
|| Object.keys(packages).length < 1 || Object.keys(packages).length > 2
|
|
377
494
|
|| !selected || typeof selected !== 'object' || Array.isArray(selected)
|
|
378
495
|
|| Object.keys(selected).length !== 2
|
|
379
496
|
|| selected.name !== sdlcPackage.name
|
|
380
497
|
|| typeof selected.version !== 'string' || !pin.test(selected.version)) {
|
|
381
498
|
problems.push(`packages.sdlc must declare ${sdlcPackage.name} and an exact version or commit hash`);
|
|
382
499
|
}
|
|
500
|
+
if (hasVisualProfile && (!selectedUi || typeof selectedUi !== 'object' || Array.isArray(selectedUi)
|
|
501
|
+
|| Object.keys(selectedUi).length !== 2
|
|
502
|
+
|| selectedUi.name !== uiPackage.name
|
|
503
|
+
|| typeof selectedUi.version !== 'string' || !pin.test(selectedUi.version))) {
|
|
504
|
+
problems.push(`packages.ui must declare ${uiPackage.name} and an exact version or commit hash when visual-theme is selected`);
|
|
505
|
+
}
|
|
506
|
+
if (!hasVisualProfile && selectedUi) problems.push('packages.ui requires the visual-theme profile');
|
|
507
|
+
} else if (manifest.profiles?.includes('visual-theme')) {
|
|
508
|
+
problems.push(`packages must declare ${sdlcPackage.name} and ${uiPackage.name} when visual-theme is selected`);
|
|
383
509
|
}
|
|
384
510
|
const legacyManaged = Array.isArray(manifest.managed)
|
|
385
511
|
&& manifest.managed.length === legacyManagedPaths.length
|
|
386
512
|
&& manifest.managed.every((entry, index) => entry === legacyManagedPaths[index]);
|
|
513
|
+
const expectedManagedPaths = managedPathsFor(manifest);
|
|
387
514
|
const completeManaged = Array.isArray(manifest.managed)
|
|
515
|
+
&& manifest.managed.length === expectedManagedPaths.length
|
|
516
|
+
&& manifest.managed.every((entry, index) => entry === expectedManagedPaths[index]);
|
|
517
|
+
const profilelessManaged = Array.isArray(manifest.managed)
|
|
388
518
|
&& manifest.managed.length === managedPaths.length
|
|
389
519
|
&& manifest.managed.every((entry, index) => entry === managedPaths[index]);
|
|
390
520
|
const previousCompleteManaged = Array.isArray(manifest.managed)
|
|
391
521
|
&& manifest.managed.length === previousManagedPaths.length
|
|
392
522
|
&& 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
|
|
523
|
+
if (!legacyManaged && !profilelessManaged && !previousCompleteManaged && !completeManaged) {
|
|
524
|
+
problems.push(`managed must be ${legacyManagedPaths.join(', ')}, the previous Lighthouse guardrail set or the selected profile set`);
|
|
395
525
|
}
|
|
396
526
|
if ('repository' in manifest) {
|
|
397
527
|
const repository = manifest.repository;
|
|
@@ -500,7 +630,12 @@ function printReconciliationPlan(plan) {
|
|
|
500
630
|
async function initialize(args) {
|
|
501
631
|
let manifest = await readManifest();
|
|
502
632
|
const manifestMissing = !manifest;
|
|
503
|
-
|
|
633
|
+
const selectedProfiles = profileArguments(args);
|
|
634
|
+
if (!manifest) manifest = defaultManifest(productNameFromArgs(args), selectedProfiles);
|
|
635
|
+
else if (selectedProfiles.some((profile) => !(manifest.profiles ?? []).includes(profile))) {
|
|
636
|
+
const missing = selectedProfiles.filter((profile) => !(manifest.profiles ?? []).includes(profile));
|
|
637
|
+
throw new Error(`The existing lighthouse.json does not select ${missing.join(', ')}; edit its profiles and rerun lighthouse init.`);
|
|
638
|
+
}
|
|
504
639
|
const problems = validate(manifest);
|
|
505
640
|
if (problems.length) throw new Error(problems.map((problem) => `Invalid lighthouse.json: ${problem}`).join('\n'));
|
|
506
641
|
const plan = await planInitialization(manifest, manifestMissing);
|
|
@@ -619,7 +754,7 @@ async function upgrade(args) {
|
|
|
619
754
|
if (!manifest) throw new Error('Cannot read lighthouse.json: file does not exist; run lighthouse init --name <product>.');
|
|
620
755
|
const problems = validate(manifest);
|
|
621
756
|
if (problems.length) throw new Error(problems.map((problem) => `Invalid lighthouse.json: ${problem}`).join('\n'));
|
|
622
|
-
const plan = await planUpgrade(manifest, scopesFromArgs(args));
|
|
757
|
+
const plan = await planUpgrade(manifest, scopesFromArgs(args, manifest));
|
|
623
758
|
const format = formatFromArgs(args);
|
|
624
759
|
const apply = args.includes('--apply');
|
|
625
760
|
const dryRun = args.includes('--dry-run') || !apply;
|
|
@@ -786,7 +921,32 @@ async function verify(manifest, format = 'text') {
|
|
|
786
921
|
readFile,
|
|
787
922
|
validateGitConventions,
|
|
788
923
|
branch: gitBranch(),
|
|
924
|
+
uiTokens: manifest?.profiles?.includes('visual-theme') ? packageAsset('@juangcarmona/lighthouse-ui', 'tokens.css') : null,
|
|
789
925
|
});
|
|
926
|
+
const adoption = Object.values(capabilityAdoption(await readUpgradeState()));
|
|
927
|
+
for (const item of adoption) {
|
|
928
|
+
if (item.status !== 'adopted' || !item.recordPath || await exists(resolve(root, item.recordPath))) continue;
|
|
929
|
+
item.status = 'failed';
|
|
930
|
+
item.nextAction = `lighthouse capabilities plan --id ${item.id} --provider ${item.provider} --repo <owner/repo> --plan-file .lighthouse/reconciliation/${item.id}.plan.json`;
|
|
931
|
+
}
|
|
932
|
+
if (adoption.length) {
|
|
933
|
+
const pending = adoption.filter((item) => !['adopted', 'declined', 'available'].includes(item.status));
|
|
934
|
+
const available = adoption.some((item) => item.status === 'available');
|
|
935
|
+
const declined = adoption.some((item) => item.status === 'declined');
|
|
936
|
+
report.results.push({
|
|
937
|
+
id: 'foundation.capability-adoption',
|
|
938
|
+
owner: '@juangcarmona/lighthouse-sdlc',
|
|
939
|
+
level: 'recommended',
|
|
940
|
+
kind: 'hard',
|
|
941
|
+
verification: 'deterministic',
|
|
942
|
+
status: pending.length ? 'unconfigured' : available || declined ? 'not-applicable' : 'passed',
|
|
943
|
+
adoptionStatus: pending.length ? pending[0].status : available ? 'available' : declined ? 'declined' : 'adopted',
|
|
944
|
+
evidence: adoption.map((item) => ({ capability: item.id, catalogVersion: item.catalogVersion })),
|
|
945
|
+
problems: pending.map((item) => `${item.id} is ${item.status}; run ${item.nextAction}`),
|
|
946
|
+
nextAction: pending[0]?.nextAction ?? null,
|
|
947
|
+
});
|
|
948
|
+
report.summary.unconfigured = (report.summary.unconfigured ?? 0) + (pending.length ? 1 : 0);
|
|
949
|
+
}
|
|
790
950
|
if (format === 'json') {
|
|
791
951
|
console.log(JSON.stringify(report, null, 2));
|
|
792
952
|
if (report.problems.length) process.exitCode = 1;
|
|
@@ -796,6 +956,158 @@ async function verify(manifest, format = 'text') {
|
|
|
796
956
|
console.log('Lighthouse manifest, repository boundaries and managed projection are valid.');
|
|
797
957
|
}
|
|
798
958
|
|
|
959
|
+
function planHash(plan) {
|
|
960
|
+
const { authorization, ...payload } = plan;
|
|
961
|
+
return contentHash(JSON.stringify(payload));
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
function capabilityPlanText(plan) {
|
|
965
|
+
const lines = [
|
|
966
|
+
`Capability adoption: ${plan.status}`,
|
|
967
|
+
`Capability: ${plan.capability.id}@${plan.capability.version} (${plan.capability.provider})`,
|
|
968
|
+
`Repository: ${plan.repository.repo}`,
|
|
969
|
+
`Remote writes: none`,
|
|
970
|
+
`Authorization: ${plan.authorization.planHash}`,
|
|
971
|
+
];
|
|
972
|
+
for (const change of plan.reconciliation.changes) lines.push(` ${change.status} ${change.name}`);
|
|
973
|
+
if (plan.nextAction) lines.push(`Next: ${plan.nextAction}`);
|
|
974
|
+
return `${lines.join('\n')}\n`;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
async function writeCapabilityPlan(path, plan) {
|
|
978
|
+
const target = resolve(root, path);
|
|
979
|
+
await mkdir(dirname(target), { recursive: true });
|
|
980
|
+
await writeTextAtomically(target, `${JSON.stringify(plan, null, 2)}\n`);
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
async function writeCapabilityEvidence(plan, status, extra = {}) {
|
|
984
|
+
const path = resolve(root, '.lighthouse/reconciliation', `${plan.capability.id}.json`);
|
|
985
|
+
await mkdir(dirname(path), { recursive: true });
|
|
986
|
+
const evidence = {
|
|
987
|
+
schemaVersion: 1,
|
|
988
|
+
capability: plan.capability,
|
|
989
|
+
catalogVersion: plan.catalogVersion,
|
|
990
|
+
provider: plan.repository,
|
|
991
|
+
desiredHash: plan.reconciliation.desiredHash,
|
|
992
|
+
observedHash: extra.observedHash ?? plan.reconciliation.observedHash,
|
|
993
|
+
targetHash: extra.targetHash ?? plan.reconciliation.targetHash,
|
|
994
|
+
operations: extra.operations ?? plan.reconciliation.operations,
|
|
995
|
+
operationPolicy: plan.reconciliation.operationPolicy ?? plan.operationPolicy ?? null,
|
|
996
|
+
blockedOperations: extra.blockedOperations ?? plan.reconciliation.blockedOperations ?? [],
|
|
997
|
+
conflicts: extra.conflicts ?? plan.reconciliation.changes.filter((change) => change.status === 'conflict'),
|
|
998
|
+
status,
|
|
999
|
+
verified: status === 'adopted',
|
|
1000
|
+
};
|
|
1001
|
+
await writeTextAtomically(path, `${JSON.stringify(evidence, null, 2)}\n`);
|
|
1002
|
+
return path;
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
async function capabilities(args) {
|
|
1006
|
+
const action = args[0];
|
|
1007
|
+
const id = args.includes('--id') ? args[args.indexOf('--id') + 1] : args.find((arg) => arg.startsWith('--id='))?.slice(5) ?? 'labels';
|
|
1008
|
+
const provider = args.includes('--provider') ? args[args.indexOf('--provider') + 1] : args.find((arg) => arg.startsWith('--provider='))?.slice(11) ?? 'github';
|
|
1009
|
+
const repo = args.includes('--repo') ? args[args.indexOf('--repo') + 1] : args.find((arg) => arg.startsWith('--repo='))?.slice(7);
|
|
1010
|
+
const inputPath = args.includes('--input') ? args[args.indexOf('--input') + 1] : args.find((arg) => arg.startsWith('--input='))?.slice(8);
|
|
1011
|
+
const format = formatFromArgs(args);
|
|
1012
|
+
const planFile = args.includes('--plan-file') ? args[args.indexOf('--plan-file') + 1] : args.find((arg) => arg.startsWith('--plan-file='))?.slice(12);
|
|
1013
|
+
if (['select', 'decline'].includes(action)) {
|
|
1014
|
+
const catalog = sdlcCapabilityCatalog();
|
|
1015
|
+
if (!catalog) throw new Error('The installed SDLC package does not publish the capability adoption contract; upgrade it before selecting capabilities.');
|
|
1016
|
+
const capability = catalog.capabilities.find((item) => item.id === id && item.provider === provider);
|
|
1017
|
+
if (!capability) throw new Error(`Capability ${id} for provider ${provider} is not declared by the installed SDLC package.`);
|
|
1018
|
+
const state = await readUpgradeState();
|
|
1019
|
+
if (!state) throw new Error('Cannot select a capability before Lighthouse ownership evidence exists; run lighthouse init or reconcile --apply.');
|
|
1020
|
+
state.adoption = state.adoption ?? {};
|
|
1021
|
+
const status = action === 'decline' ? 'declined' : 'pending-adoption';
|
|
1022
|
+
state.adoption[id] = {
|
|
1023
|
+
id,
|
|
1024
|
+
status,
|
|
1025
|
+
selection: capability.selection,
|
|
1026
|
+
provider: capability.provider,
|
|
1027
|
+
version: capability.version,
|
|
1028
|
+
catalogVersion: catalog.package.version,
|
|
1029
|
+
nextAction: status === 'declined' ? null : `lighthouse capabilities plan --id ${id} --provider ${provider} --repo <owner/repo> --plan-file .lighthouse/reconciliation/${id}.plan.json`,
|
|
1030
|
+
};
|
|
1031
|
+
await writeTextAtomically(upgradeStatePath, `${JSON.stringify(state, null, 2)}\n`);
|
|
1032
|
+
const result = { schemaVersion: 1, capability: id, provider, status, applied: true };
|
|
1033
|
+
if (format === 'json') console.log(JSON.stringify(result, null, 2)); else console.log(`Capability ${id}: ${status}.`);
|
|
1034
|
+
return;
|
|
1035
|
+
}
|
|
1036
|
+
if (action === 'plan') {
|
|
1037
|
+
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>]');
|
|
1038
|
+
const catalog = sdlcCapabilityCatalog();
|
|
1039
|
+
if (!catalog || !sdlcCapabilities || !sdlcReconcile || !sdlcGithub) throw new Error('The installed SDLC package does not publish the capability adoption contract; upgrade it before planning adoption.');
|
|
1040
|
+
const capability = catalog.capabilities.find((item) => item.id === id && item.provider === provider);
|
|
1041
|
+
if (!capability) throw new Error(`Capability ${id} for provider ${provider} is not declared by the installed SDLC package.`);
|
|
1042
|
+
const declaration = inputPath ? JSON.parse(await readFile(resolve(root, inputPath), 'utf8')) : sdlcCapabilities.declarationFromCatalog(catalog, id);
|
|
1043
|
+
const labels = await sdlcGithub.readGithubLabels(repo);
|
|
1044
|
+
const reconciliation = sdlcReconcile.buildLabelReconciliationPlan(declaration, labels, await (async () => {
|
|
1045
|
+
const path = resolve(root, '.lighthouse/reconciliation', `${id}.json`);
|
|
1046
|
+
try { return JSON.parse(await readFile(path, 'utf8')); } catch { return null; }
|
|
1047
|
+
})());
|
|
1048
|
+
const plan = {
|
|
1049
|
+
schemaVersion: 1,
|
|
1050
|
+
status: reconciliation.status,
|
|
1051
|
+
capability: { id: capability.id, provider: capability.provider, selection: capability.selection, version: capability.version },
|
|
1052
|
+
catalogVersion: catalog.package.version,
|
|
1053
|
+
operationPolicy: capability.operations,
|
|
1054
|
+
repository: { provider, repo },
|
|
1055
|
+
declaration,
|
|
1056
|
+
reconciliation,
|
|
1057
|
+
authorization: { required: true, planHash: '' },
|
|
1058
|
+
nextAction: reconciliation.canApply ? `lighthouse capabilities apply --plan-file ${planFile} --authorize <plan-hash>` : 'Resolve the reported conflicts before authorizing this plan.',
|
|
1059
|
+
};
|
|
1060
|
+
plan.authorization.planHash = planHash(plan);
|
|
1061
|
+
await writeCapabilityPlan(planFile, plan);
|
|
1062
|
+
if (format === 'json') console.log(JSON.stringify(plan, null, 2)); else process.stdout.write(capabilityPlanText(plan));
|
|
1063
|
+
if (!reconciliation.valid || !reconciliation.canApply) process.exitCode = 1;
|
|
1064
|
+
return;
|
|
1065
|
+
}
|
|
1066
|
+
if (action === 'apply') {
|
|
1067
|
+
const authorization = args.includes('--authorize') ? args[args.indexOf('--authorize') + 1] : args.find((arg) => arg.startsWith('--authorize='))?.slice(12);
|
|
1068
|
+
if (!planFile || !authorization) throw new Error('Usage: lighthouse capabilities apply --plan-file <path> --authorize <plan-hash> [--format <text|json>]');
|
|
1069
|
+
const plan = JSON.parse(await readFile(resolve(root, planFile), 'utf8'));
|
|
1070
|
+
if (authorization !== plan.authorization?.planHash || planHash(plan) !== authorization) throw new Error('Capability plan authorization does not match the plan file; generate a new plan.');
|
|
1071
|
+
if (!plan.reconciliation?.canApply) throw new Error('Capability plan is blocked; resolve its conflicts and generate a new plan.');
|
|
1072
|
+
if (!sdlcReconcile || !sdlcGithub) throw new Error('The installed SDLC package does not publish the capability adoption contract; upgrade it before applying adoption.');
|
|
1073
|
+
const current = await sdlcGithub.readGithubLabels(plan.repository.repo);
|
|
1074
|
+
let reconciliation = sdlcReconcile.buildLabelReconciliationPlan(plan.declaration, current, plan.reconciliation.record);
|
|
1075
|
+
const originalOperations = new Set(plan.reconciliation.operations.map((operation) => operation.name));
|
|
1076
|
+
const plannedTarget = new Map((plan.reconciliation.target ?? []).map((label) => [label.name, JSON.stringify(label)]));
|
|
1077
|
+
const outsidePlanDrift = current.some((label) => !plannedTarget.has(label.name) || (plannedTarget.has(label.name) && !originalOperations.has(label.name) && JSON.stringify(label) !== plannedTarget.get(label.name)));
|
|
1078
|
+
if (reconciliation.observedHash !== plan.reconciliation.observedHash && (outsidePlanDrift || reconciliation.operations.some((operation) => !originalOperations.has(operation.name)))) {
|
|
1079
|
+
throw new Error('GitHub labels changed outside this plan; refusing to apply. Generate a new plan.');
|
|
1080
|
+
}
|
|
1081
|
+
const completed = [];
|
|
1082
|
+
try {
|
|
1083
|
+
for (const operation of reconciliation.operations) {
|
|
1084
|
+
await sdlcGithub.createGithubLabel(plan.repository.repo, operation, { dryRun: false });
|
|
1085
|
+
completed.push(operation);
|
|
1086
|
+
}
|
|
1087
|
+
} catch (error) {
|
|
1088
|
+
await writeCapabilityEvidence(plan, 'failed', { operations: completed, observedHash: reconciliation.observedHash });
|
|
1089
|
+
throw new Error(`Capability adoption stopped after ${completed.length} operation(s): ${error.message}`);
|
|
1090
|
+
}
|
|
1091
|
+
const finalLabels = await sdlcGithub.readGithubLabels(plan.repository.repo);
|
|
1092
|
+
const final = sdlcReconcile.buildLabelReconciliationPlan(plan.declaration, finalLabels, plan.reconciliation.record);
|
|
1093
|
+
if (!final.valid || final.operations.length || final.changes.some((change) => change.status === 'conflict')) {
|
|
1094
|
+
await writeCapabilityEvidence(plan, 'blocked', { operations: completed, observedHash: final.observedHash, targetHash: final.targetHash, conflicts: final.changes.filter((change) => change.status === 'conflict') });
|
|
1095
|
+
throw new Error('Capability adoption did not reach the declared state; inspect the evidence and generate a new plan.');
|
|
1096
|
+
}
|
|
1097
|
+
const evidencePath = await writeCapabilityEvidence(plan, 'adopted', { operations: completed, observedHash: final.observedHash, targetHash: final.targetHash });
|
|
1098
|
+
const state = await readUpgradeState();
|
|
1099
|
+
if (state) {
|
|
1100
|
+
state.adoption = state.adoption ?? {};
|
|
1101
|
+
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' };
|
|
1102
|
+
await writeTextAtomically(upgradeStatePath, `${JSON.stringify(state, null, 2)}\n`);
|
|
1103
|
+
}
|
|
1104
|
+
const result = { ...plan, status: 'adopted', evidencePath, reconciliation: final, applied: true };
|
|
1105
|
+
if (format === 'json') console.log(JSON.stringify(result, null, 2)); else process.stdout.write(capabilityPlanText(result));
|
|
1106
|
+
return;
|
|
1107
|
+
}
|
|
1108
|
+
throw new Error('Usage: lighthouse capabilities <select|decline|plan|apply> ...');
|
|
1109
|
+
}
|
|
1110
|
+
|
|
799
1111
|
function formatFromArgs(args) {
|
|
800
1112
|
const index = args.indexOf('--format');
|
|
801
1113
|
if (index !== -1) return args[index + 1];
|
|
@@ -808,17 +1120,27 @@ const commandArgs = args.slice(1);
|
|
|
808
1120
|
let invalidArgument = false;
|
|
809
1121
|
for (let index = 0; index < commandArgs.length; index += 1) {
|
|
810
1122
|
const arg = commandArgs[index];
|
|
1123
|
+
if (command === 'capabilities' && index === 0 && ['select', 'decline', 'plan', 'apply'].includes(arg)) continue;
|
|
811
1124
|
if (arg === '--dry-run') continue;
|
|
812
1125
|
if (arg === '--apply') { if (!['authorities', 'reconcile', 'upgrade'].includes(command)) invalidArgument = true; continue; }
|
|
813
1126
|
if (arg === '--name') { if (!commandArgs[index + 1]) invalidArgument = true; else index += 1; continue; }
|
|
814
1127
|
if (arg.startsWith('--name=') && arg.length > '--name='.length) continue;
|
|
1128
|
+
if (arg === '--profile') {
|
|
1129
|
+
if (command !== 'init' || !commandArgs[index + 1]) invalidArgument = true;
|
|
1130
|
+
else index += 1;
|
|
1131
|
+
continue;
|
|
1132
|
+
}
|
|
1133
|
+
if (arg.startsWith('--profile=') && arg.length > '--profile='.length) {
|
|
1134
|
+
if (command !== 'init') invalidArgument = true;
|
|
1135
|
+
continue;
|
|
1136
|
+
}
|
|
815
1137
|
if (arg === '--format') {
|
|
816
|
-
if (!['verify', 'upgrade'].includes(command) || !['text', 'json'].includes(commandArgs[index + 1])) invalidArgument = true;
|
|
1138
|
+
if (!['verify', 'upgrade', 'capabilities'].includes(command) || !['text', 'json'].includes(commandArgs[index + 1])) invalidArgument = true;
|
|
817
1139
|
else index += 1;
|
|
818
1140
|
continue;
|
|
819
1141
|
}
|
|
820
1142
|
if (arg.startsWith('--format=') && ['text', 'json'].includes(arg.slice('--format='.length))) {
|
|
821
|
-
if (!['verify', 'upgrade'].includes(command)) invalidArgument = true;
|
|
1143
|
+
if (!['verify', 'upgrade', 'capabilities'].includes(command)) invalidArgument = true;
|
|
822
1144
|
continue;
|
|
823
1145
|
}
|
|
824
1146
|
if (arg === '--scope') {
|
|
@@ -830,9 +1152,15 @@ for (let index = 0; index < commandArgs.length; index += 1) {
|
|
|
830
1152
|
if (command !== 'upgrade') invalidArgument = true;
|
|
831
1153
|
continue;
|
|
832
1154
|
}
|
|
1155
|
+
if (['--id', '--provider', '--repo', '--input', '--plan-file', '--authorize'].includes(arg)) {
|
|
1156
|
+
if (!commandArgs[index + 1]) invalidArgument = true;
|
|
1157
|
+
else index += 1;
|
|
1158
|
+
continue;
|
|
1159
|
+
}
|
|
1160
|
+
if (['--id=', '--provider=', '--repo=', '--input=', '--plan-file=', '--authorize='].some((prefix) => arg.startsWith(prefix) && arg.length > prefix.length)) continue;
|
|
833
1161
|
invalidArgument = true;
|
|
834
1162
|
}
|
|
835
|
-
if (!['init', 'authorities', 'reconcile', 'upgrade', 'verify'].includes(command) || invalidArgument) {
|
|
1163
|
+
if (!['init', 'authorities', 'reconcile', 'upgrade', 'capabilities', 'verify'].includes(command) || invalidArgument) {
|
|
836
1164
|
console.error(usage);
|
|
837
1165
|
process.exit(2);
|
|
838
1166
|
}
|
|
@@ -841,6 +1169,7 @@ try {
|
|
|
841
1169
|
else if (command === 'authorities') await authorities(args.slice(1));
|
|
842
1170
|
else if (command === 'reconcile') await reconcile(args.slice(1));
|
|
843
1171
|
else if (command === 'upgrade') await upgrade(args.slice(1));
|
|
1172
|
+
else if (command === 'capabilities') await capabilities(args.slice(1));
|
|
844
1173
|
else {
|
|
845
1174
|
const manifest = await readManifest();
|
|
846
1175
|
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.
|