@juangcarmona/lighthouse 0.7.0 → 0.9.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 +7 -4
- package/assets/command-aliases.json +11 -0
- package/assets/schemas/command-aliases.schema.json +35 -0
- package/assets/schemas/init-plan.schema.json +40 -0
- package/assets/skills/init/SKILL.md +30 -0
- package/package.json +6 -4
- package/src/aliases.mjs +56 -0
- package/src/create-dotnet.mjs +31 -1
- package/src/init-plan.mjs +62 -0
- package/src/lighthouse.mjs +499 -14
package/README.md
CHANGED
|
@@ -6,6 +6,8 @@ This package is the core Lighthouse harness. It provides the `lighthouse` comman
|
|
|
6
6
|
|
|
7
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
|
+
When the request is expressed in product terms, use the projected `.agents/skills/lighthouse-init/SKILL.md` to prepare a JSON plan that follows `assets/schemas/init-plan.schema.json`. A reference project is optional; the agent asks only the questions needed to resolve the application shape, stacks and profiles. `lighthouse init --plan-file <path>` previews the local operations by default, `--format json` emits the machine-readable plan and `--apply` is required to write it. The CLI does not inspect or copy a reference and performs no remote writes.
|
|
10
|
+
|
|
9
11
|
For a Lighthouse-owned .NET backend, run `lighthouse create-dotnet <empty-target> --name <PascalCaseProduct>`. The creator seeds the modular backend, Aspire AppHost, CQRS/analyzer sources, tests, profile guidance and deterministic verifier into a new or empty directory. It refuses populated targets and never contacts a remote repository. Run `dotnet restore <Product>.slnx`, then `npm run verify` in the generated product.
|
|
10
12
|
|
|
11
13
|
The manifest can declare `application.shape` and `application.stacks` when the project has enough information to select compatible profiles. `lighthouse profiles --format json` reports selected profiles, applicability, owned surfaces and verification layers without changing the repository.
|
|
@@ -16,7 +18,7 @@ Use `lighthouse upgrade` to preview a versioned foundation update. The command i
|
|
|
16
18
|
|
|
17
19
|
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; the .NET creator is the packaged entry point for the selected backend profile.
|
|
18
20
|
|
|
19
|
-
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.
|
|
21
|
+
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. In an existing repository, run `lighthouse authorities --adopt-existing` to validate the current ProductShape and ArcShape surfaces without running either initializer; review the dry-run plan and add `--apply` only to record both exact versions in Lighthouse-owned configuration.
|
|
20
22
|
|
|
21
23
|
Use `lighthouse authorities` to prepare the two baseline authorities through their own CLIs. The command previews both plans by default; `--apply` runs ProductShape's full model initialization and ArcShape's architecture initialization without installing agent capabilities. Provider-owned files remain owned by the provider, and Lighthouse records the exact adopted versions in `lighthouse.json` only after both applications succeed.
|
|
22
24
|
|
|
@@ -37,12 +39,13 @@ Initialization reports missing ProductShape and ArcShape authority inputs as ado
|
|
|
37
39
|
1. Inventory the current source layout, tools, managed files and CI before editing.
|
|
38
40
|
2. Map existing files to the selected Lighthouse contracts and identify collisions.
|
|
39
41
|
3. Adopt the manifest and verifier first; the initializer reports conflicts and preserves product-owned guidance and generated outputs.
|
|
40
|
-
4.
|
|
41
|
-
5.
|
|
42
|
+
4. If ProductShape or ArcShape already exists, run `lighthouse authorities --adopt-existing` and repair any provider validation result before applying it.
|
|
43
|
+
5. Add profiles one at a time and measure their checks before making a workflow status required.
|
|
44
|
+
6. Record each exception in the project-owned documentation and remove it when the product is ready.
|
|
42
45
|
|
|
43
46
|
## Current command scope
|
|
44
47
|
|
|
45
|
-
The command supports `init`, `create-dotnet`, `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.
|
|
48
|
+
The command supports `init`, `aliases`, `create-dotnet`, `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. `lighthouse aliases --format json` exposes the installed `/lh:*` aliases and their canonical `/lighthouse:*` commands. `lighthouse authorities --adopt-existing` validates existing ProductShape and ArcShape authorities without writing provider-owned files. Lifecycle stages, tracker operations, agent runs and CodeGraph checks require their own configured integrations.
|
|
46
49
|
|
|
47
50
|
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.
|
|
48
51
|
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "./schemas/command-aliases.schema.json",
|
|
3
|
+
"schemaVersion": 1,
|
|
4
|
+
"contractVersion": 1,
|
|
5
|
+
"package": { "name": "@juangcarmona/lighthouse" },
|
|
6
|
+
"canonicalNamespace": "/lighthouse",
|
|
7
|
+
"aliasNamespace": "/lh",
|
|
8
|
+
"aliases": [
|
|
9
|
+
{ "alias": "/lh:init", "canonical": "/lighthouse:init", "asset": "skills/init/SKILL.md" }
|
|
10
|
+
]
|
|
11
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://github.com/juangcarmona/lighthouse/schemas/lighthouse-command-aliases.schema.json",
|
|
4
|
+
"title": "Lighthouse command aliases",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": false,
|
|
7
|
+
"required": ["schemaVersion", "contractVersion", "package", "canonicalNamespace", "aliasNamespace", "aliases"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"$schema": {"type": "string"},
|
|
10
|
+
"schemaVersion": {"const": 1},
|
|
11
|
+
"contractVersion": {"const": 1},
|
|
12
|
+
"package": {
|
|
13
|
+
"type": "object",
|
|
14
|
+
"additionalProperties": false,
|
|
15
|
+
"required": ["name"],
|
|
16
|
+
"properties": {"name": {"const": "@juangcarmona/lighthouse"}}
|
|
17
|
+
},
|
|
18
|
+
"canonicalNamespace": {"const": "/lighthouse"},
|
|
19
|
+
"aliasNamespace": {"const": "/lh"},
|
|
20
|
+
"aliases": {
|
|
21
|
+
"type": "array",
|
|
22
|
+
"minItems": 1,
|
|
23
|
+
"items": {
|
|
24
|
+
"type": "object",
|
|
25
|
+
"additionalProperties": false,
|
|
26
|
+
"required": ["alias", "canonical", "asset"],
|
|
27
|
+
"properties": {
|
|
28
|
+
"alias": {"type": "string", "pattern": "^/lh:[a-z][a-z0-9-]*$"},
|
|
29
|
+
"canonical": {"type": "string", "pattern": "^/lighthouse:[a-z][a-z0-9-]*$"},
|
|
30
|
+
"asset": {"type": "string", "pattern": "^skills/[a-z0-9-]+/SKILL\\.md$"}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://github.com/juangcarmona/lighthouse/schemas/init-plan.schema.json",
|
|
4
|
+
"title": "Lighthouse semantic initialization plan",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": false,
|
|
7
|
+
"required": ["schemaVersion", "product", "intent", "application", "profiles", "openQuestions"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"$schema": {"type": "string"},
|
|
10
|
+
"schemaVersion": {"const": 1},
|
|
11
|
+
"product": {"type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9 ._-]{0,63}$"},
|
|
12
|
+
"intent": {"type": "string", "minLength": 1},
|
|
13
|
+
"application": {
|
|
14
|
+
"type": "object",
|
|
15
|
+
"additionalProperties": false,
|
|
16
|
+
"required": ["shape", "stacks"],
|
|
17
|
+
"properties": {
|
|
18
|
+
"shape": {"enum": ["unknown", "frontend", "backend", "full-stack", "service", "library", "tooling", "cli", "worker", "mobile"]},
|
|
19
|
+
"stacks": {"type": "array", "items": {"type": "string", "minLength": 1}, "uniqueItems": true}
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"profiles": {"type": "array", "items": {"type": "string", "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$"}, "uniqueItems": true},
|
|
23
|
+
"reference": {
|
|
24
|
+
"oneOf": [
|
|
25
|
+
{"type": "null"},
|
|
26
|
+
{
|
|
27
|
+
"type": "object",
|
|
28
|
+
"additionalProperties": false,
|
|
29
|
+
"required": ["kind"],
|
|
30
|
+
"properties": {
|
|
31
|
+
"kind": {"enum": ["none", "local", "repository"]},
|
|
32
|
+
"location": {"type": "string", "minLength": 1},
|
|
33
|
+
"purpose": {"type": "string", "minLength": 1}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
]
|
|
37
|
+
},
|
|
38
|
+
"openQuestions": {"type": "array", "items": {"type": "string", "minLength": 1}, "uniqueItems": true}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: lighthouse-init
|
|
3
|
+
description: Turn a natural-language project request into a reviewable Lighthouse initialization plan.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Lighthouse semantic initialization
|
|
7
|
+
|
|
8
|
+
Use this skill when an adopter describes a project in product terms and wants Lighthouse to prepare its foundation.
|
|
9
|
+
|
|
10
|
+
## Inputs
|
|
11
|
+
|
|
12
|
+
1. Read the installed profile catalog and the repository's current Lighthouse manifest when one exists.
|
|
13
|
+
2. Treat a reference project as optional. If the adopter names one, record its location and purpose; do not assume it exists or copy it.
|
|
14
|
+
3. Ask only the questions needed to resolve the application shape, stacks, profiles and material profile combinations. Keep unresolved questions in the plan instead of guessing.
|
|
15
|
+
|
|
16
|
+
## Plan
|
|
17
|
+
|
|
18
|
+
Write a JSON plan that validates against `assets/schemas/init-plan.schema.json` with `schemaVersion: 1`, `product`, `intent`, `application`, `profiles`, optional `reference` metadata and `openQuestions`.
|
|
19
|
+
|
|
20
|
+
Select profiles from the installed catalog. Do not invent a profile, make a stack mandatory, or turn a reference into a requirement. An absent reference is represented by an omitted `reference` field or `{"kind":"none"}`.
|
|
21
|
+
|
|
22
|
+
## Apply
|
|
23
|
+
|
|
24
|
+
Review the generated plan with the adopter, then run `lighthouse init --plan-file <path>` for a dry run. Use `--format json` for agent consumption. Apply only after the adopter explicitly authorizes `lighthouse init --plan-file <path> --apply`.
|
|
25
|
+
|
|
26
|
+
The CLI owns deterministic validation, profile compatibility, conflict detection and local writes. It does not inspect or copy the reference and it performs no remote writes. A plan with `openQuestions` is not ready to apply.
|
|
27
|
+
|
|
28
|
+
Use `lighthouse aliases --format json` to discover the installed `/lh:*` aliases and their canonical `/lighthouse:*` commands. The generated `.lighthouse/commands.json` is the host-readable projection; aliases reference the package-owned command or skill asset and never create a second implementation.
|
|
29
|
+
|
|
30
|
+
After applying, run `lighthouse verify` and report the created, preserved and conflicting paths. Keep product and architecture work in ProductShape and ArcShape's own routes.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@juangcarmona/lighthouse",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Lighthouse adoption and verification CLI",
|
|
6
6
|
"repository": {
|
|
@@ -20,13 +20,15 @@
|
|
|
20
20
|
"node": ">=22"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@arcshape/cli": "0.1.
|
|
23
|
+
"@arcshape/cli": "0.1.6",
|
|
24
24
|
"@juangcarmona/lighthouse-ui": "0.2.0",
|
|
25
|
-
"@prodshape/cli": "0.
|
|
26
|
-
"@juangcarmona/lighthouse-sdlc": "0.
|
|
25
|
+
"@prodshape/cli": "0.23.0",
|
|
26
|
+
"@juangcarmona/lighthouse-sdlc": "0.8.0"
|
|
27
27
|
},
|
|
28
28
|
"files": [
|
|
29
29
|
"src/lighthouse.mjs",
|
|
30
|
+
"src/aliases.mjs",
|
|
31
|
+
"src/init-plan.mjs",
|
|
30
32
|
"src/guardrails.mjs",
|
|
31
33
|
"assets",
|
|
32
34
|
"README.md",
|
package/src/aliases.mjs
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { access } from 'node:fs/promises';
|
|
2
|
+
import { constants, readFileSync } from 'node:fs';
|
|
3
|
+
import { dirname, resolve } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
|
|
6
|
+
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
7
|
+
const contractPath = resolve(packageRoot, 'assets/command-aliases.json');
|
|
8
|
+
const packageName = '@juangcarmona/lighthouse';
|
|
9
|
+
const aliasPattern = /^\/lh:[a-z][a-z0-9-]*$/u;
|
|
10
|
+
const canonicalPattern = /^\/lighthouse:[a-z][a-z0-9-]*$/u;
|
|
11
|
+
const assetPattern = /^skills\/[a-z0-9-]+\/SKILL\.md$/u;
|
|
12
|
+
|
|
13
|
+
async function exists(path) {
|
|
14
|
+
try { await access(path, constants.F_OK); return true; } catch { return false; }
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function readCommandAliases(path = contractPath) {
|
|
18
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function validateCommandAliases(contract) {
|
|
22
|
+
const errors = [];
|
|
23
|
+
if (!contract || typeof contract !== 'object' || Array.isArray(contract)) return [{ code: 'aliases.invalid', message: 'command alias contract must be an object' }];
|
|
24
|
+
if (contract.schemaVersion !== 1) errors.push({ code: 'aliases.schema-version', message: 'command alias schemaVersion must be 1' });
|
|
25
|
+
if (contract.contractVersion !== 1) errors.push({ code: 'aliases.contract-version', message: 'command alias contractVersion must be 1' });
|
|
26
|
+
if (contract.package?.name !== packageName) errors.push({ code: 'aliases.package', message: `command alias contract package must be ${packageName}` });
|
|
27
|
+
if (contract.canonicalNamespace !== '/lighthouse') errors.push({ code: 'aliases.canonical-namespace', message: 'canonical namespace must be /lighthouse' });
|
|
28
|
+
if (contract.aliasNamespace !== '/lh') errors.push({ code: 'aliases.alias-namespace', message: 'alias namespace must be /lh' });
|
|
29
|
+
if (!Array.isArray(contract.aliases) || contract.aliases.length === 0) return [...errors, { code: 'aliases.list', message: 'command alias contract must declare at least one alias' }];
|
|
30
|
+
const aliases = new Set();
|
|
31
|
+
const canonicals = new Set();
|
|
32
|
+
for (const [index, item] of contract.aliases.entries()) {
|
|
33
|
+
if (!item || typeof item !== 'object' || Array.isArray(item)) {
|
|
34
|
+
errors.push({ code: 'aliases.entry', message: `alias ${index} must be an object` });
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (typeof item.alias !== 'string' || !aliasPattern.test(item.alias)) errors.push({ code: 'aliases.alias', message: `alias ${index} must use the /lh:<verb> form` });
|
|
38
|
+
if (typeof item.canonical !== 'string' || !canonicalPattern.test(item.canonical)) errors.push({ code: 'aliases.canonical', message: `alias ${index} must target a /lighthouse:<verb> command` });
|
|
39
|
+
if (typeof item.asset !== 'string' || !assetPattern.test(item.asset)) errors.push({ code: 'aliases.asset', message: `alias ${index} must reference a packaged skill asset` });
|
|
40
|
+
if (aliases.has(item.alias)) errors.push({ code: 'aliases.duplicate', message: `alias ${item.alias} is declared more than once` });
|
|
41
|
+
if (canonicals.has(item.canonical)) errors.push({ code: 'aliases.duplicate-target', message: `canonical command ${item.canonical} has more than one alias` });
|
|
42
|
+
aliases.add(item.alias);
|
|
43
|
+
canonicals.add(item.canonical);
|
|
44
|
+
}
|
|
45
|
+
return errors;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function verifyCommandAliases(contract = readCommandAliases()) {
|
|
49
|
+
const errors = validateCommandAliases(contract);
|
|
50
|
+
for (const item of contract.aliases ?? []) {
|
|
51
|
+
if (typeof item.asset === 'string' && !(await exists(resolve(packageRoot, 'assets', item.asset)))) errors.push({ code: 'aliases.missing-asset', message: `alias ${item.alias} references missing asset ${item.asset}` });
|
|
52
|
+
}
|
|
53
|
+
return errors;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export { contractPath };
|
package/src/create-dotnet.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { lstat, mkdir, mkdtemp, readFile, readdir, rename, rm, rmdir, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
3
4
|
import { dirname, join, resolve } from 'node:path';
|
|
4
5
|
import { fileURLToPath } from 'node:url';
|
|
5
6
|
import { createRequire } from 'node:module';
|
|
@@ -9,6 +10,23 @@ const templateRoot = resolve(packageRoot, 'assets/dotnet-template');
|
|
|
9
10
|
const cliPackage = JSON.parse(await readFile(resolve(packageRoot, 'package.json'), 'utf8'));
|
|
10
11
|
const require = createRequire(import.meta.url);
|
|
11
12
|
const sdlcContract = JSON.parse(await readFile(require.resolve('@juangcarmona/lighthouse-sdlc/contract.json'), 'utf8'));
|
|
13
|
+
const sdlcPackageRoot = dirname(require.resolve('@juangcarmona/lighthouse-sdlc/contract.json'));
|
|
14
|
+
const sdlcWorkflowPaths = [
|
|
15
|
+
'.github/workflows/lighthouse-sdlc-router.yml',
|
|
16
|
+
'.github/workflows/lighthouse-sdlc-triage.md',
|
|
17
|
+
'.github/workflows/lighthouse-sdlc-triage.lock.yml',
|
|
18
|
+
'.github/workflows/lighthouse-sdlc-refine.md',
|
|
19
|
+
'.github/workflows/lighthouse-sdlc-refine.lock.yml',
|
|
20
|
+
'.github/workflows/lighthouse-sdlc-propose.md',
|
|
21
|
+
'.github/workflows/lighthouse-sdlc-propose.lock.yml',
|
|
22
|
+
'.github/workflows/lighthouse-sdlc-implement.md',
|
|
23
|
+
'.github/workflows/lighthouse-sdlc-implement.lock.yml',
|
|
24
|
+
'.github/workflows/lighthouse-sdlc-integrate.md',
|
|
25
|
+
'.github/workflows/lighthouse-sdlc-integrate.lock.yml',
|
|
26
|
+
'.github/workflows/lighthouse-sdlc-review.md',
|
|
27
|
+
'.github/workflows/lighthouse-sdlc-review.lock.yml',
|
|
28
|
+
'.github/workflows/shared/lighthouse-sdlc-stage.md',
|
|
29
|
+
];
|
|
12
30
|
const args = process.argv.slice(2);
|
|
13
31
|
const target = args[0] ? resolve(args[0]) : null;
|
|
14
32
|
const name = args.includes('--name') ? args[args.indexOf('--name') + 1] : null;
|
|
@@ -74,19 +92,23 @@ try {
|
|
|
74
92
|
await writeFile(solutionFile, `<Solution>\n${projects.map((project) => ` ${project}`).join('\n')}\n</Solution>\n`);
|
|
75
93
|
|
|
76
94
|
await copy(resolve(packageRoot, 'src/lighthouse.mjs'), resolve(stage, 'scripts/lighthouse.mjs'));
|
|
95
|
+
await copy(resolve(packageRoot, 'src/aliases.mjs'), resolve(stage, 'scripts/aliases.mjs'));
|
|
96
|
+
await copy(resolve(packageRoot, 'src/init-plan.mjs'), resolve(stage, 'scripts/init-plan.mjs'));
|
|
77
97
|
await copy(resolve(packageRoot, 'src/guardrails.mjs'), resolve(stage, 'scripts/guardrails.mjs'));
|
|
78
98
|
await copy(resolve(packageRoot, 'src/profiles.mjs'), resolve(stage, 'scripts/profiles.mjs'));
|
|
79
99
|
await copy(resolve(packageRoot, 'src/verify-dotnet.mjs'), resolve(stage, 'scripts/verify-dotnet.mjs'));
|
|
80
100
|
await copy(resolve(packageRoot, 'assets/guardrails'), resolve(stage, 'assets/guardrails'));
|
|
81
101
|
await copy(resolve(packageRoot, 'assets/profiles'), resolve(stage, 'assets/profiles'));
|
|
82
102
|
await copy(resolve(packageRoot, 'assets/schemas'), resolve(stage, 'schemas'));
|
|
103
|
+
await copy(resolve(packageRoot, 'assets/command-aliases.json'), resolve(stage, 'assets/command-aliases.json'));
|
|
104
|
+
await copy(resolve(packageRoot, 'assets/skills/init'), resolve(stage, 'assets/skills/init'));
|
|
83
105
|
|
|
84
106
|
await writeFile(resolve(stage, 'lighthouse.json'), `${JSON.stringify({
|
|
85
107
|
$schema: './schemas/lighthouse-manifest.schema.json', schemaVersion: 1, baseline: '0.1.0',
|
|
86
108
|
product: name, application: { shape: 'backend', stacks: ['dotnet'] }, profiles: ['dotnet-backend'],
|
|
87
109
|
integrations: { productShape: null, arcShape: null },
|
|
88
110
|
packages: { sdlc: { name: '@juangcarmona/lighthouse-sdlc', version: sdlcContract.version } },
|
|
89
|
-
managed: ['LIGHTHOUSE.md', 'scripts/lighthouse-guard.mjs', '.husky/commit-msg', '.husky/pre-push', '.github/workflows/lighthouse.yml', '.lighthouse/sdlc.json', '.lighthouse/upgrade.json', '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'],
|
|
111
|
+
managed: ['LIGHTHOUSE.md', 'scripts/lighthouse-guard.mjs', '.husky/commit-msg', '.husky/pre-push', '.github/workflows/lighthouse.yml', '.lighthouse/sdlc.json', ...sdlcWorkflowPaths, '.agents/skills/lighthouse-init/SKILL.md', '.lighthouse/upgrade.json', '.lighthouse/commands.json', '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'],
|
|
90
112
|
repository: {
|
|
91
113
|
layout: ['src', 'docs', 'infra', 'scripts'], publicSite: { enabled: true },
|
|
92
114
|
conventions: { commits: 'conventional', branchPattern: '^(feat|fix|docs|refactor|test|chore|build|ci|perf|revert)/[1-9]\\d*-[a-z0-9]+(?:-[a-z0-9]+)*$' },
|
|
@@ -103,6 +125,14 @@ try {
|
|
|
103
125
|
|
|
104
126
|
const initialized = (await import('node:child_process')).spawnSync(process.execPath, [resolve(stage, 'scripts/lighthouse.mjs'), 'init'], { cwd: stage, encoding: 'utf8' });
|
|
105
127
|
if (initialized.status !== 0) throw new Error(initialized.stderr || 'Projection generation failed.');
|
|
128
|
+
await copy(resolve(sdlcPackageRoot, 'workflows'), resolve(stage, '.github/workflows'));
|
|
129
|
+
const upgradePath = resolve(stage, '.lighthouse/upgrade.json');
|
|
130
|
+
const upgrade = JSON.parse(await readFile(upgradePath, 'utf8'));
|
|
131
|
+
for (const path of sdlcWorkflowPaths) {
|
|
132
|
+
const content = await readFile(resolve(stage, path), 'utf8');
|
|
133
|
+
upgrade.files[path] = { hash: createHash('sha256').update(content).digest('hex'), scope: 'sdlc' };
|
|
134
|
+
}
|
|
135
|
+
await writeFile(upgradePath, `${JSON.stringify(upgrade, null, 2)}\n`);
|
|
106
136
|
if (existing) await rmdir(target);
|
|
107
137
|
await rename(stage, target);
|
|
108
138
|
process.stdout.write(`Created ${name} in ${target}. Run dotnet restore ${name}.slnx, then npm run verify.\n`);
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
const productNamePattern = /^[A-Za-z0-9][A-Za-z0-9 ._-]{0,63}$/u;
|
|
2
|
+
const shapes = new Set(['unknown', 'frontend', 'backend', 'full-stack', 'service', 'library', 'tooling', 'cli', 'worker', 'mobile']);
|
|
3
|
+
const referenceKinds = new Set(['none', 'local', 'repository']);
|
|
4
|
+
|
|
5
|
+
function isObject(value) {
|
|
6
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function uniqueStrings(value, name, { required = false } = {}) {
|
|
10
|
+
if (!Array.isArray(value) || (required && value.length === 0) || value.some((item) => typeof item !== 'string' || item.length === 0)) {
|
|
11
|
+
return [`${name} must contain non-empty strings`];
|
|
12
|
+
}
|
|
13
|
+
return new Set(value).size === value.length ? [] : [`${name} must be unique`];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function referenceProblems(reference) {
|
|
17
|
+
if (reference === undefined || reference === null) return [];
|
|
18
|
+
if (!isObject(reference)) return ['reference must be null or an object'];
|
|
19
|
+
const problems = [];
|
|
20
|
+
if (!referenceKinds.has(reference.kind)) problems.push('reference.kind must be none, local or repository');
|
|
21
|
+
if (reference.kind !== 'none' && (typeof reference.location !== 'string' || reference.location.length === 0)) {
|
|
22
|
+
problems.push('reference.location is required for local and repository references');
|
|
23
|
+
}
|
|
24
|
+
if (reference.kind === 'none' && 'location' in reference) problems.push('reference.location is not allowed when reference.kind is none');
|
|
25
|
+
if ('purpose' in reference && (typeof reference.purpose !== 'string' || reference.purpose.length === 0)) problems.push('reference.purpose must be a non-empty string');
|
|
26
|
+
return problems;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function validateInitPlan(plan, supportedProfiles) {
|
|
30
|
+
if (!isObject(plan)) return ['plan must be an object'];
|
|
31
|
+
const problems = [];
|
|
32
|
+
const allowed = new Set(['$schema', 'schemaVersion', 'product', 'intent', 'application', 'profiles', 'reference', 'openQuestions']);
|
|
33
|
+
for (const key of Object.keys(plan)) if (!allowed.has(key)) problems.push(`unknown field ${key}`);
|
|
34
|
+
if (plan.schemaVersion !== 1) problems.push('schemaVersion must be 1');
|
|
35
|
+
if (typeof plan.product !== 'string' || !productNamePattern.test(plan.product)) problems.push('product must be a readable name of 1–64 characters');
|
|
36
|
+
if (typeof plan.intent !== 'string' || plan.intent.trim().length === 0) problems.push('intent must be a non-empty string');
|
|
37
|
+
if (!isObject(plan.application)) problems.push('application must be an object');
|
|
38
|
+
else {
|
|
39
|
+
if (!shapes.has(plan.application.shape)) problems.push(`application.shape must be one of ${[...shapes].join(', ')}`);
|
|
40
|
+
problems.push(...uniqueStrings(plan.application.stacks, 'application.stacks'));
|
|
41
|
+
}
|
|
42
|
+
problems.push(...uniqueStrings(plan.profiles, 'profiles'));
|
|
43
|
+
for (const profile of plan.profiles ?? []) {
|
|
44
|
+
if (!supportedProfiles.includes(profile)) problems.push(`unknown profile ${profile}; use ${supportedProfiles.join(', ')}`);
|
|
45
|
+
}
|
|
46
|
+
problems.push(...referenceProblems(plan.reference));
|
|
47
|
+
problems.push(...uniqueStrings(plan.openQuestions, 'openQuestions'));
|
|
48
|
+
return problems;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function hasUnresolvedQuestions(plan) {
|
|
52
|
+
return Array.isArray(plan.openQuestions) && plan.openQuestions.length > 0;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function manifestFromInitPlan(plan, createManifest) {
|
|
56
|
+
const manifest = createManifest(plan.product, plan.profiles);
|
|
57
|
+
manifest.application = {
|
|
58
|
+
shape: plan.application.shape,
|
|
59
|
+
stacks: [...plan.application.stacks],
|
|
60
|
+
};
|
|
61
|
+
return manifest;
|
|
62
|
+
}
|
package/src/lighthouse.mjs
CHANGED
|
@@ -7,9 +7,13 @@ 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;
|
|
16
|
+
let sdlcEvidence;
|
|
13
17
|
try {
|
|
14
18
|
[sdlcCapabilities, sdlcReconcile, sdlcGithub] = await Promise.all([
|
|
15
19
|
import('@juangcarmona/lighthouse-sdlc/capabilities'),
|
|
@@ -17,6 +21,8 @@ try {
|
|
|
17
21
|
import('@juangcarmona/lighthouse-sdlc/github'),
|
|
18
22
|
]);
|
|
19
23
|
} catch { /* Standalone generated starters can run without the optional SDLC package. */ }
|
|
24
|
+
try { sdlcAliases = await import('@juangcarmona/lighthouse-sdlc/aliases'); } catch { /* Older standalone installations may not publish aliases. */ }
|
|
25
|
+
try { sdlcEvidence = await import('@juangcarmona/lighthouse-sdlc/evidence'); } catch { /* Older standalone installations may not publish evidence. */ }
|
|
20
26
|
|
|
21
27
|
const root = process.cwd();
|
|
22
28
|
const packageRequire = createRequire(import.meta.url);
|
|
@@ -34,6 +40,22 @@ const upgradeStatePath = resolve(root, upgradeStateRelative);
|
|
|
34
40
|
const layout = ['src', 'docs', 'infra', 'scripts'];
|
|
35
41
|
const sdlcPackage = { name: '@juangcarmona/lighthouse-sdlc', version: installedPackageVersion('@juangcarmona/lighthouse-sdlc', 'contract.json') ?? '0.1.0' };
|
|
36
42
|
const uiPackage = { name: '@juangcarmona/lighthouse-ui', version: installedPackageVersion('@juangcarmona/lighthouse-ui', 'DESIGN.md') ?? '0.1.0' };
|
|
43
|
+
const sdlcWorkflowPaths = Object.freeze([
|
|
44
|
+
'.github/workflows/lighthouse-sdlc-router.yml',
|
|
45
|
+
'.github/workflows/lighthouse-sdlc-triage.md',
|
|
46
|
+
'.github/workflows/lighthouse-sdlc-triage.lock.yml',
|
|
47
|
+
'.github/workflows/lighthouse-sdlc-refine.md',
|
|
48
|
+
'.github/workflows/lighthouse-sdlc-refine.lock.yml',
|
|
49
|
+
'.github/workflows/lighthouse-sdlc-propose.md',
|
|
50
|
+
'.github/workflows/lighthouse-sdlc-propose.lock.yml',
|
|
51
|
+
'.github/workflows/lighthouse-sdlc-implement.md',
|
|
52
|
+
'.github/workflows/lighthouse-sdlc-implement.lock.yml',
|
|
53
|
+
'.github/workflows/lighthouse-sdlc-integrate.md',
|
|
54
|
+
'.github/workflows/lighthouse-sdlc-integrate.lock.yml',
|
|
55
|
+
'.github/workflows/lighthouse-sdlc-review.md',
|
|
56
|
+
'.github/workflows/lighthouse-sdlc-review.lock.yml',
|
|
57
|
+
'.github/workflows/shared/lighthouse-sdlc-stage.md',
|
|
58
|
+
]);
|
|
37
59
|
const managedPaths = [
|
|
38
60
|
projection,
|
|
39
61
|
'scripts/lighthouse-guard.mjs',
|
|
@@ -41,7 +63,10 @@ const managedPaths = [
|
|
|
41
63
|
'.husky/pre-push',
|
|
42
64
|
'.github/workflows/lighthouse.yml',
|
|
43
65
|
'.lighthouse/sdlc.json',
|
|
66
|
+
...sdlcWorkflowPaths,
|
|
67
|
+
'.agents/skills/lighthouse-init/SKILL.md',
|
|
44
68
|
upgradeStateRelative,
|
|
69
|
+
'.lighthouse/commands.json',
|
|
45
70
|
];
|
|
46
71
|
const profileManagedPaths = Object.freeze({
|
|
47
72
|
'visual-theme': ['docs/design/DESIGN.md', '.agents/skills/lighthouse-ui/SKILL.md'],
|
|
@@ -52,13 +77,15 @@ const profileManagedPaths = Object.freeze({
|
|
|
52
77
|
'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'],
|
|
53
78
|
'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'],
|
|
54
79
|
});
|
|
55
|
-
const previousManagedPaths =
|
|
80
|
+
const previousManagedPaths = [projection, 'scripts/lighthouse-guard.mjs', '.husky/commit-msg', '.husky/pre-push', '.github/workflows/lighthouse.yml', '.lighthouse/sdlc.json', '.agents/skills/lighthouse-init/SKILL.md'];
|
|
56
81
|
const legacyManagedPaths = [projection];
|
|
82
|
+
const preSemanticManagedPaths = [projection, 'scripts/lighthouse-guard.mjs', '.husky/commit-msg', '.husky/pre-push', '.github/workflows/lighthouse.yml', '.lighthouse/sdlc.json', upgradeStateRelative];
|
|
83
|
+
const preSemanticWithoutUpgradePaths = preSemanticManagedPaths.slice(0, -1);
|
|
57
84
|
const supportedProfiles = readProfileCatalog().profiles.map(({ id }) => id);
|
|
58
85
|
const branchPattern = '^(feat|fix|docs|refactor|test|chore|build|ci|perf|revert)/[1-9]\\d*-[a-z0-9]+(?:-[a-z0-9]+)*$';
|
|
59
|
-
const authorityVersions = { productShape: '0.
|
|
86
|
+
const authorityVersions = { productShape: '0.23.0', arcShape: '0.1.6' };
|
|
60
87
|
const upgradeSchema = './node_modules/@juangcarmona/lighthouse/assets/schemas/lighthouse-upgrade.schema.json';
|
|
61
|
-
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]';
|
|
88
|
+
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]';
|
|
62
89
|
|
|
63
90
|
const defaultRepository = () => ({
|
|
64
91
|
layout: [...layout],
|
|
@@ -73,8 +100,8 @@ function managedPathsFor(manifest) {
|
|
|
73
100
|
|
|
74
101
|
function upgradeScopesFor(manifest) {
|
|
75
102
|
return {
|
|
76
|
-
harness: [projection, 'scripts/lighthouse-guard.mjs', '.husky/commit-msg', '.husky/pre-push', '.github/workflows/lighthouse.yml', ...((manifest?.profiles ?? []).flatMap((profile) => profileManagedPaths[profile] ?? []))],
|
|
77
|
-
sdlc: ['.lighthouse/sdlc.json'],
|
|
103
|
+
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] ?? []))],
|
|
104
|
+
sdlc: ['.lighthouse/sdlc.json', '.lighthouse/commands.json', ...sdlcWorkflowPaths],
|
|
78
105
|
};
|
|
79
106
|
}
|
|
80
107
|
|
|
@@ -83,6 +110,18 @@ function packageAsset(packageName, entry) {
|
|
|
83
110
|
return readFileSync(path, 'utf8');
|
|
84
111
|
}
|
|
85
112
|
|
|
113
|
+
function sdlcWorkflowFiles() {
|
|
114
|
+
const files = {};
|
|
115
|
+
for (const target of sdlcWorkflowPaths) {
|
|
116
|
+
try {
|
|
117
|
+
files[target] = packageAsset('@juangcarmona/lighthouse-sdlc', `workflows/${target.slice('.github/workflows/'.length)}`);
|
|
118
|
+
} catch {
|
|
119
|
+
return {};
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return files;
|
|
123
|
+
}
|
|
124
|
+
|
|
86
125
|
function profileAssets(manifest) {
|
|
87
126
|
const assets = {};
|
|
88
127
|
if ((manifest?.profiles ?? []).includes('visual-theme')) {
|
|
@@ -270,7 +309,7 @@ function render(manifest) {
|
|
|
270
309
|
const sdlcLine = selectedSdlc ? `- SDLC package: \`${selectedSdlc.name}@${selectedSdlc.version}\`\n` : '';
|
|
271
310
|
const selectedUi = manifest.packages?.ui;
|
|
272
311
|
const uiLine = selectedUi ? `- UI package: \`${selectedUi.name}@${selectedUi.version}\`\n` : '';
|
|
273
|
-
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`;
|
|
312
|
+
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`;
|
|
274
313
|
}
|
|
275
314
|
|
|
276
315
|
function guardrailScript(manifest) {
|
|
@@ -354,6 +393,59 @@ jobs:
|
|
|
354
393
|
`;
|
|
355
394
|
}
|
|
356
395
|
|
|
396
|
+
function readSdlcCommandAliases() {
|
|
397
|
+
try {
|
|
398
|
+
const path = packageRequire.resolve('@juangcarmona/lighthouse-sdlc/command-aliases.json');
|
|
399
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
400
|
+
} catch { return null; }
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function commandAliasesProjection() {
|
|
404
|
+
const contracts = [{ contract: readCommandAliases(), packageVersion: cliPackage.version }];
|
|
405
|
+
const sdlc = readSdlcCommandAliases();
|
|
406
|
+
if (sdlc) contracts.push({ contract: sdlc, packageVersion: sdlcPackage.version });
|
|
407
|
+
const aliases = contracts.flatMap(({ contract, packageVersion }) => (contract.aliases ?? []).map((alias) => ({
|
|
408
|
+
...alias,
|
|
409
|
+
owner: contract.package.name,
|
|
410
|
+
packageVersion,
|
|
411
|
+
})));
|
|
412
|
+
return {
|
|
413
|
+
schemaVersion: 1,
|
|
414
|
+
contractVersion: 1,
|
|
415
|
+
canonicalNamespace: '/lighthouse',
|
|
416
|
+
aliasNamespace: '/lh',
|
|
417
|
+
packages: contracts.map(({ contract, packageVersion }) => ({
|
|
418
|
+
name: contract.package.name,
|
|
419
|
+
version: packageVersion,
|
|
420
|
+
contractVersion: contract.contractVersion,
|
|
421
|
+
aliases: contract.aliases,
|
|
422
|
+
})),
|
|
423
|
+
aliases,
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function commandAliasProjectionText(projection) {
|
|
428
|
+
return `${JSON.stringify(projection, null, 2)}\n`;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
async function commandAliases(format = 'text') {
|
|
432
|
+
const harness = readCommandAliases();
|
|
433
|
+
const errors = await verifyCommandAliases(harness);
|
|
434
|
+
const sdlc = readSdlcCommandAliases();
|
|
435
|
+
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.' });
|
|
436
|
+
else if (sdlcAliases) errors.push(...(await sdlcAliases.verifyCommandAliases(sdlc)));
|
|
437
|
+
else errors.push({ code: 'aliases.sdlc-validator-unavailable', message: 'The installed SDLC package does not publish its alias validator.' });
|
|
438
|
+
const projection = commandAliasesProjection();
|
|
439
|
+
const report = { ...projection, status: errors.length ? 'invalid' : 'valid', valid: errors.length === 0, errors };
|
|
440
|
+
if (format === 'json') console.log(JSON.stringify(report, null, 2));
|
|
441
|
+
else {
|
|
442
|
+
console.log(`Lighthouse command aliases: ${report.status}`);
|
|
443
|
+
for (const alias of report.aliases) console.log(` ${alias.alias} -> ${alias.canonical}`);
|
|
444
|
+
for (const error of report.errors) console.error(` ${error.code}: ${error.message}`);
|
|
445
|
+
}
|
|
446
|
+
if (errors.length) process.exitCode = 1;
|
|
447
|
+
}
|
|
448
|
+
|
|
357
449
|
function managedFiles(manifest) {
|
|
358
450
|
const files = {
|
|
359
451
|
[projection]: render(manifest),
|
|
@@ -361,10 +453,13 @@ function managedFiles(manifest) {
|
|
|
361
453
|
'.husky/commit-msg': commitHook(),
|
|
362
454
|
'.husky/pre-push': prePushHook(),
|
|
363
455
|
'.github/workflows/lighthouse.yml': workflow(),
|
|
456
|
+
'.agents/skills/lighthouse-init/SKILL.md': readFileSync(new URL('../assets/skills/init/SKILL.md', import.meta.url), 'utf8'),
|
|
364
457
|
};
|
|
365
458
|
Object.assign(files, profileAssets(manifest));
|
|
459
|
+
Object.assign(files, sdlcWorkflowFiles());
|
|
366
460
|
const controlPlane = sdlcControlPlane(manifest);
|
|
367
461
|
if (controlPlane) files['.lighthouse/sdlc.json'] = controlPlane;
|
|
462
|
+
files['.lighthouse/commands.json'] = commandAliasProjectionText(commandAliasesProjection());
|
|
368
463
|
return files;
|
|
369
464
|
}
|
|
370
465
|
|
|
@@ -586,7 +681,13 @@ function validate(manifest) {
|
|
|
586
681
|
const previousCompleteManaged = Array.isArray(manifest.managed)
|
|
587
682
|
&& manifest.managed.length === previousManagedPaths.length
|
|
588
683
|
&& manifest.managed.every((entry, index) => entry === previousManagedPaths[index]);
|
|
589
|
-
|
|
684
|
+
const preSemanticManaged = Array.isArray(manifest.managed)
|
|
685
|
+
&& manifest.managed.length === preSemanticManagedPaths.length
|
|
686
|
+
&& manifest.managed.every((entry, index) => entry === preSemanticManagedPaths[index]);
|
|
687
|
+
const preSemanticWithoutUpgrade = Array.isArray(manifest.managed)
|
|
688
|
+
&& manifest.managed.length === preSemanticWithoutUpgradePaths.length
|
|
689
|
+
&& manifest.managed.every((entry, index) => entry === preSemanticWithoutUpgradePaths[index]);
|
|
690
|
+
if (!legacyManaged && !profilelessManaged && !previousCompleteManaged && !completeManaged && !preSemanticManaged && !preSemanticWithoutUpgrade) {
|
|
590
691
|
problems.push(`managed must be ${legacyManagedPaths.join(', ')}, the previous Lighthouse guardrail set or the selected profile set`);
|
|
591
692
|
}
|
|
592
693
|
if ('repository' in manifest) {
|
|
@@ -637,6 +738,103 @@ function productNameFromArgs(args) {
|
|
|
637
738
|
return value;
|
|
638
739
|
}
|
|
639
740
|
|
|
741
|
+
function valueFromArgs(args, name) {
|
|
742
|
+
const index = args.indexOf(name);
|
|
743
|
+
if (index !== -1) return args[index + 1];
|
|
744
|
+
return args.find((arg) => arg.startsWith(`${name}=`))?.slice(name.length + 1);
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
async function readInitPlan(planFile) {
|
|
748
|
+
let plan;
|
|
749
|
+
try { plan = JSON.parse(await readFile(resolve(root, planFile), 'utf8')); }
|
|
750
|
+
catch (error) { throw new Error(`Cannot read initialization plan ${planFile}: ${error.message}`); }
|
|
751
|
+
const problems = validateInitPlan(plan, supportedProfiles);
|
|
752
|
+
if (problems.length) throw new Error(`Invalid initialization plan:\n${problems.map((problem) => ` ${problem}`).join('\n')}`);
|
|
753
|
+
return plan;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
function printInitializationPlan(plan, result, format) {
|
|
757
|
+
const report = {
|
|
758
|
+
schemaVersion: 1,
|
|
759
|
+
intent: plan.intent,
|
|
760
|
+
product: plan.product,
|
|
761
|
+
application: plan.application,
|
|
762
|
+
profiles: plan.profiles,
|
|
763
|
+
reference: plan.reference ?? null,
|
|
764
|
+
openQuestions: plan.openQuestions,
|
|
765
|
+
status: result.plan.conflicts.length || plan.openQuestions.length ? 'blocked' : 'ready',
|
|
766
|
+
manifest: {
|
|
767
|
+
product: result.manifest.product,
|
|
768
|
+
application: result.manifest.application,
|
|
769
|
+
profiles: result.manifest.profiles,
|
|
770
|
+
},
|
|
771
|
+
operations: result.plan.operations.map(({ path, action }) => ({ path, action })),
|
|
772
|
+
conflicts: result.plan.conflicts,
|
|
773
|
+
apply: 'lighthouse init --plan-file <path> --apply',
|
|
774
|
+
};
|
|
775
|
+
if (format === 'json') {
|
|
776
|
+
console.log(JSON.stringify(report, null, 2));
|
|
777
|
+
return;
|
|
778
|
+
}
|
|
779
|
+
console.log('Lighthouse semantic initialization plan:');
|
|
780
|
+
console.log(` product ${plan.product}`);
|
|
781
|
+
console.log(` intent ${plan.intent}`);
|
|
782
|
+
console.log(` application ${plan.application.shape} (${plan.application.stacks.join(', ') || 'no stack'})`);
|
|
783
|
+
console.log(` profiles ${plan.profiles.join(', ') || 'none'}`);
|
|
784
|
+
console.log(` reference ${plan.reference?.kind ?? 'none'}`);
|
|
785
|
+
if (plan.openQuestions.length) for (const question of plan.openQuestions) console.error(` question ${question}`);
|
|
786
|
+
for (const operation of result.plan.operations) console.log(` ${operation.action} ${operation.path}`);
|
|
787
|
+
for (const conflict of result.plan.conflicts) console.error(` conflict ${conflict}`);
|
|
788
|
+
console.log(' default is dry-run; apply only after review with --apply');
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
async function initializeFromPlan(args, planFile) {
|
|
792
|
+
const plan = await readInitPlan(planFile);
|
|
793
|
+
const format = formatFromArgs(args);
|
|
794
|
+
const apply = args.includes('--apply');
|
|
795
|
+
const dryRun = args.includes('--dry-run') || !apply;
|
|
796
|
+
if (args.includes('--apply') && args.includes('--dry-run')) throw new Error('--apply and --dry-run cannot be used together.');
|
|
797
|
+
if (format === 'json' && apply) throw new Error('--format json is available for semantic initialization dry runs; omit it when applying.');
|
|
798
|
+
const existing = await readManifest();
|
|
799
|
+
let manifest;
|
|
800
|
+
if (existing) {
|
|
801
|
+
const problems = validate(existing);
|
|
802
|
+
if (problems.length) throw new Error(problems.map((problem) => `Invalid lighthouse.json: ${problem}`).join('\n'));
|
|
803
|
+
if (existing.product !== plan.product) throw new Error(`Initialization plan targets ${plan.product}, but lighthouse.json belongs to ${existing.product}.`);
|
|
804
|
+
if (JSON.stringify(existing.profiles ?? []) !== JSON.stringify(plan.profiles)) throw new Error('Initialization plan profiles differ from lighthouse.json; resolve the selection before applying.');
|
|
805
|
+
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.');
|
|
806
|
+
manifest = existing;
|
|
807
|
+
} else {
|
|
808
|
+
manifest = manifestFromInitPlan(plan, defaultManifest);
|
|
809
|
+
}
|
|
810
|
+
const problems = validate(manifest);
|
|
811
|
+
if (problems.length) throw new Error(problems.map((problem) => `Invalid lighthouse.json: ${problem}`).join('\n'));
|
|
812
|
+
const composition = composeProfiles(readProfileCatalog(), manifest);
|
|
813
|
+
if (composition.status === 'failed') throw new Error(`Initialization plan has incompatible profiles:\n${composition.selected.flatMap((profile) => profile.problems).map((problem) => ` ${problem}`).join('\n')}`);
|
|
814
|
+
const initialization = { manifest, plan: await planInitialization(manifest, !existing) };
|
|
815
|
+
printInitializationPlan(plan, initialization, format);
|
|
816
|
+
if (initialization.plan.conflicts.length) {
|
|
817
|
+
if (dryRun) process.exitCode = 1;
|
|
818
|
+
else throw new Error('Initialization stopped; resolve the reported conflicts and run the plan again.');
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
821
|
+
if (hasUnresolvedQuestions(plan)) {
|
|
822
|
+
if (dryRun) process.exitCode = 1;
|
|
823
|
+
else throw new Error('Initialization stopped; answer the open questions before using --apply.');
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
826
|
+
if (dryRun) return;
|
|
827
|
+
if (!existing) await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, { flag: 'wx' });
|
|
828
|
+
for (const operation of initialization.plan.operations.filter((item) => item.path.endsWith('/'))) await mkdir(resolve(root, operation.path), { recursive: true });
|
|
829
|
+
const files = managedFiles(manifest);
|
|
830
|
+
for (const [file, content] of Object.entries(files)) {
|
|
831
|
+
await writeFile(resolve(root, file), content, { flag: 'wx', mode: file.startsWith('.husky/') ? 0o755 : 0o644 });
|
|
832
|
+
if (file.startsWith('.husky/')) await chmod(resolve(root, file), 0o755);
|
|
833
|
+
}
|
|
834
|
+
await writeUpgradeState(manifest, null, files);
|
|
835
|
+
console.log(`Initialized ${manifest.product} from the semantic plan. Run lighthouse verify.`);
|
|
836
|
+
}
|
|
837
|
+
|
|
640
838
|
async function planInitialization(manifest, manifestMissing) {
|
|
641
839
|
const operations = [];
|
|
642
840
|
const conflicts = [];
|
|
@@ -694,6 +892,8 @@ function printReconciliationPlan(plan) {
|
|
|
694
892
|
}
|
|
695
893
|
|
|
696
894
|
async function initialize(args) {
|
|
895
|
+
const planFile = valueFromArgs(args, '--plan-file');
|
|
896
|
+
if (planFile) return initializeFromPlan(args, planFile);
|
|
697
897
|
let manifest = await readManifest();
|
|
698
898
|
const manifestMissing = !manifest;
|
|
699
899
|
const selectedProfiles = profileArguments(args);
|
|
@@ -854,6 +1054,25 @@ const authorityCommands = [
|
|
|
854
1054
|
},
|
|
855
1055
|
];
|
|
856
1056
|
|
|
1057
|
+
const authorityAdoptionCommands = [
|
|
1058
|
+
{
|
|
1059
|
+
id: 'productShape',
|
|
1060
|
+
label: 'ProductShape',
|
|
1061
|
+
packageName: '@prodshape/cli',
|
|
1062
|
+
command: 'prodshape',
|
|
1063
|
+
path: 'docs/product/model',
|
|
1064
|
+
validateArgs: ['validate', '--format', 'json'],
|
|
1065
|
+
},
|
|
1066
|
+
{
|
|
1067
|
+
id: 'arcShape',
|
|
1068
|
+
label: 'ArcShape',
|
|
1069
|
+
packageName: '@arcshape/cli',
|
|
1070
|
+
command: 'arcshape',
|
|
1071
|
+
path: 'docs/architecture',
|
|
1072
|
+
validateArgs: ['validate', '--docs', 'docs/architecture', '--links', '--citations', '--format', 'json'],
|
|
1073
|
+
},
|
|
1074
|
+
];
|
|
1075
|
+
|
|
857
1076
|
function providerExecutable(authority) {
|
|
858
1077
|
const packageJson = packageRequire.resolve(`${authority.packageName}/package.json`);
|
|
859
1078
|
const metadata = JSON.parse(readFileSync(packageJson, 'utf8'));
|
|
@@ -887,6 +1106,132 @@ function runAuthorityCommand(authority, dryRun) {
|
|
|
887
1106
|
};
|
|
888
1107
|
}
|
|
889
1108
|
|
|
1109
|
+
function parseProviderReport(output) {
|
|
1110
|
+
const text = output.trim();
|
|
1111
|
+
try { return JSON.parse(text); } catch { /* The provider may prefix JSON with a diagnostic line. */ }
|
|
1112
|
+
const start = text.indexOf('{');
|
|
1113
|
+
const end = text.lastIndexOf('}');
|
|
1114
|
+
if (start === -1 || end <= start) return null;
|
|
1115
|
+
try { return JSON.parse(text.slice(start, end + 1)); } catch { return null; }
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
function authorityStatus(present, validation, recordedVersion, providerVersion) {
|
|
1119
|
+
if (!present) return 'absent';
|
|
1120
|
+
if (validation.status === 'unavailable') return 'unavailable';
|
|
1121
|
+
if (!validation.valid) return 'invalid';
|
|
1122
|
+
if (recordedVersion && recordedVersion !== providerVersion) return 'drifted';
|
|
1123
|
+
return recordedVersion ? 'adopted' : 'ready';
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
async function runAuthorityValidation(authority, manifest) {
|
|
1127
|
+
const providerVersion = installedPackageVersion(authority.packageName, 'package.json');
|
|
1128
|
+
const present = await isDirectory(resolve(root, authority.path));
|
|
1129
|
+
const command = `${authority.command} ${authority.validateArgs.join(' ')}`;
|
|
1130
|
+
if (!present) {
|
|
1131
|
+
return {
|
|
1132
|
+
id: authority.id,
|
|
1133
|
+
label: authority.label,
|
|
1134
|
+
provider: authority.packageName,
|
|
1135
|
+
version: providerVersion,
|
|
1136
|
+
path: authority.path,
|
|
1137
|
+
command,
|
|
1138
|
+
recordedVersion: manifest.integrations[authority.id],
|
|
1139
|
+
status: 'absent',
|
|
1140
|
+
valid: false,
|
|
1141
|
+
diagnostics: [`${authority.path} is missing`],
|
|
1142
|
+
nextAction: `Create the ${authority.label} authority through its supported initializer, then rerun lighthouse authorities --adopt-existing.`,
|
|
1143
|
+
};
|
|
1144
|
+
}
|
|
1145
|
+
if (!providerVersion) {
|
|
1146
|
+
return {
|
|
1147
|
+
id: authority.id,
|
|
1148
|
+
label: authority.label,
|
|
1149
|
+
provider: authority.packageName,
|
|
1150
|
+
version: null,
|
|
1151
|
+
path: authority.path,
|
|
1152
|
+
command,
|
|
1153
|
+
recordedVersion: manifest.integrations[authority.id],
|
|
1154
|
+
status: 'unavailable',
|
|
1155
|
+
valid: false,
|
|
1156
|
+
diagnostics: [`${authority.packageName} is not installed`],
|
|
1157
|
+
nextAction: `Install ${authority.packageName}, then rerun lighthouse authorities --adopt-existing.`,
|
|
1158
|
+
};
|
|
1159
|
+
}
|
|
1160
|
+
let result;
|
|
1161
|
+
try {
|
|
1162
|
+
result = spawnSync(process.execPath, [providerExecutable(authority), ...authority.validateArgs], {
|
|
1163
|
+
cwd: root,
|
|
1164
|
+
encoding: 'utf8',
|
|
1165
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
1166
|
+
});
|
|
1167
|
+
} catch (error) {
|
|
1168
|
+
return {
|
|
1169
|
+
id: authority.id,
|
|
1170
|
+
label: authority.label,
|
|
1171
|
+
provider: authority.packageName,
|
|
1172
|
+
version: providerVersion,
|
|
1173
|
+
path: authority.path,
|
|
1174
|
+
command,
|
|
1175
|
+
recordedVersion: manifest.integrations[authority.id],
|
|
1176
|
+
status: 'unavailable',
|
|
1177
|
+
valid: false,
|
|
1178
|
+
diagnostics: [error.message],
|
|
1179
|
+
nextAction: `Run ${command} directly, repair the provider and rerun lighthouse authorities --adopt-existing.`,
|
|
1180
|
+
};
|
|
1181
|
+
}
|
|
1182
|
+
const output = `${result.stdout ?? ''}${result.stderr ?? ''}`;
|
|
1183
|
+
const report = parseProviderReport(output);
|
|
1184
|
+
const valid = result.status === 0 && report?.valid !== false;
|
|
1185
|
+
const status = authorityStatus(present, { status: result.status === null ? 'unavailable' : 'completed', valid }, manifest.integrations[authority.id], providerVersion);
|
|
1186
|
+
return {
|
|
1187
|
+
id: authority.id,
|
|
1188
|
+
label: authority.label,
|
|
1189
|
+
provider: authority.packageName,
|
|
1190
|
+
version: providerVersion,
|
|
1191
|
+
path: authority.path,
|
|
1192
|
+
command,
|
|
1193
|
+
recordedVersion: manifest.integrations[authority.id],
|
|
1194
|
+
status,
|
|
1195
|
+
valid,
|
|
1196
|
+
diagnostics: report?.diagnostics ?? (valid ? [] : [output.trim().split('\n').filter(Boolean).slice(-3).join(' ') || `${authority.label} validation failed`]),
|
|
1197
|
+
nextAction: status === 'invalid' ? `Run ${command} directly, repair the reported authority and rerun lighthouse authorities --adopt-existing.` : null,
|
|
1198
|
+
};
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
async function authorityAdoptionPlan(manifest) {
|
|
1202
|
+
const authorities = await Promise.all(authorityAdoptionCommands.map((authority) => runAuthorityValidation(authority, manifest)));
|
|
1203
|
+
const blocking = authorities.filter((authority) => ['absent', 'invalid', 'unavailable'].includes(authority.status));
|
|
1204
|
+
const changed = authorities.some((authority) => ['ready', 'drifted'].includes(authority.status));
|
|
1205
|
+
const status = blocking.length ? 'blocked' : changed ? 'ready' : 'adopted';
|
|
1206
|
+
return {
|
|
1207
|
+
schemaVersion: 1,
|
|
1208
|
+
command: 'lighthouse authorities --adopt-existing',
|
|
1209
|
+
status,
|
|
1210
|
+
remoteWrites: 'none',
|
|
1211
|
+
providerWrites: 'none',
|
|
1212
|
+
authorities,
|
|
1213
|
+
operations: changed ? [
|
|
1214
|
+
{ action: 'record', path: 'lighthouse.json', values: Object.fromEntries(authorities.map((authority) => [authority.id, authority.version])) },
|
|
1215
|
+
{ action: 'regenerate', path: 'LIGHTHOUSE.md' },
|
|
1216
|
+
] : [],
|
|
1217
|
+
apply: 'lighthouse authorities --adopt-existing --apply',
|
|
1218
|
+
};
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
function printAuthorityAdoptionPlan(plan) {
|
|
1222
|
+
console.log(`Lighthouse existing-authority adoption plan: ${plan.status}`);
|
|
1223
|
+
console.log(' ProductShape and ArcShape provider files remain untouched.');
|
|
1224
|
+
console.log(' Remote writes: none.');
|
|
1225
|
+
for (const authority of plan.authorities) {
|
|
1226
|
+
const version = authority.version ?? 'unavailable';
|
|
1227
|
+
console.log(` ${authority.label}: ${authority.status} (${version}) ${authority.path}`);
|
|
1228
|
+
for (const diagnostic of authority.diagnostics) console.error(` ${diagnostic}`);
|
|
1229
|
+
if (authority.nextAction) console.error(` next: ${authority.nextAction}`);
|
|
1230
|
+
}
|
|
1231
|
+
for (const operation of plan.operations) console.log(` ${operation.action} ${operation.path}`);
|
|
1232
|
+
console.log(' default is dry-run; apply only after review with --apply');
|
|
1233
|
+
}
|
|
1234
|
+
|
|
890
1235
|
async function writeManifestAtomically(manifest) {
|
|
891
1236
|
const temporary = `${manifestPath}.tmp-${process.pid}`;
|
|
892
1237
|
await writeFile(temporary, `${JSON.stringify(manifest, null, 2)}\n`, { flag: 'wx' });
|
|
@@ -907,7 +1252,7 @@ async function ensureProjectionReady(manifest) {
|
|
|
907
1252
|
|
|
908
1253
|
async function writeProjection(manifest) {
|
|
909
1254
|
const target = resolve(root, projection);
|
|
910
|
-
await
|
|
1255
|
+
await writeTextAtomically(target, render(manifest));
|
|
911
1256
|
}
|
|
912
1257
|
|
|
913
1258
|
async function authorities(args) {
|
|
@@ -945,6 +1290,37 @@ async function authorities(args) {
|
|
|
945
1290
|
console.log(`Recorded ProductShape ${authorityVersions.productShape} and ArcShape ${authorityVersions.arcShape} in lighthouse.json.`);
|
|
946
1291
|
}
|
|
947
1292
|
|
|
1293
|
+
async function adoptExistingAuthorities(args) {
|
|
1294
|
+
const manifest = await readManifest();
|
|
1295
|
+
if (!manifest) throw new Error('Cannot read lighthouse.json: file does not exist; run lighthouse init --name <product>.');
|
|
1296
|
+
const problems = validate(manifest);
|
|
1297
|
+
if (problems.length) throw new Error(problems.map((problem) => `Invalid lighthouse.json: ${problem}`).join('\n'));
|
|
1298
|
+
const apply = args.includes('--apply');
|
|
1299
|
+
const dryRun = args.includes('--dry-run') || !apply;
|
|
1300
|
+
const format = formatFromArgs(args);
|
|
1301
|
+
if (args.includes('--apply') && args.includes('--dry-run')) throw new Error('--apply and --dry-run cannot be used together.');
|
|
1302
|
+
if (format === 'json' && apply) throw new Error('--format json is available for existing-authority adoption dry runs; omit it when applying.');
|
|
1303
|
+
const plan = await authorityAdoptionPlan(manifest);
|
|
1304
|
+
if (format === 'json') console.log(JSON.stringify(plan, null, 2));
|
|
1305
|
+
else printAuthorityAdoptionPlan(plan);
|
|
1306
|
+
if (dryRun) {
|
|
1307
|
+
if (plan.status === 'blocked') process.exitCode = 1;
|
|
1308
|
+
return;
|
|
1309
|
+
}
|
|
1310
|
+
if (plan.status === 'blocked') throw new Error('Existing-authority adoption stopped; resolve the reported provider conditions and rerun the plan.');
|
|
1311
|
+
await ensureProjectionReady(manifest);
|
|
1312
|
+
const updated = {
|
|
1313
|
+
...manifest,
|
|
1314
|
+
integrations: Object.fromEntries(plan.authorities.map((authority) => [authority.id, authority.version])),
|
|
1315
|
+
};
|
|
1316
|
+
if (JSON.stringify(updated) !== JSON.stringify(manifest)) await writeManifestAtomically(updated);
|
|
1317
|
+
if (!await exists(resolve(root, projection))) await writeProjection(updated);
|
|
1318
|
+
else if (await readFile(resolve(root, projection), 'utf8') !== render(updated)) await writeProjection(updated);
|
|
1319
|
+
const verified = await authorityAdoptionPlan(updated);
|
|
1320
|
+
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.');
|
|
1321
|
+
console.log('Recorded and verified the existing ProductShape and ArcShape authorities. Provider-owned files were not changed.');
|
|
1322
|
+
}
|
|
1323
|
+
|
|
948
1324
|
function gitBranch() {
|
|
949
1325
|
try {
|
|
950
1326
|
return execFileSync('git', ['branch', '--show-current'], {
|
|
@@ -994,6 +1370,40 @@ async function verify(manifest, format = 'text') {
|
|
|
994
1370
|
const profileResult = profileCompositionResult(profileComposition);
|
|
995
1371
|
report.results.push(profileResult);
|
|
996
1372
|
report.summary[profileResult.status] = (report.summary[profileResult.status] ?? 0) + 1;
|
|
1373
|
+
if (validManifest) {
|
|
1374
|
+
const recordedAuthorities = [manifest.integrations.productShape, manifest.integrations.arcShape];
|
|
1375
|
+
const anyRecorded = recordedAuthorities.some((version) => version !== null);
|
|
1376
|
+
const authorityPlan = anyRecorded ? await authorityAdoptionPlan(manifest) : null;
|
|
1377
|
+
const authorityStatuses = authorityPlan?.authorities.map((authority) => authority.status) ?? [];
|
|
1378
|
+
const authorityStatus = !anyRecorded ? 'unconfigured'
|
|
1379
|
+
: authorityStatuses.includes('unavailable') ? 'unavailable'
|
|
1380
|
+
: authorityStatuses.some((status) => ['absent', 'invalid'].includes(status)) ? 'failed'
|
|
1381
|
+
: authorityStatuses.includes('drifted') ? 'drift'
|
|
1382
|
+
: 'passed';
|
|
1383
|
+
const authorityProblems = !anyRecorded
|
|
1384
|
+
? ['ProductShape and ArcShape are not recorded; run lighthouse authorities --adopt-existing after reviewing its dry-run plan.']
|
|
1385
|
+
: authorityPlan.authorities.filter((authority) => authority.status !== 'adopted').flatMap((authority) => [
|
|
1386
|
+
`${authority.label} is ${authority.status}${authority.diagnostics.length ? `: ${authority.diagnostics.join('; ')}` : ''}`,
|
|
1387
|
+
...(authority.nextAction ? [authority.nextAction] : []),
|
|
1388
|
+
]);
|
|
1389
|
+
const authorityResult = {
|
|
1390
|
+
id: 'foundation.authorities',
|
|
1391
|
+
owner: 'lighthouse',
|
|
1392
|
+
level: anyRecorded ? 'required' : 'recommended',
|
|
1393
|
+
kind: 'hard',
|
|
1394
|
+
verification: 'deterministic',
|
|
1395
|
+
status: authorityStatus,
|
|
1396
|
+
evidence: authorityPlan?.authorities.map((authority) => ({ id: authority.id, path: authority.path, version: authority.version, recordedVersion: authority.recordedVersion, command: authority.command })) ?? [],
|
|
1397
|
+
problems: authorityProblems,
|
|
1398
|
+
nextAction: authorityProblems.length ? 'lighthouse authorities --adopt-existing' : null,
|
|
1399
|
+
};
|
|
1400
|
+
report.results.push(authorityResult);
|
|
1401
|
+
report.summary[authorityStatus] = (report.summary[authorityStatus] ?? 0) + 1;
|
|
1402
|
+
if (authorityResult.level === 'required' && !['passed', 'not-applicable'].includes(authorityStatus)) {
|
|
1403
|
+
report.blocking.push(authorityResult.id);
|
|
1404
|
+
report.problems.push(...authorityProblems);
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
997
1407
|
const adoption = Object.values(capabilityAdoption(await readUpgradeState()));
|
|
998
1408
|
for (const item of adoption) {
|
|
999
1409
|
if (item.status !== 'adopted' || !item.recordPath || await exists(resolve(root, item.recordPath))) continue;
|
|
@@ -1018,6 +1428,9 @@ async function verify(manifest, format = 'text') {
|
|
|
1018
1428
|
});
|
|
1019
1429
|
report.summary.unconfigured = (report.summary.unconfigured ?? 0) + (pending.length ? 1 : 0);
|
|
1020
1430
|
}
|
|
1431
|
+
const evidenceResult = await verifyEvidenceIndex();
|
|
1432
|
+
report.results.push(evidenceResult);
|
|
1433
|
+
report.summary[evidenceResult.status] = (report.summary[evidenceResult.status] ?? 0) + 1;
|
|
1021
1434
|
if (format === 'json') {
|
|
1022
1435
|
console.log(JSON.stringify(report, null, 2));
|
|
1023
1436
|
if (report.problems.length) process.exitCode = 1;
|
|
@@ -1027,6 +1440,72 @@ async function verify(manifest, format = 'text') {
|
|
|
1027
1440
|
console.log('Lighthouse manifest, repository boundaries and managed projection are valid.');
|
|
1028
1441
|
}
|
|
1029
1442
|
|
|
1443
|
+
async function verifyEvidenceIndex() {
|
|
1444
|
+
const evidencePath = '.lighthouse/evidence/index.json';
|
|
1445
|
+
const result = {
|
|
1446
|
+
id: 'foundation.evidence',
|
|
1447
|
+
owner: '@juangcarmona/lighthouse-sdlc',
|
|
1448
|
+
level: 'recommended',
|
|
1449
|
+
kind: 'hard',
|
|
1450
|
+
verification: 'deterministic',
|
|
1451
|
+
status: 'unconfigured',
|
|
1452
|
+
evidence: [],
|
|
1453
|
+
problems: [],
|
|
1454
|
+
nextAction: `lighthouse-sdlc evidence --build --input <records.json> --output ${evidencePath}`,
|
|
1455
|
+
};
|
|
1456
|
+
if (!(await exists(resolve(root, evidencePath)))) return result;
|
|
1457
|
+
if (!sdlcEvidence) {
|
|
1458
|
+
result.status = 'unavailable';
|
|
1459
|
+
result.problems.push('The installed SDLC package does not expose the evidence contract.');
|
|
1460
|
+
result.nextAction = 'Install a Lighthouse SDLC package that exports the evidence contract.';
|
|
1461
|
+
return result;
|
|
1462
|
+
}
|
|
1463
|
+
let index;
|
|
1464
|
+
try { index = JSON.parse(await readFile(resolve(root, evidencePath), 'utf8')); }
|
|
1465
|
+
catch (error) {
|
|
1466
|
+
result.status = 'failed';
|
|
1467
|
+
result.problems.push(`Cannot read ${evidencePath}: ${error.message}`);
|
|
1468
|
+
result.nextAction = `Repair ${evidencePath} and run lighthouse verify again.`;
|
|
1469
|
+
return result;
|
|
1470
|
+
}
|
|
1471
|
+
const errors = sdlcEvidence.validateEvidenceIndex(index);
|
|
1472
|
+
for (const error of errors) result.problems.push(`${error.field ?? 'index'}: ${error.message}`);
|
|
1473
|
+
const records = [];
|
|
1474
|
+
for (const reference of Array.isArray(index.records) ? index.records : []) {
|
|
1475
|
+
if (!reference || typeof reference !== 'object' || typeof reference.path !== 'string') continue;
|
|
1476
|
+
const path = resolve(root, '.lighthouse/evidence', reference.path);
|
|
1477
|
+
if (!(await exists(path))) {
|
|
1478
|
+
result.problems.push(`Missing evidence record ${reference.path}.`);
|
|
1479
|
+
continue;
|
|
1480
|
+
}
|
|
1481
|
+
try {
|
|
1482
|
+
const content = await readFile(path, 'utf8');
|
|
1483
|
+
const record = JSON.parse(content);
|
|
1484
|
+
for (const error of sdlcEvidence.validateEvidenceRecord(record)) result.problems.push(`${reference.path}: ${error.message}`);
|
|
1485
|
+
const observedHash = createHash('sha256').update(JSON.stringify(record)).digest('hex');
|
|
1486
|
+
if (observedHash !== reference.sha256) result.problems.push(`${reference.path}: SHA-256 does not match the index.`);
|
|
1487
|
+
result.evidence.push({ id: reference.id, path: reference.path, status: record.status, sha256: observedHash });
|
|
1488
|
+
records.push(record);
|
|
1489
|
+
} catch (error) {
|
|
1490
|
+
result.problems.push(`${reference.path}: ${error.message}`);
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
if (!errors.length && records.length === (index.records ?? []).length) {
|
|
1494
|
+
const rebuilt = sdlcEvidence.buildEvidenceIndex(records, {
|
|
1495
|
+
indexId: index.indexId,
|
|
1496
|
+
runId: index.runId,
|
|
1497
|
+
repository: index.repository,
|
|
1498
|
+
revision: index.revision,
|
|
1499
|
+
subjects: (index.coverage ?? []).map((item) => item.subject),
|
|
1500
|
+
recordPaths: Object.fromEntries((index.records ?? []).map((item) => [item.id, item.path])),
|
|
1501
|
+
});
|
|
1502
|
+
if (rebuilt.errors.length || JSON.stringify(rebuilt.index?.coverage) !== JSON.stringify(index.coverage)) result.problems.push('Evidence coverage does not match the referenced records; rebuild the index.');
|
|
1503
|
+
}
|
|
1504
|
+
result.status = result.problems.length ? 'failed' : 'passed';
|
|
1505
|
+
result.nextAction = result.problems.length ? `Repair ${evidencePath} or its records, then run lighthouse verify again.` : null;
|
|
1506
|
+
return result;
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1030
1509
|
async function profiles(manifest, format = 'text') {
|
|
1031
1510
|
const composition = composeProfiles(readProfileCatalog(), manifest);
|
|
1032
1511
|
if (format === 'json') {
|
|
@@ -1212,7 +1691,8 @@ for (let index = 0; index < commandArgs.length; index += 1) {
|
|
|
1212
1691
|
const arg = commandArgs[index];
|
|
1213
1692
|
if (command === 'capabilities' && index === 0 && ['select', 'decline', 'plan', 'apply'].includes(arg)) continue;
|
|
1214
1693
|
if (arg === '--dry-run') continue;
|
|
1215
|
-
if (arg === '--apply') { if (!['authorities', 'reconcile', 'upgrade'].includes(command)) invalidArgument = true; continue; }
|
|
1694
|
+
if (arg === '--apply') { if (!['init', 'authorities', 'reconcile', 'upgrade'].includes(command)) invalidArgument = true; continue; }
|
|
1695
|
+
if (arg === '--adopt-existing') { if (command !== 'authorities') invalidArgument = true; continue; }
|
|
1216
1696
|
if (arg === '--name') { if (!commandArgs[index + 1]) invalidArgument = true; else index += 1; continue; }
|
|
1217
1697
|
if (arg.startsWith('--name=') && arg.length > '--name='.length) continue;
|
|
1218
1698
|
if (arg === '--profile') {
|
|
@@ -1225,12 +1705,12 @@ for (let index = 0; index < commandArgs.length; index += 1) {
|
|
|
1225
1705
|
continue;
|
|
1226
1706
|
}
|
|
1227
1707
|
if (arg === '--format') {
|
|
1228
|
-
if (!['profiles', 'verify', 'upgrade', 'capabilities'].includes(command) || !['text', 'json'].includes(commandArgs[index + 1])) invalidArgument = true;
|
|
1708
|
+
if (!['init', 'aliases', 'profiles', 'authorities', 'verify', 'upgrade', 'capabilities'].includes(command) || !['text', 'json'].includes(commandArgs[index + 1])) invalidArgument = true;
|
|
1229
1709
|
else index += 1;
|
|
1230
1710
|
continue;
|
|
1231
1711
|
}
|
|
1232
1712
|
if (arg.startsWith('--format=') && ['text', 'json'].includes(arg.slice('--format='.length))) {
|
|
1233
|
-
if (!['profiles', 'verify', 'upgrade', 'capabilities'].includes(command)) invalidArgument = true;
|
|
1713
|
+
if (!['init', 'aliases', 'profiles', 'authorities', 'verify', 'upgrade', 'capabilities'].includes(command)) invalidArgument = true;
|
|
1234
1714
|
continue;
|
|
1235
1715
|
}
|
|
1236
1716
|
if (arg === '--scope') {
|
|
@@ -1243,19 +1723,24 @@ for (let index = 0; index < commandArgs.length; index += 1) {
|
|
|
1243
1723
|
continue;
|
|
1244
1724
|
}
|
|
1245
1725
|
if (['--id', '--provider', '--repo', '--input', '--plan-file', '--authorize'].includes(arg)) {
|
|
1246
|
-
if (!commandArgs[index + 1]) invalidArgument = true;
|
|
1726
|
+
if (!commandArgs[index + 1] || (arg === '--plan-file' && !['init', 'capabilities'].includes(command))) invalidArgument = true;
|
|
1247
1727
|
else index += 1;
|
|
1248
1728
|
continue;
|
|
1249
1729
|
}
|
|
1250
|
-
if (['--id=', '--provider=', '--repo=', '--input=', '--plan-file=', '--authorize='].some((prefix) => arg.startsWith(prefix) && arg.length > prefix.length))
|
|
1730
|
+
if (['--id=', '--provider=', '--repo=', '--input=', '--plan-file=', '--authorize='].some((prefix) => arg.startsWith(prefix) && arg.length > prefix.length)) {
|
|
1731
|
+
if (arg.startsWith('--plan-file=') && !['init', 'capabilities'].includes(command)) invalidArgument = true;
|
|
1732
|
+
continue;
|
|
1733
|
+
}
|
|
1251
1734
|
invalidArgument = true;
|
|
1252
1735
|
}
|
|
1253
|
-
if (!['init', 'profiles', 'authorities', 'reconcile', 'upgrade', 'capabilities', 'verify'].includes(command) || invalidArgument) {
|
|
1736
|
+
if (!['init', 'aliases', 'profiles', 'authorities', 'reconcile', 'upgrade', 'capabilities', 'verify'].includes(command) || invalidArgument) {
|
|
1254
1737
|
console.error(usage);
|
|
1255
1738
|
process.exit(2);
|
|
1256
1739
|
}
|
|
1257
1740
|
try {
|
|
1258
1741
|
if (command === 'init') await initialize(args.slice(1));
|
|
1742
|
+
else if (command === 'aliases') await commandAliases(formatFromArgs(commandArgs));
|
|
1743
|
+
else if (command === 'authorities' && commandArgs.includes('--adopt-existing')) await adoptExistingAuthorities(args.slice(1));
|
|
1259
1744
|
else if (command === 'authorities') await authorities(args.slice(1));
|
|
1260
1745
|
else if (command === 'reconcile') await reconcile(args.slice(1));
|
|
1261
1746
|
else if (command === 'upgrade') await upgrade(args.slice(1));
|