@juangcarmona/lighthouse-sdlc 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -0
- package/command-aliases.json +20 -0
- package/commands/aliases.md +5 -0
- package/commands/evidence.md +9 -0
- package/contract.json +6 -2
- package/package.json +11 -2
- package/schemas/command-aliases.schema.json +35 -0
- package/schemas/evidence-index.schema.json +65 -0
- package/schemas/evidence-record.schema.json +67 -0
- package/schemas/sdlc-contract.schema.json +4 -2
- package/src/aliases.mjs +57 -0
- package/src/evidence.mjs +204 -0
- package/src/sdlc.mjs +68 -5
- package/workflows/lighthouse-sdlc-implement.lock.yml +2212 -0
- package/workflows/lighthouse-sdlc-implement.md +41 -0
- package/workflows/lighthouse-sdlc-integrate.lock.yml +2212 -0
- package/workflows/lighthouse-sdlc-integrate.md +41 -0
- package/workflows/lighthouse-sdlc-propose.lock.yml +2212 -0
- package/workflows/lighthouse-sdlc-propose.md +41 -0
- package/workflows/lighthouse-sdlc-refine.lock.yml +2212 -0
- package/workflows/lighthouse-sdlc-refine.md +41 -0
- package/workflows/lighthouse-sdlc-review.lock.yml +2212 -0
- package/workflows/lighthouse-sdlc-review.md +41 -0
- package/workflows/lighthouse-sdlc-router.yml +176 -0
- package/workflows/lighthouse-sdlc-triage.lock.yml +2212 -0
- package/workflows/lighthouse-sdlc-triage.md +41 -0
- package/workflows/shared/lighthouse-sdlc-stage.md +81 -0
package/README.md
CHANGED
|
@@ -4,10 +4,22 @@ 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
|
+
|
|
7
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.
|
|
8
10
|
|
|
11
|
+
It provides a neutral evidence contract for delivery, product and architecture references. `lighthouse-sdlc evidence` validates records or builds a deterministic coverage index; it does not replace ProductShape, ArcShape or a future quality evaluator.
|
|
12
|
+
|
|
9
13
|
It does not run agents, store transcripts, dispatch work between repositories or replace ProductShape, ArcShape, OpenSpec or an agent host.
|
|
10
14
|
|
|
15
|
+
## gh-aw workflow projection
|
|
16
|
+
|
|
17
|
+
The package includes the six Lighthouse SDLC worker sources, their compiled `.lock.yml` workflows and the router projection. The Lighthouse CLI projects them into `.github/workflows` during initialization, reconciliation or an explicit `lighthouse upgrade --scope sdlc --apply`; package installation alone does not write repository files.
|
|
18
|
+
|
|
19
|
+
The host must provide the runner capability `[self-hosted, linux, lighthouse-sdlc]`, the GitHub and gh-aw credentials required by the compiled workflow, the selected route and stage input, and durable evidence handling for the uploaded stage result. Each worker executes one declared stage and writes `/tmp/gh-aw/agent/stage-result.json`; the host remains responsible for runtime, queues, budgets and model selection. The generic runner capability is a contract, not a SoFa or Bastion identity.
|
|
20
|
+
|
|
21
|
+
The router validates the existing Lighthouse route and `requires` declarations. A normal dispatch runs one stage. Only an explicit `ff` composition chains the selected route, and it stops when a worker fails. The `integrate` worker prepares the configured merge action without merging unless repository policy authorizes it; `review` closes the flow with `gate: null`.
|
|
22
|
+
|
|
11
23
|
Install it with the Lighthouse CLI or run `lighthouse-sdlc verify` from the package installation to check the distributed contract.
|
|
12
24
|
|
|
13
25
|
Run `lighthouse-sdlc triage --input <file>` to validate one structured triage result and receive its proposed workflow and classification labels. The command is read-only by default; `--apply --labels-file <file>` writes only after the report is ready and the labels file still matches the input.
|
|
@@ -16,6 +28,8 @@ Use `lighthouse-sdlc route --route <name>` to inspect a route before running it,
|
|
|
16
28
|
|
|
17
29
|
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.
|
|
18
30
|
|
|
31
|
+
Use `lighthouse-sdlc evidence --input <record-or-index.json>` to validate evidence, or add `--build --output <index.json>` to build an index and its compact record files from full records. The command is read-only unless the output path is explicitly provided.
|
|
32
|
+
|
|
19
33
|
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.
|
|
20
34
|
|
|
21
35
|
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,9 @@
|
|
|
1
|
+
# evidence
|
|
2
|
+
|
|
3
|
+
Validate one evidence record or an evidence index without writing repository or provider state.
|
|
4
|
+
|
|
5
|
+
Use `lighthouse-sdlc evidence --input <record-or-index.json> --format json` to validate an existing artifact.
|
|
6
|
+
|
|
7
|
+
Use `lighthouse-sdlc evidence --input <records.json> --build --output .lighthouse/evidence/index.json` to build a deterministic index from full records and write the compact record files beside it. The input object must contain `records` and may contain `indexId`, `runId`, `repository`, `revision` and `subjects`.
|
|
8
|
+
|
|
9
|
+
The index reports evidence coverage for ProductShape, ArcShape, Lighthouse or other explicitly referenced subjects. It does not decide product scope, architecture ownership or quality policy.
|
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", "browser-e2e", "github-triage", "github-decompose", "reconcile-labels", "github-labels"],
|
|
13
|
+
"utilities": ["status", "aliases", "route", "state", "stage-result", "control-plane", "memory", "launch", "decompose", "stacked", "browser-e2e", "evidence", "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"],
|
|
@@ -25,7 +25,11 @@
|
|
|
25
25
|
"decompositionSchema": "schemas/decomposition.schema.json",
|
|
26
26
|
"capabilitiesSchema": "schemas/capabilities.schema.json",
|
|
27
27
|
"browserE2eSchema": "schemas/browser-e2e.schema.json",
|
|
28
|
+
"evidenceRecordSchema": "schemas/evidence-record.schema.json",
|
|
29
|
+
"evidenceIndexSchema": "schemas/evidence-index.schema.json",
|
|
28
30
|
"capabilityCatalog": "capabilities.json",
|
|
29
|
-
"capabilityCatalogSchema": "schemas/capability-catalog.schema.json"
|
|
31
|
+
"capabilityCatalogSchema": "schemas/capability-catalog.schema.json",
|
|
32
|
+
"commandAliases": "command-aliases.json",
|
|
33
|
+
"commandAliasesSchema": "schemas/command-aliases.schema.json"
|
|
30
34
|
}
|
|
31
35
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@juangcarmona/lighthouse-sdlc",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.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",
|
|
@@ -30,6 +31,7 @@
|
|
|
30
31
|
"stages.json",
|
|
31
32
|
"schemas",
|
|
32
33
|
"skills",
|
|
34
|
+
"workflows",
|
|
33
35
|
"src"
|
|
34
36
|
],
|
|
35
37
|
"bin": {
|
|
@@ -46,9 +48,11 @@
|
|
|
46
48
|
"./routes": "./src/routes.mjs",
|
|
47
49
|
"./reconcile": "./src/reconcile.mjs",
|
|
48
50
|
"./capabilities": "./src/capabilities.mjs",
|
|
51
|
+
"./aliases": "./src/aliases.mjs",
|
|
49
52
|
"./stacked": "./src/stacked.mjs",
|
|
50
53
|
"./workflow": "./src/workflow.mjs",
|
|
51
54
|
"./browser-e2e": "./src/browser-e2e.mjs",
|
|
55
|
+
"./evidence": "./src/evidence.mjs",
|
|
52
56
|
"./contract.json": "./contract.json",
|
|
53
57
|
"./schemas/control-plane.schema.json": "./schemas/control-plane.schema.json",
|
|
54
58
|
"./schemas/triage.schema.json": "./schemas/triage.schema.json",
|
|
@@ -59,6 +63,11 @@
|
|
|
59
63
|
"./schemas/capabilities.schema.json": "./schemas/capabilities.schema.json",
|
|
60
64
|
"./schemas/capability-catalog.schema.json": "./schemas/capability-catalog.schema.json",
|
|
61
65
|
"./schemas/browser-e2e.schema.json": "./schemas/browser-e2e.schema.json",
|
|
62
|
-
"./
|
|
66
|
+
"./schemas/evidence-record.schema.json": "./schemas/evidence-record.schema.json",
|
|
67
|
+
"./schemas/evidence-index.schema.json": "./schemas/evidence-index.schema.json",
|
|
68
|
+
"./capabilities.json": "./capabilities.json",
|
|
69
|
+
"./command-aliases.json": "./command-aliases.json",
|
|
70
|
+
"./schemas/command-aliases.schema.json": "./schemas/command-aliases.schema.json",
|
|
71
|
+
"./workflows/*": "./workflows/*"
|
|
63
72
|
}
|
|
64
73
|
}
|
|
@@ -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,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://github.com/juangcarmona/lighthouse/schemas/lighthouse-evidence-index.schema.json",
|
|
4
|
+
"title": "Lighthouse evidence index",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": false,
|
|
7
|
+
"required": ["schemaVersion", "indexId", "runId", "records", "coverage"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"schemaVersion": { "const": 1 },
|
|
10
|
+
"indexId": { "type": "string", "minLength": 1 },
|
|
11
|
+
"runId": { "type": "string", "minLength": 1 },
|
|
12
|
+
"repository": { "type": "string", "minLength": 1 },
|
|
13
|
+
"revision": { "type": "string", "minLength": 1 },
|
|
14
|
+
"records": { "type": "array", "items": { "$ref": "#/$defs/recordRef" } },
|
|
15
|
+
"coverage": { "type": "array", "items": { "$ref": "#/$defs/coverage" } }
|
|
16
|
+
},
|
|
17
|
+
"$defs": {
|
|
18
|
+
"subject": {
|
|
19
|
+
"type": "object",
|
|
20
|
+
"additionalProperties": false,
|
|
21
|
+
"required": ["type", "id"],
|
|
22
|
+
"properties": {
|
|
23
|
+
"type": { "type": "string", "minLength": 1 },
|
|
24
|
+
"id": { "type": "string", "minLength": 1 }
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"authorityRef": {
|
|
28
|
+
"type": "object",
|
|
29
|
+
"additionalProperties": false,
|
|
30
|
+
"required": ["authority", "id"],
|
|
31
|
+
"properties": {
|
|
32
|
+
"authority": { "type": "string", "minLength": 1 },
|
|
33
|
+
"id": { "type": "string", "minLength": 1 },
|
|
34
|
+
"revision": { "type": "string", "minLength": 1 },
|
|
35
|
+
"path": { "type": "string", "pattern": "^(?!/)(?!.*(?:^|/)\\.\\.(?:/|$)).+" },
|
|
36
|
+
"sha256": { "type": "string", "pattern": "^[a-f0-9]{64}$" }
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"recordRef": {
|
|
40
|
+
"type": "object",
|
|
41
|
+
"additionalProperties": false,
|
|
42
|
+
"required": ["id", "path", "sha256", "kind", "status"],
|
|
43
|
+
"properties": {
|
|
44
|
+
"id": { "type": "string", "minLength": 1 },
|
|
45
|
+
"path": { "type": "string", "pattern": "^(?!/)(?!.*(?:^|/)\\.\\.(?:/|$)).+" },
|
|
46
|
+
"sha256": { "type": "string", "pattern": "^[a-f0-9]{64}$" },
|
|
47
|
+
"kind": { "type": "string", "minLength": 1 },
|
|
48
|
+
"status": { "enum": ["passed", "failed", "unavailable", "unconfigured", "pending-review", "not-applicable"] },
|
|
49
|
+
"subject": { "$ref": "#/$defs/subject" },
|
|
50
|
+
"authorityRefs": { "type": "array", "items": { "$ref": "#/$defs/authorityRef" } }
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
"coverage": {
|
|
54
|
+
"type": "object",
|
|
55
|
+
"additionalProperties": false,
|
|
56
|
+
"required": ["subject", "status", "evidenceIds"],
|
|
57
|
+
"properties": {
|
|
58
|
+
"subject": { "$ref": "#/$defs/subject" },
|
|
59
|
+
"status": { "enum": ["unverified", "observed", "verified", "incomplete", "conflict", "not-applicable"] },
|
|
60
|
+
"evidenceIds": { "type": "array", "items": { "type": "string", "minLength": 1 } },
|
|
61
|
+
"nextAction": { "type": "string", "minLength": 1 }
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://github.com/juangcarmona/lighthouse/schemas/lighthouse-evidence-record.schema.json",
|
|
4
|
+
"title": "Lighthouse evidence record",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": false,
|
|
7
|
+
"required": ["schemaVersion", "id", "runId", "kind", "scope", "status", "producer", "payload"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"schemaVersion": { "const": 1 },
|
|
10
|
+
"id": { "type": "string", "minLength": 1 },
|
|
11
|
+
"runId": { "type": "string", "minLength": 1 },
|
|
12
|
+
"kind": { "type": "string", "pattern": "^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$" },
|
|
13
|
+
"scope": { "enum": ["product", "architecture", "delivery", "release", "run"] },
|
|
14
|
+
"status": { "enum": ["passed", "failed", "unavailable", "unconfigured", "pending-review", "not-applicable"] },
|
|
15
|
+
"producer": { "type": "string", "minLength": 1 },
|
|
16
|
+
"subject": { "$ref": "#/$defs/subject" },
|
|
17
|
+
"authorityRefs": { "type": "array", "items": { "$ref": "#/$defs/authorityRef" } },
|
|
18
|
+
"source": { "$ref": "#/$defs/source" },
|
|
19
|
+
"observedAt": { "type": "string", "format": "date-time" },
|
|
20
|
+
"artifacts": { "type": "array", "items": { "$ref": "#/$defs/artifact" } },
|
|
21
|
+
"payload": { "type": "object" },
|
|
22
|
+
"nextAction": { "type": "string", "minLength": 1 }
|
|
23
|
+
},
|
|
24
|
+
"$defs": {
|
|
25
|
+
"subject": {
|
|
26
|
+
"type": "object",
|
|
27
|
+
"additionalProperties": false,
|
|
28
|
+
"required": ["type", "id"],
|
|
29
|
+
"properties": {
|
|
30
|
+
"type": { "type": "string", "minLength": 1 },
|
|
31
|
+
"id": { "type": "string", "minLength": 1 }
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"authorityRef": {
|
|
35
|
+
"type": "object",
|
|
36
|
+
"additionalProperties": false,
|
|
37
|
+
"required": ["authority", "id"],
|
|
38
|
+
"properties": {
|
|
39
|
+
"authority": { "type": "string", "minLength": 1 },
|
|
40
|
+
"id": { "type": "string", "minLength": 1 },
|
|
41
|
+
"revision": { "type": "string", "minLength": 1 },
|
|
42
|
+
"path": { "type": "string", "pattern": "^(?!/)(?!.*(?:^|/)\\.\\.(?:/|$)).+" },
|
|
43
|
+
"sha256": { "type": "string", "pattern": "^[a-f0-9]{64}$" }
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
"source": {
|
|
47
|
+
"type": "object",
|
|
48
|
+
"additionalProperties": false,
|
|
49
|
+
"properties": {
|
|
50
|
+
"format": { "type": "string", "minLength": 1 },
|
|
51
|
+
"path": { "type": "string", "pattern": "^(?!/)(?!.*(?:^|/)\\.\\.(?:/|$)).+" },
|
|
52
|
+
"command": { "type": "string", "minLength": 1 }
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
"artifact": {
|
|
56
|
+
"type": "object",
|
|
57
|
+
"additionalProperties": false,
|
|
58
|
+
"required": ["type", "path"],
|
|
59
|
+
"properties": {
|
|
60
|
+
"id": { "type": "string", "minLength": 1 },
|
|
61
|
+
"type": { "type": "string", "minLength": 1 },
|
|
62
|
+
"path": { "type": "string", "pattern": "^(?!/)(?!.*(?:^|/)\\.\\.(?:/|$)).+" },
|
|
63
|
+
"sha256": { "type": "string", "pattern": "^[a-f0-9]{64}$" }
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"assets": {
|
|
18
18
|
"type": "object",
|
|
19
19
|
"additionalProperties": false,
|
|
20
|
-
"required": ["routes", "gates", "stages", "controlPlaneSchema", "triageSchema", "stageResultSchema", "stateSchema", "decompositionSchema", "capabilitiesSchema"],
|
|
20
|
+
"required": ["routes", "gates", "stages", "controlPlaneSchema", "triageSchema", "stageResultSchema", "stateSchema", "decompositionSchema", "capabilitiesSchema", "evidenceRecordSchema", "evidenceIndexSchema"],
|
|
21
21
|
"properties": {
|
|
22
22
|
"routes": { "type": "string" },
|
|
23
23
|
"gates": { "type": "string" },
|
|
@@ -27,7 +27,9 @@
|
|
|
27
27
|
"stageResultSchema": { "type": "string" },
|
|
28
28
|
"stateSchema": { "type": "string" },
|
|
29
29
|
"decompositionSchema": { "type": "string" },
|
|
30
|
-
"capabilitiesSchema": { "type": "string" }
|
|
30
|
+
"capabilitiesSchema": { "type": "string" },
|
|
31
|
+
"evidenceRecordSchema": { "type": "string" },
|
|
32
|
+
"evidenceIndexSchema": { "type": "string" }
|
|
31
33
|
}
|
|
32
34
|
}
|
|
33
35
|
},
|
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 };
|
package/src/evidence.mjs
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { resolve } from 'node:path';
|
|
4
|
+
import { EVIDENCE_STATUSES, validateStageResult } from './contracts.mjs';
|
|
5
|
+
import { validateBrowserEvidence } from './browser-e2e.mjs';
|
|
6
|
+
|
|
7
|
+
export const EVIDENCE_SCOPES = Object.freeze(['product', 'architecture', 'delivery', 'release', 'run']);
|
|
8
|
+
export const COVERAGE_STATUSES = Object.freeze(['unverified', 'observed', 'verified', 'incomplete', 'conflict', 'not-applicable']);
|
|
9
|
+
|
|
10
|
+
const evidenceStatusSet = new Set(EVIDENCE_STATUSES);
|
|
11
|
+
const scopeSet = new Set(EVIDENCE_SCOPES);
|
|
12
|
+
const coverageStatusSet = new Set(COVERAGE_STATUSES);
|
|
13
|
+
|
|
14
|
+
function issue(code, message, field) {
|
|
15
|
+
return field ? { code, message, field } : { code, message };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function record(value) {
|
|
19
|
+
return value && typeof value === 'object' && !Array.isArray(value);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function text(value) {
|
|
23
|
+
return typeof value === 'string' && value.trim().length > 0;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function relativePath(value) {
|
|
27
|
+
return text(value) && !value.startsWith('/') && !value.split('/').includes('..');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function validSubject(value) {
|
|
31
|
+
return record(value) && text(value.type) && text(value.id);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function subjectKey(value) {
|
|
35
|
+
return `${value.type}:${value.id}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function validateAuthorityRefs(value, errors, field = 'authorityRefs') {
|
|
39
|
+
if (value === undefined) return;
|
|
40
|
+
if (!Array.isArray(value)) {
|
|
41
|
+
errors.push(issue('evidence.authority-refs', 'authorityRefs must be an array', field));
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
for (const [index, item] of value.entries()) {
|
|
45
|
+
if (!record(item) || !text(item.authority) || !text(item.id)) errors.push(issue('evidence.authority-ref', `authorityRefs[${index}] needs authority and id`, `${field}[${index}]`));
|
|
46
|
+
if (item.path !== undefined && !relativePath(item.path)) errors.push(issue('evidence.authority-path', `authorityRefs[${index}].path must be relative`, `${field}[${index}].path`));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function validateArtifacts(value, errors, field = 'artifacts') {
|
|
51
|
+
if (value === undefined) return;
|
|
52
|
+
if (!Array.isArray(value)) {
|
|
53
|
+
errors.push(issue('evidence.artifacts', 'artifacts must be an array', field));
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const ids = new Set();
|
|
57
|
+
for (const [index, item] of value.entries()) {
|
|
58
|
+
if (!record(item) || !text(item.type) || !relativePath(item.path)) {
|
|
59
|
+
errors.push(issue('evidence.artifact', `artifacts[${index}] needs a type and relative path`, `${field}[${index}]`));
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (item.id !== undefined) {
|
|
63
|
+
if (!text(item.id) || ids.has(item.id)) errors.push(issue('evidence.artifact-id', `artifacts[${index}].id must be unique`, `${field}[${index}].id`));
|
|
64
|
+
ids.add(item.id);
|
|
65
|
+
}
|
|
66
|
+
if (item.sha256 !== undefined && !/^[a-f0-9]{64}$/u.test(item.sha256)) errors.push(issue('evidence.artifact-hash', `artifacts[${index}].sha256 must be a lowercase SHA-256`, `${field}[${index}].sha256`));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function validateEvidenceRecord(value) {
|
|
71
|
+
const errors = [];
|
|
72
|
+
if (!record(value)) return [issue('evidence.invalid', 'evidence record must be an object')];
|
|
73
|
+
if (value.schemaVersion !== 1) errors.push(issue('evidence.schema-version', 'schemaVersion must be 1', 'schemaVersion'));
|
|
74
|
+
for (const field of ['id', 'runId', 'kind', 'producer']) if (!text(value[field]) && field !== 'producer') errors.push(issue(`evidence.${field}`, `${field} must be a non-empty string`, field));
|
|
75
|
+
if (!text(value.producer)) errors.push(issue('evidence.producer', 'producer must be a non-empty string', 'producer'));
|
|
76
|
+
if (!scopeSet.has(value.scope)) errors.push(issue('evidence.scope', `scope must be one of ${EVIDENCE_SCOPES.join(', ')}`, 'scope'));
|
|
77
|
+
if (!evidenceStatusSet.has(value.status)) errors.push(issue('evidence.status', `status must be one of ${EVIDENCE_STATUSES.join(', ')}`, 'status'));
|
|
78
|
+
if (!record(value.payload)) errors.push(issue('evidence.payload', 'payload must be an object', 'payload'));
|
|
79
|
+
if (value.subject !== undefined && !validSubject(value.subject)) errors.push(issue('evidence.subject', 'subject needs a type and id', 'subject'));
|
|
80
|
+
validateAuthorityRefs(value.authorityRefs, errors);
|
|
81
|
+
validateArtifacts(value.artifacts, errors);
|
|
82
|
+
if (value.source !== undefined && (!record(value.source) || (value.source.path !== undefined && !relativePath(value.source.path)))) errors.push(issue('evidence.source', 'source must be an object with a relative path', 'source'));
|
|
83
|
+
if (value.observedAt !== undefined && (typeof value.observedAt !== 'string' || Number.isNaN(Date.parse(value.observedAt)))) errors.push(issue('evidence.observed-at', 'observedAt must be an ISO date-time', 'observedAt'));
|
|
84
|
+
if (['failed', 'unavailable', 'unconfigured', 'pending-review'].includes(value.status) && !text(value.nextAction)) errors.push(issue('evidence.next-action', `status ${value.status} needs nextAction`, 'nextAction'));
|
|
85
|
+
if (value.nextAction !== undefined && !text(value.nextAction)) errors.push(issue('evidence.next-action', 'nextAction must be a non-empty string when present', 'nextAction'));
|
|
86
|
+
return errors;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function clean(value) {
|
|
90
|
+
return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function recordFrom(value, { id, kind, scope = 'delivery', producer, subject, authorityRefs, source, artifacts, nextAction } = {}) {
|
|
94
|
+
const status = kind === 'stage-result'
|
|
95
|
+
? ({ passed: 'passed', failed: 'failed', blocked: 'pending-review', ready: 'pending-review' }[value.status] ?? 'unconfigured')
|
|
96
|
+
: value.status;
|
|
97
|
+
return clean({
|
|
98
|
+
schemaVersion: 1,
|
|
99
|
+
id: id ?? `${kind}:${value.runId ?? value.journey?.id ?? 'unknown'}`,
|
|
100
|
+
runId: value.runId ?? `${kind}:${value.journey?.id ?? 'unknown'}`,
|
|
101
|
+
kind,
|
|
102
|
+
scope,
|
|
103
|
+
status,
|
|
104
|
+
producer,
|
|
105
|
+
subject,
|
|
106
|
+
authorityRefs,
|
|
107
|
+
source,
|
|
108
|
+
observedAt: value.observedAt,
|
|
109
|
+
artifacts: artifacts ?? value.artifacts ?? [],
|
|
110
|
+
payload: value,
|
|
111
|
+
nextAction: nextAction ?? value.nextAction,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function adaptStageResult(value, options = {}) {
|
|
116
|
+
const errors = validateStageResult(value);
|
|
117
|
+
return { record: errors.length ? null : recordFrom(value, { ...options, kind: 'stage-result', producer: options.producer ?? 'lighthouse-sdlc' }), errors };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function adaptBrowserEvidence(value, options = {}) {
|
|
121
|
+
const errors = validateBrowserEvidence(value);
|
|
122
|
+
return { record: errors.length ? null : recordFrom(value, { ...options, kind: 'browser-e2e', producer: options.producer ?? 'lighthouse-sdlc' }), errors };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function hash(value) {
|
|
126
|
+
return createHash('sha256').update(JSON.stringify(value)).digest('hex');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function coverageStatus(records) {
|
|
130
|
+
if (!records.length) return 'unverified';
|
|
131
|
+
const statuses = new Set(records.map((item) => item.status));
|
|
132
|
+
if (statuses.size === 1 && statuses.has('passed')) return 'verified';
|
|
133
|
+
if (statuses.has('failed') && statuses.size > 1) return 'conflict';
|
|
134
|
+
if (statuses.has('failed') || statuses.has('unavailable') || statuses.has('unconfigured') || statuses.has('pending-review')) return 'incomplete';
|
|
135
|
+
if (statuses.size === 1 && statuses.has('not-applicable')) return 'not-applicable';
|
|
136
|
+
return 'observed';
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function buildEvidenceIndex(records, { indexId = 'evidence-index', runId, repository, revision, subjects = [], recordPaths = {} } = {}) {
|
|
140
|
+
const errors = [];
|
|
141
|
+
if (!Array.isArray(records) || records.length === 0) errors.push(issue('evidence.index-records', 'records must contain at least one evidence record', 'records'));
|
|
142
|
+
const validRecords = Array.isArray(records) ? records : [];
|
|
143
|
+
for (const [index, item] of validRecords.entries()) for (const error of validateEvidenceRecord(item)) errors.push({ ...error, field: error.field ? `records[${index}].${error.field}` : `records[${index}]` });
|
|
144
|
+
const ids = new Set();
|
|
145
|
+
for (const [index, item] of validRecords.entries()) {
|
|
146
|
+
if (ids.has(item.id)) errors.push(issue('evidence.duplicate-id', `records[${index}] duplicates evidence id ${item.id}`, `records[${index}].id`));
|
|
147
|
+
ids.add(item.id);
|
|
148
|
+
}
|
|
149
|
+
const effectiveRunId = runId ?? validRecords[0]?.runId;
|
|
150
|
+
if (!text(effectiveRunId)) errors.push(issue('evidence.index-run-id', 'runId must be provided or present on every record', 'runId'));
|
|
151
|
+
const expected = [...subjects, ...validRecords.filter((item) => item.subject).map((item) => item.subject)];
|
|
152
|
+
const subjectMap = new Map();
|
|
153
|
+
for (const item of expected) if (validSubject(item)) subjectMap.set(subjectKey(item), item);
|
|
154
|
+
const refs = validRecords.map((item) => clean({ id: item.id, path: recordPaths[item.id] ?? `records/${item.id}.json`, sha256: hash(item), kind: item.kind, status: item.status, subject: item.subject, authorityRefs: item.authorityRefs }));
|
|
155
|
+
const coverage = [...subjectMap.entries()].map(([key, subject]) => {
|
|
156
|
+
const related = validRecords.filter((item) => item.subject && subjectKey(item.subject) === key);
|
|
157
|
+
const status = coverageStatus(related);
|
|
158
|
+
return clean({ subject, status, evidenceIds: related.map((item) => item.id), nextAction: status === 'verified' || status === 'not-applicable' ? undefined : `Collect or resolve evidence for ${key}.` });
|
|
159
|
+
});
|
|
160
|
+
if (errors.length) return { index: null, errors };
|
|
161
|
+
return {
|
|
162
|
+
index: clean({ schemaVersion: 1, indexId, runId: effectiveRunId, repository, revision, records: refs, coverage }),
|
|
163
|
+
errors: [],
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function validateEvidenceIndex(value) {
|
|
168
|
+
const errors = [];
|
|
169
|
+
if (!record(value)) return [issue('evidence-index.invalid', 'evidence index must be an object')];
|
|
170
|
+
if (value.schemaVersion !== 1) errors.push(issue('evidence-index.schema-version', 'schemaVersion must be 1', 'schemaVersion'));
|
|
171
|
+
for (const field of ['indexId', 'runId']) if (!text(value[field])) errors.push(issue(`evidence-index.${field}`, `${field} must be a non-empty string`, field));
|
|
172
|
+
if (!Array.isArray(value.records)) errors.push(issue('evidence-index.records', 'records must be an array', 'records'));
|
|
173
|
+
else {
|
|
174
|
+
const ids = new Set();
|
|
175
|
+
for (const [index, item] of value.records.entries()) {
|
|
176
|
+
if (!record(item) || !text(item.id) || !relativePath(item.path) || !/^[a-f0-9]{64}$/u.test(item.sha256) || !text(item.kind) || !evidenceStatusSet.has(item.status)) errors.push(issue('evidence-index.record-ref', `records[${index}] needs id, relative path, SHA-256, kind and status`, `records[${index}]`));
|
|
177
|
+
if (item?.id && ids.has(item.id)) errors.push(issue('evidence-index.duplicate-id', `records[${index}] duplicates evidence id ${item.id}`, `records[${index}].id`));
|
|
178
|
+
if (item?.id) ids.add(item.id);
|
|
179
|
+
if (item?.subject !== undefined && !validSubject(item.subject)) errors.push(issue('evidence-index.subject', `records[${index}].subject needs a type and id`, `records[${index}].subject`));
|
|
180
|
+
validateAuthorityRefs(item?.authorityRefs, errors, `records[${index}].authorityRefs`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
if (!Array.isArray(value.coverage)) errors.push(issue('evidence-index.coverage', 'coverage must be an array', 'coverage'));
|
|
184
|
+
else {
|
|
185
|
+
const subjects = new Set();
|
|
186
|
+
for (const [index, item] of value.coverage.entries()) {
|
|
187
|
+
if (!record(item) || !validSubject(item.subject) || !coverageStatusSet.has(item.status) || !Array.isArray(item.evidenceIds)) errors.push(issue('evidence-index.coverage-entry', `coverage[${index}] needs subject, status and evidenceIds`, `coverage[${index}]`));
|
|
188
|
+
const key = item?.subject && validSubject(item.subject) ? subjectKey(item.subject) : null;
|
|
189
|
+
if (key && subjects.has(key)) errors.push(issue('evidence-index.coverage-duplicate', `coverage[${index}] duplicates subject ${key}`, `coverage[${index}].subject`));
|
|
190
|
+
if (key) subjects.add(key);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return errors;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export function evidenceReport(value) {
|
|
197
|
+
const errors = value?.kind ? validateEvidenceRecord(value) : validateEvidenceIndex(value);
|
|
198
|
+
return { schemaVersion: 1, status: errors.length ? 'invalid' : 'valid', valid: errors.length === 0, errors };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export async function readEvidenceIndex(path, cwd = process.cwd()) {
|
|
202
|
+
const value = JSON.parse(await readFile(resolve(cwd, path), 'utf8'));
|
|
203
|
+
return { value, errors: validateEvidenceIndex(value) };
|
|
204
|
+
}
|