@juangcarmona/lighthouse-sdlc 0.5.0 → 0.7.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 +6 -0
- package/command-aliases.json +20 -0
- package/commands/aliases.md +5 -0
- package/commands/browser-e2e.md +10 -0
- package/contract.json +5 -2
- package/package.json +8 -2
- package/schemas/browser-e2e.schema.json +51 -0
- package/schemas/command-aliases.schema.json +35 -0
- package/skills/browser-e2e/SKILL.md +14 -0
- package/src/aliases.mjs +57 -0
- package/src/browser-e2e.mjs +48 -0
- package/src/sdlc.mjs +39 -5
package/README.md
CHANGED
|
@@ -4,6 +4,10 @@ This package provides the provider-neutral delivery contract for Lighthouse adop
|
|
|
4
4
|
|
|
5
5
|
It owns the six lifecycle skills, their command entrypoints, route, stage and gate descriptors, deterministic triage and decomposition projections, rebuildable state checks and the provider-neutral control-plane contract.
|
|
6
6
|
|
|
7
|
+
It publishes `command-aliases.json` as the versioned, machine-readable mapping from selected `/lh:*` aliases to canonical `/lighthouse:*` command assets. Run `lighthouse-sdlc aliases --format json` to inspect it; the configured agent host decides how to expose the aliases.
|
|
8
|
+
|
|
9
|
+
It also provides the `browser-e2e` evidence contract for Playwright journeys. The contract belongs to the SDLC because it records delivery evidence; the tests, journeys and browser configuration remain owned by the adopter.
|
|
10
|
+
|
|
7
11
|
It does not run agents, store transcripts, dispatch work between repositories or replace ProductShape, ArcShape, OpenSpec or an agent host.
|
|
8
12
|
|
|
9
13
|
Install it with the Lighthouse CLI or run `lighthouse-sdlc verify` from the package installation to check the distributed contract.
|
|
@@ -12,6 +16,8 @@ Run `lighthouse-sdlc triage --input <file>` to validate one structured triage re
|
|
|
12
16
|
|
|
13
17
|
Use `lighthouse-sdlc route --route <name>` to inspect a route before running it, `stage-result --input <file>` to validate a stage result, `state --input <events.json>` to replay workflow events, `control-plane --input <file>` to validate host declarations, `decompose --input <file>` to produce a dependency-checked slice plan and `stacked --input <file>` to produce an ordered pull-request plan.
|
|
14
18
|
|
|
19
|
+
Use `lighthouse-sdlc browser-e2e --input <evidence.json>` to validate a Playwright journey result. It is read-only and does not start a browser or write provider state.
|
|
20
|
+
|
|
15
21
|
Use `lighthouse-sdlc github-triage --repo <owner/repo> --issue <number> --assessment <file>` for the first GitHub projection. It reads issue metadata by default; `--apply` is the explicit label mutation boundary and rereads the issue before writing.
|
|
16
22
|
|
|
17
23
|
Use `lighthouse-sdlc github-decompose --repo <owner/repo> --issue <number> --input <file>` to turn a validated slice plan into child-issue operations. The default is a preview; `--apply` creates the child issues in dependency order with the parent link and acceptance evidence in each body.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "./schemas/command-aliases.schema.json",
|
|
3
|
+
"schemaVersion": 1,
|
|
4
|
+
"contractVersion": 1,
|
|
5
|
+
"package": { "name": "@juangcarmona/lighthouse-sdlc" },
|
|
6
|
+
"canonicalNamespace": "/lighthouse",
|
|
7
|
+
"aliasNamespace": "/lh",
|
|
8
|
+
"aliases": [
|
|
9
|
+
{ "alias": "/lh:control", "canonical": "/lighthouse:control-plane", "asset": "commands/control-plane.md" },
|
|
10
|
+
{ "alias": "/lh:gh-decompose", "canonical": "/lighthouse:github-decompose", "asset": "commands/github-decompose.md" },
|
|
11
|
+
{ "alias": "/lh:gh-triage", "canonical": "/lighthouse:github-triage", "asset": "commands/github-triage.md" },
|
|
12
|
+
{ "alias": "/lh:result", "canonical": "/lighthouse:stage-result", "asset": "commands/stage-result.md" },
|
|
13
|
+
{ "alias": "/lh:e2e", "canonical": "/lighthouse:browser-e2e", "asset": "commands/browser-e2e.md" },
|
|
14
|
+
{ "alias": "/lh:caps", "canonical": "/lighthouse:capabilities", "asset": "commands/capabilities.md" },
|
|
15
|
+
{ "alias": "/lh:labels", "canonical": "/lighthouse:reconcile-labels", "asset": "commands/reconcile-labels.md" },
|
|
16
|
+
{ "alias": "/lh:gh-labels", "canonical": "/lighthouse:github-labels", "asset": "commands/github-labels.md" },
|
|
17
|
+
{ "alias": "/lh:mem", "canonical": "/lighthouse:memory", "asset": "commands/memory.md" },
|
|
18
|
+
{ "alias": "/lh:impl", "canonical": "/lighthouse:implement", "asset": "commands/implement.md" }
|
|
19
|
+
]
|
|
20
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# `/lighthouse:aliases`
|
|
2
|
+
|
|
3
|
+
Use `lighthouse-sdlc aliases --format json` to inspect the SDLC package's versioned agent-command aliases. The output maps each `/lh:*` alias to one canonical `/lighthouse:*` command and its packaged command asset.
|
|
4
|
+
|
|
5
|
+
The alias listing is read-only. The configured agent host decides how to expose the aliases and must reference the canonical command asset instead of copying or maintaining a second command implementation.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Browser E2E
|
|
2
|
+
|
|
3
|
+
Use this command during `integrate` and `review` when the selected application exposes a browser journey.
|
|
4
|
+
|
|
5
|
+
1. Read the ProductShape journey and its bounded context before choosing the test.
|
|
6
|
+
2. Run the adopter's Playwright command and keep the report, screenshots, traces or videos under the repository's evidence path.
|
|
7
|
+
3. Create a browser evidence JSON document with `runner: playwright`, the journey identity, evidence statuses, relative artifact paths and the next action.
|
|
8
|
+
4. Run `lighthouse-sdlc browser-e2e --input <evidence.json>` and stop when the report is invalid or the journey is not passed.
|
|
9
|
+
|
|
10
|
+
The command validates evidence; it does not start Playwright, invent product journeys or make browser tests mandatory for applications without a browser surface.
|
package/contract.json
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
{ "name": "integrate", "skill": "skills/integrate/SKILL.md", "command": "commands/integrate.md", "gate": "done" },
|
|
11
11
|
{ "name": "review", "skill": "skills/review/SKILL.md", "command": "commands/review.md", "gate": null }
|
|
12
12
|
],
|
|
13
|
-
"utilities": ["status", "route", "state", "stage-result", "control-plane", "memory", "launch", "decompose", "stacked", "github-triage", "github-decompose", "reconcile-labels", "github-labels"],
|
|
13
|
+
"utilities": ["status", "aliases", "route", "state", "stage-result", "control-plane", "memory", "launch", "decompose", "stacked", "browser-e2e", "github-triage", "github-decompose", "reconcile-labels", "github-labels"],
|
|
14
14
|
"routes": ["supervised", "plan-only", "unattended", "hotfix", "stacked"],
|
|
15
15
|
"metaCommands": ["ff", "hotfix", "decompose", "stacked"],
|
|
16
16
|
"gates": ["triaged", "ready", "planned", "done"],
|
|
@@ -24,7 +24,10 @@
|
|
|
24
24
|
"stateSchema": "schemas/workflow-state.schema.json",
|
|
25
25
|
"decompositionSchema": "schemas/decomposition.schema.json",
|
|
26
26
|
"capabilitiesSchema": "schemas/capabilities.schema.json",
|
|
27
|
+
"browserE2eSchema": "schemas/browser-e2e.schema.json",
|
|
27
28
|
"capabilityCatalog": "capabilities.json",
|
|
28
|
-
"capabilityCatalogSchema": "schemas/capability-catalog.schema.json"
|
|
29
|
+
"capabilityCatalogSchema": "schemas/capability-catalog.schema.json",
|
|
30
|
+
"commandAliases": "command-aliases.json",
|
|
31
|
+
"commandAliasesSchema": "schemas/command-aliases.schema.json"
|
|
29
32
|
}
|
|
30
33
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@juangcarmona/lighthouse-sdlc",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Provider-neutral SDLC skills, commands and route contracts for Lighthouse adopters",
|
|
6
6
|
"repository": {
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
},
|
|
22
22
|
"files": [
|
|
23
23
|
"commands",
|
|
24
|
+
"command-aliases.json",
|
|
24
25
|
"contract.json",
|
|
25
26
|
"capabilities.json",
|
|
26
27
|
"gates.json",
|
|
@@ -46,8 +47,10 @@
|
|
|
46
47
|
"./routes": "./src/routes.mjs",
|
|
47
48
|
"./reconcile": "./src/reconcile.mjs",
|
|
48
49
|
"./capabilities": "./src/capabilities.mjs",
|
|
50
|
+
"./aliases": "./src/aliases.mjs",
|
|
49
51
|
"./stacked": "./src/stacked.mjs",
|
|
50
52
|
"./workflow": "./src/workflow.mjs",
|
|
53
|
+
"./browser-e2e": "./src/browser-e2e.mjs",
|
|
51
54
|
"./contract.json": "./contract.json",
|
|
52
55
|
"./schemas/control-plane.schema.json": "./schemas/control-plane.schema.json",
|
|
53
56
|
"./schemas/triage.schema.json": "./schemas/triage.schema.json",
|
|
@@ -57,6 +60,9 @@
|
|
|
57
60
|
"./schemas/decomposition.schema.json": "./schemas/decomposition.schema.json",
|
|
58
61
|
"./schemas/capabilities.schema.json": "./schemas/capabilities.schema.json",
|
|
59
62
|
"./schemas/capability-catalog.schema.json": "./schemas/capability-catalog.schema.json",
|
|
60
|
-
"./
|
|
63
|
+
"./schemas/browser-e2e.schema.json": "./schemas/browser-e2e.schema.json",
|
|
64
|
+
"./capabilities.json": "./capabilities.json",
|
|
65
|
+
"./command-aliases.json": "./command-aliases.json",
|
|
66
|
+
"./schemas/command-aliases.schema.json": "./schemas/command-aliases.schema.json"
|
|
61
67
|
}
|
|
62
68
|
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://github.com/juangcarmona/lighthouse/schemas/browser-e2e.schema.json",
|
|
4
|
+
"title": "Lighthouse browser end-to-end evidence",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": false,
|
|
7
|
+
"required": ["schemaVersion", "runner", "journey", "command", "status", "evidence", "artifacts", "nextAction"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"schemaVersion": { "const": 1 },
|
|
10
|
+
"runner": { "const": "playwright" },
|
|
11
|
+
"journey": {
|
|
12
|
+
"type": "object",
|
|
13
|
+
"additionalProperties": false,
|
|
14
|
+
"required": ["id", "boundedContext"],
|
|
15
|
+
"properties": {
|
|
16
|
+
"id": { "type": "string", "minLength": 1 },
|
|
17
|
+
"boundedContext": { "type": "string", "minLength": 1 },
|
|
18
|
+
"title": { "type": "string", "minLength": 1 }
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"command": { "type": "string", "minLength": 1 },
|
|
22
|
+
"status": { "enum": ["passed", "failed", "unavailable", "pending-review"] },
|
|
23
|
+
"evidence": {
|
|
24
|
+
"type": "array",
|
|
25
|
+
"items": {
|
|
26
|
+
"type": "object",
|
|
27
|
+
"additionalProperties": false,
|
|
28
|
+
"required": ["id", "status", "source"],
|
|
29
|
+
"properties": {
|
|
30
|
+
"id": { "type": "string", "minLength": 1 },
|
|
31
|
+
"status": { "enum": ["passed", "failed", "unavailable", "pending-review"] },
|
|
32
|
+
"source": { "type": "string", "minLength": 1 },
|
|
33
|
+
"reason": { "type": "string", "minLength": 1 }
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"artifacts": {
|
|
38
|
+
"type": "array",
|
|
39
|
+
"items": {
|
|
40
|
+
"type": "object",
|
|
41
|
+
"additionalProperties": false,
|
|
42
|
+
"required": ["type", "path"],
|
|
43
|
+
"properties": {
|
|
44
|
+
"type": { "enum": ["screenshot", "trace", "video", "report"] },
|
|
45
|
+
"path": { "type": "string", "minLength": 1 }
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
"nextAction": { "type": "string", "minLength": 1 }
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://github.com/juangcarmona/lighthouse/schemas/lighthouse-sdlc-command-aliases.schema.json",
|
|
4
|
+
"title": "Lighthouse SDLC 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-sdlc"}}
|
|
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": "^(commands|meta-commands)/[a-z0-9-]+\\.md$"}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: lighthouse-browser-e2e
|
|
3
|
+
description: Validate Playwright browser evidence for a selected product journey during integration or review.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Browser E2E
|
|
7
|
+
|
|
8
|
+
Use this skill only when the selected application has a browser surface and the product definition names a journey to verify.
|
|
9
|
+
|
|
10
|
+
Read the journey, bounded context and acceptance evidence before writing or changing a test. Keep the test in the adopter repository and use the project's existing Playwright configuration when one exists.
|
|
11
|
+
|
|
12
|
+
Run the narrowest useful journey first. Collect the report and any screenshot, trace or video needed to inspect a failure. Store relative paths in the browser evidence document and validate it with `lighthouse-sdlc browser-e2e --input <evidence.json>`.
|
|
13
|
+
|
|
14
|
+
Finish with one of `passed`, `failed`, `unavailable` or `pending-review`, plus a concrete `nextAction`. A missing artifact, unavailable browser or unresolved human judgement stays visible in the result.
|
package/src/aliases.mjs
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { access, readFile } from 'node:fs/promises';
|
|
2
|
+
import { constants } 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, 'command-aliases.json');
|
|
8
|
+
const packageName = '@juangcarmona/lighthouse-sdlc';
|
|
9
|
+
const aliasPattern = /^\/lh:[a-z][a-z0-9-]*$/u;
|
|
10
|
+
const canonicalPattern = /^\/lighthouse:[a-z][a-z0-9-]*$/u;
|
|
11
|
+
const assetPattern = /^(?:commands|meta-commands)\/[a-z0-9-]+\.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 async function readCommandAliases(path = contractPath) {
|
|
18
|
+
return JSON.parse(await readFile(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 command 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) {
|
|
49
|
+
const value = contract ?? await readCommandAliases();
|
|
50
|
+
const errors = validateCommandAliases(value);
|
|
51
|
+
for (const item of value.aliases ?? []) {
|
|
52
|
+
if (typeof item.asset === 'string' && !(await exists(resolve(packageRoot, item.asset)))) errors.push({ code: 'aliases.missing-asset', message: `alias ${item.alias} references missing asset ${item.asset}` });
|
|
53
|
+
}
|
|
54
|
+
return errors;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export { contractPath };
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
const statuses = new Set(['passed', 'failed', 'unavailable', 'pending-review']);
|
|
2
|
+
const artifactTypes = new Set(['screenshot', 'trace', 'video', 'report']);
|
|
3
|
+
|
|
4
|
+
function issue(code, message, field) {
|
|
5
|
+
return field ? { code, message, field } : { code, message };
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function record(value) {
|
|
9
|
+
return value && typeof value === 'object' && !Array.isArray(value);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function validateBrowserEvidence(value) {
|
|
13
|
+
const errors = [];
|
|
14
|
+
if (!record(value)) return [issue('browser-e2e.invalid', 'browser evidence must be an object')];
|
|
15
|
+
if (value.schemaVersion !== 1) errors.push(issue('browser-e2e.schema-version', 'schemaVersion must be 1', 'schemaVersion'));
|
|
16
|
+
if (value.runner !== 'playwright') errors.push(issue('browser-e2e.runner', 'runner must be playwright', 'runner'));
|
|
17
|
+
if (!record(value.journey) || typeof value.journey.id !== 'string' || !value.journey.id.trim() || typeof value.journey.boundedContext !== 'string' || !value.journey.boundedContext.trim()) {
|
|
18
|
+
errors.push(issue('browser-e2e.journey', 'journey needs an id and boundedContext', 'journey'));
|
|
19
|
+
}
|
|
20
|
+
if (typeof value.command !== 'string' || !value.command.trim()) errors.push(issue('browser-e2e.command', 'command must be a non-empty string', 'command'));
|
|
21
|
+
if (!statuses.has(value.status)) errors.push(issue('browser-e2e.status', `status must be one of ${[...statuses].join(', ')}`, 'status'));
|
|
22
|
+
if (!Array.isArray(value.evidence) || value.evidence.length === 0) errors.push(issue('browser-e2e.evidence', 'evidence must contain at least one item', 'evidence'));
|
|
23
|
+
else {
|
|
24
|
+
const ids = new Set();
|
|
25
|
+
for (const [index, item] of value.evidence.entries()) {
|
|
26
|
+
if (!record(item) || typeof item.id !== 'string' || !item.id.trim() || !statuses.has(item.status) || typeof item.source !== 'string' || !item.source.trim()) {
|
|
27
|
+
errors.push(issue('browser-e2e.evidence-item', `evidence[${index}] needs an id, supported status and source`, `evidence[${index}]`));
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (ids.has(item.id)) errors.push(issue('browser-e2e.evidence-duplicate', `evidence id ${item.id} is duplicated`, `evidence[${index}]`));
|
|
31
|
+
ids.add(item.id);
|
|
32
|
+
if (['failed', 'unavailable', 'pending-review'].includes(item.status) && (typeof item.reason !== 'string' || !item.reason.trim())) errors.push(issue('browser-e2e.evidence-reason', `evidence[${index}] needs a reason for status ${item.status}`, `evidence[${index}]`));
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (!Array.isArray(value.artifacts)) errors.push(issue('browser-e2e.artifacts', 'artifacts must be an array', 'artifacts'));
|
|
36
|
+
else for (const [index, artifact] of value.artifacts.entries()) {
|
|
37
|
+
if (!record(artifact) || !artifactTypes.has(artifact.type) || typeof artifact.path !== 'string' || !artifact.path.trim() || artifact.path.startsWith('/')) errors.push(issue('browser-e2e.artifact', `artifacts[${index}] needs a relative path and supported type`, `artifacts[${index}]`));
|
|
38
|
+
}
|
|
39
|
+
if (value.status === 'passed' && (!value.artifacts?.length || value.evidence?.some((item) => item.status !== 'passed'))) errors.push(issue('browser-e2e.passed-evidence', 'passed browser evidence needs artifacts and only passed evidence items', 'status'));
|
|
40
|
+
if (value.status !== 'passed' && value.evidence?.every((item) => item.status === 'passed')) errors.push(issue('browser-e2e.status-evidence', `status ${value.status} needs a matching evidence item`, 'status'));
|
|
41
|
+
if (typeof value.nextAction !== 'string' || !value.nextAction.trim()) errors.push(issue('browser-e2e.next-action', 'nextAction must be a non-empty string', 'nextAction'));
|
|
42
|
+
return errors;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function browserEvidenceReport(value) {
|
|
46
|
+
const errors = validateBrowserEvidence(value);
|
|
47
|
+
return { schemaVersion: 1, status: errors.length ? 'invalid' : value.status, valid: errors.length === 0, errors, journey: value.journey ?? null, runner: value.runner ?? null, artifacts: value.artifacts ?? [] };
|
|
48
|
+
}
|
package/src/sdlc.mjs
CHANGED
|
@@ -13,7 +13,9 @@ import { applyTriageLabels, buildTriageReport } from './triage.mjs';
|
|
|
13
13
|
import { validateMemoryRecord } from './memory.mjs';
|
|
14
14
|
import { replayWorkflow } from './workflow.mjs';
|
|
15
15
|
import { validateStageResult } from './contracts.mjs';
|
|
16
|
+
import { browserEvidenceReport } from './browser-e2e.mjs';
|
|
16
17
|
import { readCapabilityCatalog, validateCapabilityCatalog } from './capabilities.mjs';
|
|
18
|
+
import { readCommandAliases, validateCommandAliases, verifyCommandAliases } from './aliases.mjs';
|
|
17
19
|
|
|
18
20
|
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
19
21
|
const contractPath = resolve(packageRoot, 'contract.json');
|
|
@@ -39,10 +41,10 @@ async function verifyContract() {
|
|
|
39
41
|
for (const stage of stages) {
|
|
40
42
|
for (const asset of [stage.skill, stage.command]) if (!(await exists(resolve(packageRoot, asset)))) problems.push(`missing stage asset ${asset}`);
|
|
41
43
|
}
|
|
42
|
-
for (const asset of [contract.assets?.routes, contract.assets?.gates, contract.assets?.stages, contract.assets?.controlPlaneSchema, contract.assets?.triageSchema, contract.assets?.stageResultSchema, contract.assets?.stateSchema, contract.assets?.decompositionSchema, contract.assets?.capabilitiesSchema, contract.assets?.capabilityCatalog, contract.assets?.capabilityCatalogSchema]) {
|
|
44
|
+
for (const asset of [contract.assets?.routes, contract.assets?.gates, contract.assets?.stages, contract.assets?.controlPlaneSchema, contract.assets?.triageSchema, contract.assets?.stageResultSchema, contract.assets?.stateSchema, contract.assets?.decompositionSchema, contract.assets?.capabilitiesSchema, contract.assets?.capabilityCatalog, contract.assets?.capabilityCatalogSchema, contract.assets?.browserE2eSchema, contract.assets?.commandAliases, contract.assets?.commandAliasesSchema]) {
|
|
43
45
|
if (!asset || !(await exists(resolve(packageRoot, asset)))) problems.push(`missing contract asset ${asset ?? '(empty)'}`);
|
|
44
46
|
}
|
|
45
|
-
for (const asset of ['routes.json', 'gates.json', 'stages.json', 'schemas/routes.schema.json', 'schemas/gates.schema.json', 'schemas/stages.schema.json', 'schemas/control-plane.schema.json', 'schemas/triage.schema.json', 'schemas/stage-result.schema.json', 'schemas/workflow-state.schema.json', 'schemas/decomposition.schema.json', 'schemas/capabilities.schema.json', 'schemas/capability-catalog.schema.json', 'src/capabilities.mjs', 'src/contracts.mjs', 'src/control-plane.mjs', 'src/decompose.mjs', 'src/github.mjs', 'src/memory.mjs', 'src/reconcile.mjs', 'src/routes.mjs', 'src/stacked.mjs', 'src/triage.mjs', 'src/workflow.mjs']) {
|
|
47
|
+
for (const asset of ['routes.json', 'gates.json', 'stages.json', 'command-aliases.json', 'schemas/routes.schema.json', 'schemas/gates.schema.json', 'schemas/stages.schema.json', 'schemas/control-plane.schema.json', 'schemas/triage.schema.json', 'schemas/stage-result.schema.json', 'schemas/workflow-state.schema.json', 'schemas/decomposition.schema.json', 'schemas/capabilities.schema.json', 'schemas/capability-catalog.schema.json', 'schemas/browser-e2e.schema.json', 'schemas/command-aliases.schema.json', 'src/aliases.mjs', 'src/browser-e2e.mjs', 'src/capabilities.mjs', 'src/contracts.mjs', 'src/control-plane.mjs', 'src/decompose.mjs', 'src/github.mjs', 'src/memory.mjs', 'src/reconcile.mjs', 'src/routes.mjs', 'src/stacked.mjs', 'src/triage.mjs', 'src/workflow.mjs']) {
|
|
46
48
|
if (!(await exists(resolve(packageRoot, asset)))) problems.push(`missing package asset ${asset}`);
|
|
47
49
|
}
|
|
48
50
|
try {
|
|
@@ -64,11 +66,30 @@ async function verifyContract() {
|
|
|
64
66
|
const catalog = await readCapabilityCatalog();
|
|
65
67
|
problems.push(...validateCapabilityCatalog(catalog).map((error) => `${error.code}: ${error.message}`));
|
|
66
68
|
} catch (error) { problems.push(`Cannot read capabilities.json: ${error.message}`); }
|
|
69
|
+
try {
|
|
70
|
+
const aliases = await readCommandAliases();
|
|
71
|
+
problems.push(...(await verifyCommandAliases(aliases)).map((error) => `${error.code}: ${error.message}`));
|
|
72
|
+
} catch (error) { problems.push(`Cannot read command-aliases.json: ${error.message}`); }
|
|
67
73
|
for (const command of ['status', ...expectedStages, ...(contract.utilities ?? []).filter((command) => command !== 'status')]) if (!(await exists(resolve(packageRoot, `commands/${command}.md`)))) problems.push(`missing command asset ${command}`);
|
|
68
74
|
for (const metaCommand of contract.metaCommands ?? []) if (!(await exists(resolve(packageRoot, `meta-commands/${metaCommand}.md`)))) problems.push(`missing meta-command asset ${metaCommand}`);
|
|
69
75
|
return problems;
|
|
70
76
|
}
|
|
71
77
|
|
|
78
|
+
async function runAliases(args) {
|
|
79
|
+
const format = option(args, '--format') ?? 'json';
|
|
80
|
+
if (!['json', 'text'].includes(format)) { console.error('Usage: lighthouse-sdlc aliases [--format <json|text>]'); process.exit(2); }
|
|
81
|
+
const contract = await readCommandAliases();
|
|
82
|
+
const errors = await verifyCommandAliases(contract);
|
|
83
|
+
const result = { schemaVersion: 1, status: errors.length ? 'invalid' : 'valid', valid: errors.length === 0, package: contract.package, contractVersion: contract.contractVersion, canonicalNamespace: contract.canonicalNamespace, aliasNamespace: contract.aliasNamespace, aliases: contract.aliases, errors };
|
|
84
|
+
if (format === 'json') console.log(JSON.stringify(result, null, 2));
|
|
85
|
+
else {
|
|
86
|
+
console.log(`SDLC command aliases: ${result.status}`);
|
|
87
|
+
for (const alias of result.aliases) console.log(` ${alias.alias} -> ${alias.canonical}`);
|
|
88
|
+
for (const error of result.errors) console.error(` ${error.code}: ${error.message}`);
|
|
89
|
+
}
|
|
90
|
+
if (errors.length) process.exit(1);
|
|
91
|
+
}
|
|
92
|
+
|
|
72
93
|
function option(args, name) {
|
|
73
94
|
const index = args.indexOf(name);
|
|
74
95
|
if (index !== -1) return args[index + 1];
|
|
@@ -160,6 +181,15 @@ async function runStageResult(args) {
|
|
|
160
181
|
if (errors.length) process.exit(1);
|
|
161
182
|
}
|
|
162
183
|
|
|
184
|
+
async function runBrowserE2e(args) {
|
|
185
|
+
const inputPath = option(args, '--input');
|
|
186
|
+
const format = option(args, '--format') ?? 'json';
|
|
187
|
+
if (!inputPath) { console.error('Usage: lighthouse-sdlc browser-e2e --input <evidence.json> [--format <json|text>]'); process.exit(2); }
|
|
188
|
+
const report = browserEvidenceReport(await readJson(resolve(process.cwd(), inputPath)));
|
|
189
|
+
output(report, format);
|
|
190
|
+
if (!report.valid || report.status !== 'passed') process.exit(1);
|
|
191
|
+
}
|
|
192
|
+
|
|
163
193
|
async function runMemory(args) {
|
|
164
194
|
const inputPath = option(args, '--input');
|
|
165
195
|
const policyPath = option(args, '--policy');
|
|
@@ -318,20 +348,24 @@ async function runGithubLabels(args) {
|
|
|
318
348
|
}
|
|
319
349
|
|
|
320
350
|
const command = process.argv[2] ?? 'verify';
|
|
321
|
-
if (!['contract', 'verify', 'triage', 'state', 'stage-result', 'control-plane', 'memory', 'launch', 'decompose', 'stacked', 'route', 'github-triage', 'github-decompose', 'reconcile-labels', 'github-labels'].includes(command)) {
|
|
322
|
-
console.error('Usage: lighthouse-sdlc <contract|verify|triage|state|stage-result|control-plane|memory|launch|decompose|stacked|route|github-triage|github-decompose|reconcile-labels|github-labels>');
|
|
351
|
+
if (!['contract', 'verify', 'aliases', 'triage', 'state', 'stage-result', 'browser-e2e', 'control-plane', 'memory', 'launch', 'decompose', 'stacked', 'route', 'github-triage', 'github-decompose', 'reconcile-labels', 'github-labels'].includes(command)) {
|
|
352
|
+
console.error('Usage: lighthouse-sdlc <contract|verify|aliases|triage|state|stage-result|browser-e2e|control-plane|memory|launch|decompose|stacked|route|github-triage|github-decompose|reconcile-labels|github-labels>');
|
|
323
353
|
process.exit(2);
|
|
324
354
|
}
|
|
325
355
|
if (command === 'contract') {
|
|
326
356
|
console.log(await readFile(contractPath, 'utf8'));
|
|
327
357
|
} else if (command === 'triage') {
|
|
328
358
|
await runTriage(process.argv.slice(3));
|
|
359
|
+
} else if (command === 'aliases') {
|
|
360
|
+
await runAliases(process.argv.slice(3));
|
|
329
361
|
} else if (command === 'state') {
|
|
330
362
|
await runState(process.argv.slice(3));
|
|
331
363
|
} else if (command === 'control-plane') {
|
|
332
364
|
await runControlPlane(process.argv.slice(3));
|
|
333
365
|
} else if (command === 'stage-result') {
|
|
334
366
|
await runStageResult(process.argv.slice(3));
|
|
367
|
+
} else if (command === 'browser-e2e') {
|
|
368
|
+
await runBrowserE2e(process.argv.slice(3));
|
|
335
369
|
} else if (command === 'memory') {
|
|
336
370
|
await runMemory(process.argv.slice(3));
|
|
337
371
|
} else if (command === 'launch') {
|
|
@@ -359,4 +393,4 @@ if (command === 'contract') {
|
|
|
359
393
|
console.log('Lighthouse SDLC package contract is valid.');
|
|
360
394
|
}
|
|
361
395
|
|
|
362
|
-
export { contractPath, runTriage, verifyContract, runState, runStageResult, runControlPlane, runMemory, runLaunch, runDecompose, runRoute, runStacked, runGithubTriage, runGithubDecompose, runReconcileLabels, runGithubLabels };
|
|
396
|
+
export { contractPath, runTriage, verifyContract, runAliases, runState, runStageResult, runBrowserE2e, runControlPlane, runMemory, runLaunch, runDecompose, runRoute, runStacked, runGithubTriage, runGithubDecompose, runReconcileLabels, runGithubLabels };
|