@juangcarmona/lighthouse-sdlc 0.0.0-stage → 0.3.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 CHANGED
@@ -1,3 +1,15 @@
1
- # Temporary Holding Version
1
+ # Lighthouse SDLC
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ This package provides the provider-neutral delivery contract for Lighthouse adopters.
4
+
5
+ It owns the six lifecycle skills, their command entrypoints, route and gate descriptors, the triage schema and verifier, the control-plane schema and deterministic package check.
6
+
7
+ It does not run agents, store transcripts, dispatch work between repositories or replace ProductShape, ArcShape, OpenSpec or an agent host.
8
+
9
+ Install it with the Lighthouse CLI or run `lighthouse-sdlc verify` from the package installation to check the distributed contract.
10
+
11
+ 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.
12
+
13
+ Evidence is explicit: an absent field appears in `missingEvidence`, a check that cannot run appears in `unavailableChecks`, and a judgement awaiting review appears in `pendingReview`. Only `provided` evidence can make a report ready to apply.
14
+
15
+ The package is a source of lifecycle instructions and contracts; an adopter's host adapter decides where provider-specific projections are installed.
@@ -0,0 +1,3 @@
1
+ # `/lighthouse:implement`
2
+
3
+ Run the `lighthouse-sdlc-implement` skill for one approved plan and stop at the `done` gate.
@@ -0,0 +1,3 @@
1
+ # `/lighthouse:integrate`
2
+
3
+ Run the `lighthouse-sdlc-integrate` skill for one completed slice and prepare the configured merge action.
@@ -0,0 +1,3 @@
1
+ # `/lighthouse:propose`
2
+
3
+ Run the `lighthouse-sdlc-propose` skill for one Ready work item and stop at the `planned` gate.
@@ -0,0 +1,3 @@
1
+ # `/lighthouse:refine`
2
+
3
+ Run the `lighthouse-sdlc-refine` skill for one triaged work item and stop at the `ready` gate.
@@ -0,0 +1,3 @@
1
+ # `/lighthouse:review`
2
+
3
+ Run the `lighthouse-sdlc-review` skill for one integrated change and finish with a review record.
@@ -0,0 +1,5 @@
1
+ # `/lighthouse:status`
2
+
3
+ Read `.lighthouse/sdlc.json` and report the selected SDLC package, routes, gates, state provider, memory scopes, role mappings and adapter gaps.
4
+
5
+ Read-only completion means the report matches the declared control plane and names the next supported action for every gap.
@@ -0,0 +1,9 @@
1
+ # `/lighthouse:triage`
2
+
3
+ Run the `lighthouse-sdlc-triage` skill for one work item and stop at the `triaged` gate.
4
+
5
+ The structured result records `size`, `urgency`, `impact`, `risk`, `blast-radius`, dependencies, route, owner, confidence and evidence, then proposes workflow and classification labels from the project's configuration.
6
+
7
+ Use `lighthouse-sdlc triage --input <file>` for deterministic validation. The command is read-only unless an explicit `--apply --labels-file <file>` request is supplied.
8
+
9
+ Represent evidence with `status: provided`, `status: unavailable` or `status: pending-review`; omit the item when evidence is absent. The report keeps those cases in `missingEvidence`, `unavailableChecks` and `pendingReview` respectively.
package/contract.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "$schema": "./schemas/sdlc-contract.schema.json",
3
+ "name": "@juangcarmona/lighthouse-sdlc",
4
+ "version": "0.1.0",
5
+ "stages": [
6
+ { "name": "triage", "skill": "skills/triage/SKILL.md", "command": "commands/triage.md", "gate": "triaged" },
7
+ { "name": "refine", "skill": "skills/refine/SKILL.md", "command": "commands/refine.md", "gate": "ready" },
8
+ { "name": "propose", "skill": "skills/propose/SKILL.md", "command": "commands/propose.md", "gate": "planned" },
9
+ { "name": "implement", "skill": "skills/implement/SKILL.md", "command": "commands/implement.md", "gate": "done" },
10
+ { "name": "integrate", "skill": "skills/integrate/SKILL.md", "command": "commands/integrate.md", "gate": "done" },
11
+ { "name": "review", "skill": "skills/review/SKILL.md", "command": "commands/review.md", "gate": null }
12
+ ],
13
+ "utilities": ["status"],
14
+ "routes": ["supervised", "plan-only", "unattended", "hotfix", "stacked"],
15
+ "metaCommands": ["ff", "hotfix", "decompose", "stacked"],
16
+ "gates": ["triaged", "ready", "planned", "done"],
17
+ "assets": {
18
+ "routes": "routes.json",
19
+ "gates": "gates.json",
20
+ "controlPlaneSchema": "schemas/control-plane.schema.json",
21
+ "triageSchema": "schemas/triage.schema.json"
22
+ }
23
+ }
package/gates.json ADDED
@@ -0,0 +1,10 @@
1
+ {
2
+ "$schema": "./schemas/gates.schema.json",
3
+ "version": 1,
4
+ "gates": [
5
+ { "id": "triaged", "stage": "triage", "question": "Is the work item understood enough to choose a route?", "decisionOwner": "adopter" },
6
+ { "id": "ready", "stage": "refine", "question": "Is the problem defined well enough to design a solution?", "decisionOwner": "adopter" },
7
+ { "id": "planned", "stage": "propose", "question": "Is the solution and its evidence plan ready to implement?", "decisionOwner": "adopter" },
8
+ { "id": "done", "stage": "implement", "question": "Does the change satisfy its deterministic and named judgement checks?", "decisionOwner": "adopter" }
9
+ ]
10
+ }
@@ -0,0 +1,3 @@
1
+ # `/lighthouse:decompose`
2
+
3
+ Split a large work item into dependency-ordered slices with independent acceptance evidence and a parent record.
@@ -0,0 +1,5 @@
1
+ # `/lighthouse:ff`
2
+
3
+ Compose the stages declared by a selected route only when its independent review, correction limit and durable evidence are available.
4
+
5
+ Stop at every human-only or forge-protected decision and report the next stage instead of invoking it implicitly.
@@ -0,0 +1,3 @@
1
+ # `/lighthouse:hotfix`
2
+
3
+ Use the `hotfix` route for a confirmed defect with a rollback plan, keeping triage, Done evidence, integration and review visible.
@@ -0,0 +1,3 @@
1
+ # `/lighthouse:stacked`
2
+
3
+ Prepare dependency-ordered changes as isolated pull requests and keep each slice within its declared scope.
package/package.json CHANGED
@@ -1,6 +1,43 @@
1
1
  {
2
2
  "name": "@juangcarmona/lighthouse-sdlc",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.3.0",
4
+ "type": "module",
5
+ "description": "Provider-neutral SDLC skills, commands and route contracts for Lighthouse adopters",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/juangcarmona/lighthouse.git",
9
+ "directory": "src/packages/sdlc"
10
+ },
11
+ "homepage": "https://juangcarmona.github.io/lighthouse/",
12
+ "bugs": {
13
+ "url": "https://github.com/juangcarmona/lighthouse/issues"
14
+ },
15
+ "publishConfig": {
16
+ "access": "public",
17
+ "provenance": true
18
+ },
19
+ "engines": {
20
+ "node": ">=22"
21
+ },
22
+ "files": [
23
+ "commands",
24
+ "contract.json",
25
+ "gates.json",
26
+ "meta-commands",
27
+ "README.md",
28
+ "routes.json",
29
+ "schemas",
30
+ "skills",
31
+ "src"
32
+ ],
33
+ "bin": {
34
+ "lighthouse-sdlc": "./src/sdlc.mjs"
35
+ },
36
+ "exports": {
37
+ ".": "./src/sdlc.mjs",
38
+ "./triage": "./src/triage.mjs",
39
+ "./contract.json": "./contract.json",
40
+ "./schemas/control-plane.schema.json": "./schemas/control-plane.schema.json",
41
+ "./schemas/triage.schema.json": "./schemas/triage.schema.json"
42
+ }
43
+ }
package/routes.json ADDED
@@ -0,0 +1,11 @@
1
+ {
2
+ "$schema": "./schemas/routes.schema.json",
3
+ "version": 1,
4
+ "routes": [
5
+ { "id": "supervised", "stages": ["triage", "refine", "propose", "implement", "integrate", "review"], "humanGates": ["triaged", "ready", "planned", "done"] },
6
+ { "id": "plan-only", "stages": ["triage", "refine", "propose"], "humanGates": ["triaged", "ready", "planned"] },
7
+ { "id": "unattended", "stages": ["triage", "refine", "propose", "implement", "integrate", "review"], "humanGates": ["triaged", "ready", "planned", "done"], "requires": ["bounded-agent-run", "independent-review"] },
8
+ { "id": "hotfix", "stages": ["triage", "implement", "integrate", "review"], "humanGates": ["triaged", "done"], "requires": ["confirmed-defect", "rollback-plan"] },
9
+ { "id": "stacked", "stages": ["triage", "refine", "propose", "implement", "integrate", "review"], "humanGates": ["triaged", "ready", "planned", "done"], "requires": ["dependency-order", "isolated-change-scope"] }
10
+ ]
11
+ }
@@ -0,0 +1,27 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://github.com/juangcarmona/lighthouse/schemas/lighthouse-sdlc-control-plane.schema.json",
4
+ "title": "Lighthouse SDLC control plane",
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "required": ["schemaVersion", "package", "routes", "gates", "state", "memory", "roles", "adapters"],
8
+ "properties": {
9
+ "$schema": { "type": "string" },
10
+ "schemaVersion": { "const": 1 },
11
+ "package": {
12
+ "type": "object",
13
+ "additionalProperties": false,
14
+ "required": ["name", "version"],
15
+ "properties": {
16
+ "name": { "const": "@juangcarmona/lighthouse-sdlc" },
17
+ "version": { "type": "string", "pattern": "^\\d+\\.\\d+\\.\\d+$" }
18
+ }
19
+ },
20
+ "routes": { "type": "array", "items": { "type": "string" } },
21
+ "gates": { "type": "array", "items": { "type": "string" } },
22
+ "state": { "type": "object", "additionalProperties": false, "required": ["provider"], "properties": { "provider": { "type": "string" } } },
23
+ "memory": { "type": "object", "additionalProperties": false, "required": ["provider", "scopes"], "properties": { "provider": { "type": "string" }, "scopes": { "type": "array", "items": { "type": "string" } } } },
24
+ "roles": { "type": "object", "additionalProperties": { "type": "string" } },
25
+ "adapters": { "type": "object", "additionalProperties": { "type": "string" } }
26
+ }
27
+ }
@@ -0,0 +1,26 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://github.com/juangcarmona/lighthouse/schemas/sdlc-gates.schema.json",
4
+ "title": "Lighthouse SDLC gates",
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "required": ["$schema", "version", "gates"],
8
+ "properties": {
9
+ "$schema": { "type": "string" },
10
+ "version": { "const": 1 },
11
+ "gates": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/gate" } }
12
+ },
13
+ "$defs": {
14
+ "gate": {
15
+ "type": "object",
16
+ "additionalProperties": false,
17
+ "required": ["id", "stage", "question", "decisionOwner"],
18
+ "properties": {
19
+ "id": { "type": "string" },
20
+ "stage": { "type": "string" },
21
+ "question": { "type": "string" },
22
+ "decisionOwner": { "type": "string" }
23
+ }
24
+ }
25
+ }
26
+ }
@@ -0,0 +1,26 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://github.com/juangcarmona/lighthouse/schemas/sdlc-routes.schema.json",
4
+ "title": "Lighthouse SDLC routes",
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "required": ["$schema", "version", "routes"],
8
+ "properties": {
9
+ "$schema": { "type": "string" },
10
+ "version": { "const": 1 },
11
+ "routes": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/route" } }
12
+ },
13
+ "$defs": {
14
+ "route": {
15
+ "type": "object",
16
+ "additionalProperties": false,
17
+ "required": ["id", "stages", "humanGates"],
18
+ "properties": {
19
+ "id": { "type": "string" },
20
+ "stages": { "type": "array", "minItems": 1, "items": { "type": "string" } },
21
+ "humanGates": { "type": "array", "items": { "type": "string" } },
22
+ "requires": { "type": "array", "items": { "type": "string" } }
23
+ }
24
+ }
25
+ }
26
+ }
@@ -0,0 +1,42 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://github.com/juangcarmona/lighthouse/schemas/sdlc-contract.schema.json",
4
+ "title": "Lighthouse SDLC package contract",
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "required": ["$schema", "name", "version", "stages", "utilities", "routes", "metaCommands", "gates", "assets"],
8
+ "properties": {
9
+ "$schema": { "type": "string" },
10
+ "name": { "const": "@juangcarmona/lighthouse-sdlc" },
11
+ "version": { "type": "string", "pattern": "^\\d+\\.\\d+\\.\\d+$" },
12
+ "stages": { "type": "array", "minItems": 6, "items": { "$ref": "#/$defs/stage" } },
13
+ "utilities": { "type": "array", "items": { "type": "string" } },
14
+ "routes": { "type": "array", "items": { "type": "string" } },
15
+ "metaCommands": { "type": "array", "items": { "type": "string" } },
16
+ "gates": { "type": "array", "items": { "type": "string" } },
17
+ "assets": {
18
+ "type": "object",
19
+ "additionalProperties": false,
20
+ "required": ["routes", "gates", "controlPlaneSchema", "triageSchema"],
21
+ "properties": {
22
+ "routes": { "type": "string" },
23
+ "gates": { "type": "string" },
24
+ "controlPlaneSchema": { "type": "string" },
25
+ "triageSchema": { "type": "string" }
26
+ }
27
+ }
28
+ },
29
+ "$defs": {
30
+ "stage": {
31
+ "type": "object",
32
+ "additionalProperties": false,
33
+ "required": ["name", "skill", "command", "gate"],
34
+ "properties": {
35
+ "name": { "type": "string" },
36
+ "skill": { "type": "string" },
37
+ "command": { "type": "string" },
38
+ "gate": { "type": ["string", "null"] }
39
+ }
40
+ }
41
+ }
42
+ }
@@ -0,0 +1,62 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://github.com/juangcarmona/lighthouse/schemas/lighthouse-sdlc-triage.schema.json",
4
+ "title": "Lighthouse triage input",
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "required": ["schemaVersion", "workItem", "currentLabels", "assessment", "configuration"],
8
+ "properties": {
9
+ "schemaVersion": { "const": 1 },
10
+ "workItem": {
11
+ "type": "object",
12
+ "additionalProperties": false,
13
+ "required": ["id", "title"],
14
+ "properties": {
15
+ "id": { "type": "string", "minLength": 1 },
16
+ "title": { "type": "string", "minLength": 1 },
17
+ "type": { "type": "string", "minLength": 1 }
18
+ }
19
+ },
20
+ "currentLabels": { "type": "array", "items": { "type": "string" } },
21
+ "assessment": {
22
+ "type": "object",
23
+ "additionalProperties": false,
24
+ "required": ["size", "urgency", "impact", "risk", "blastRadius", "dependencies", "route", "nextState", "owner", "confidence", "evidence"],
25
+ "properties": {
26
+ "size": { "enum": ["S", "M", "L", "XL"] },
27
+ "urgency": { "enum": ["normal", "asap", "now"] },
28
+ "impact": { "enum": ["low", "medium", "high"] },
29
+ "risk": { "enum": ["low", "medium", "high"] },
30
+ "blastRadius": { "enum": ["low", "medium", "high", "unknown"] },
31
+ "dependencies": { "type": "array" },
32
+ "route": { "enum": ["supervised", "plan-only", "unattended", "hotfix", "stacked"] },
33
+ "nextState": { "type": "string", "minLength": 1 },
34
+ "owner": { "type": "string", "minLength": 1 },
35
+ "confidence": { "type": "object" },
36
+ "evidence": {
37
+ "type": "array",
38
+ "items": {
39
+ "type": "object",
40
+ "additionalProperties": false,
41
+ "required": ["field"],
42
+ "properties": {
43
+ "field": { "enum": ["size", "urgency", "impact", "risk", "blast-radius"] },
44
+ "status": { "enum": ["provided", "unavailable", "pending-review"] },
45
+ "source": { "type": "string", "minLength": 1 },
46
+ "reason": { "type": "string", "minLength": 1 }
47
+ }
48
+ }
49
+ }
50
+ }
51
+ },
52
+ "configuration": {
53
+ "type": "object",
54
+ "additionalProperties": false,
55
+ "required": ["labels", "transitions"],
56
+ "properties": {
57
+ "labels": { "type": "array" },
58
+ "transitions": { "type": "array" }
59
+ }
60
+ }
61
+ }
62
+ }
@@ -0,0 +1,20 @@
1
+ ---
2
+ name: lighthouse-sdlc-implement
3
+ description: Use when a planned work item is approved for bounded implementation and needs red-green checks, evidence and Definition of Done verification.
4
+ ---
5
+
6
+ # Implement
7
+
8
+ Implement one planned slice at a time.
9
+
10
+ ## Steps
11
+
12
+ 1. Read the approved plan, slice boundary, allowed paths and Definition of Done.
13
+ 2. Start with the smallest failing deterministic check when the stack supports red-green development.
14
+ 3. Make the smallest change that turns the check green, then run the required unit, integration and applicable higher-level checks.
15
+ 4. Record changed paths, commands, results, evidence and any finding that needs a human decision.
16
+ 5. Stop at the `done` gate when the Definition of Done is satisfied.
17
+
18
+ ## Completion
19
+
20
+ The slice has reviewable code and evidence for every required check, with no silent scope expansion or automatic transition to integration.
@@ -0,0 +1,20 @@
1
+ ---
2
+ name: lighthouse-sdlc-integrate
3
+ description: Use when an implemented slice is ready for dependency checks, pull request coordination, tracker synchronization and merge preparation.
4
+ ---
5
+
6
+ # Integrate
7
+
8
+ Prepare a completed slice for the project's forge and branch policy.
9
+
10
+ ## Steps
11
+
12
+ 1. Recheck the Done evidence and the declared dependency order.
13
+ 2. Verify the branch, commit, pull request and required CI checks against project policy.
14
+ 3. Synchronize the work item and pull request state using the project's configured labels.
15
+ 4. Prepare the merge or stacked change action and leave protected or human decisions explicit.
16
+ 5. Return the integration record without merging unless the selected route and repository policy authorize it.
17
+
18
+ ## Completion
19
+
20
+ The change is ready for its configured merge action, every required check is linked and unresolved decisions have an owner.
@@ -0,0 +1,20 @@
1
+ ---
2
+ name: lighthouse-sdlc-propose
3
+ description: Use when a ready work item needs a solution proposal, OpenSpec change artifacts, test impact and a human-readable change overview.
4
+ ---
5
+
6
+ # Propose
7
+
8
+ Describe the smallest implementable solution for a Ready item.
9
+
10
+ ## Steps
11
+
12
+ 1. Read the Ready evidence and inspect the current product and architecture authorities.
13
+ 2. Create or update the OpenSpec change artifacts through the installed OpenSpec workflow.
14
+ 3. Describe the solution, interfaces, data effects, test impact, slices, risks and Definition of Done.
15
+ 4. Write `change-overview` prose with the Humanizer skill while preserving facts, links, commands, frontmatter and code.
16
+ 5. Stop at the `planned` gate and state the human decision required before implementation.
17
+
18
+ ## Completion
19
+
20
+ The change artifacts explain the future state, the slices are dependency ordered, the evidence plan is explicit and the Planned decision remains visible.
@@ -0,0 +1,20 @@
1
+ ---
2
+ name: lighthouse-sdlc-refine
3
+ description: Use when a triaged work item needs a precise problem, scope, acceptance criteria, dependencies and Definition of Ready.
4
+ ---
5
+
6
+ # Refine
7
+
8
+ Turn a triaged item into a problem that can be designed.
9
+
10
+ ## Steps
11
+
12
+ 1. Read the triage record, product context and relevant architecture evidence.
13
+ 2. State the problem, affected users, constraints, acceptance criteria and explicit out-of-scope work.
14
+ 3. Resolve or record dependencies, risks, assumptions and missing evidence.
15
+ 4. Check the Definition of Ready and return the evidence that supports each check.
16
+ 5. Stop at the `ready` gate and name the next decision.
17
+
18
+ ## Completion
19
+
20
+ The work item has a bounded problem statement, testable acceptance criteria, resolved owners for open dependencies and a Ready decision.
@@ -0,0 +1,20 @@
1
+ ---
2
+ name: lighthouse-sdlc-review
3
+ description: Use after integration to record delivery evidence, findings, follow-up work and process learning for one completed change.
4
+ ---
5
+
6
+ # Review
7
+
8
+ Close the delivery record and make the next improvement visible.
9
+
10
+ ## Steps
11
+
12
+ 1. Read the change overview, implementation evidence, integration result and review findings.
13
+ 2. Classify findings as accepted, deferred, fixed or rejected with an owner and follow-up reference.
14
+ 3. Record the evidence, outcome, useful learning and any product or architecture follow-up.
15
+ 4. Confirm the work item and pull request have the project's final labels.
16
+ 5. End the route with a review record and return the next backlog decision without opening another delivery gate.
17
+
18
+ ## Completion
19
+
20
+ The completed change has a durable review record, classified findings and a clear follow-up owner.
@@ -0,0 +1,21 @@
1
+ ---
2
+ name: lighthouse-sdlc-triage
3
+ description: Use when a work item enters the Lighthouse lifecycle and needs size, urgency, impact, dependencies, risk, blast radius and a delivery route.
4
+ ---
5
+
6
+ # Triage
7
+
8
+ Classify one work item before refinement.
9
+
10
+ ## Steps
11
+
12
+ 1. Read the work item, its linked Product Change and the repository control plane.
13
+ 2. Inspect the affected code and architecture evidence with the available repository tools.
14
+ 3. Record size, urgency, impact, dependencies, risk, blast radius, route and owner with confidence and evidence for each assessment.
15
+ 4. Mark each evidence field as provided, unavailable or pending-review; leave absent fields without an evidence item.
16
+ 5. Propose only the project's declared workflow and classification labels and report the transition.
17
+ 6. Return the structured triage record and stop at the `triaged` gate.
18
+
19
+ ## Completion
20
+
21
+ The record names the route, owner, labels, assessments, confidence, evidence and next decision, and no later stage has started.
package/src/sdlc.mjs ADDED
@@ -0,0 +1,126 @@
1
+ #!/usr/bin/env node
2
+ import { access, readFile, writeFile } from 'node:fs/promises';
3
+ import { constants } from 'node:fs';
4
+ import { dirname, resolve } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { applyTriageLabels, buildTriageReport } from './triage.mjs';
7
+
8
+ const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
9
+ const contractPath = resolve(packageRoot, 'contract.json');
10
+
11
+ async function readJson(path) {
12
+ return JSON.parse(await readFile(path, 'utf8'));
13
+ }
14
+
15
+ async function exists(path) {
16
+ try { await access(path, constants.F_OK); return true; } catch { return false; }
17
+ }
18
+
19
+ async function verifyContract() {
20
+ const problems = [];
21
+ let contract;
22
+ try { contract = await readJson(contractPath); }
23
+ catch (error) { return [`Cannot read contract.json: ${error.message}`]; }
24
+ if (contract.name !== '@juangcarmona/lighthouse-sdlc') problems.push('contract name is incorrect');
25
+ if (!/^\d+\.\d+\.\d+$/.test(contract.version ?? '')) problems.push('contract version must be semver-like');
26
+ const stages = Array.isArray(contract.stages) ? contract.stages : [];
27
+ const expectedStages = ['triage', 'refine', 'propose', 'implement', 'integrate', 'review'];
28
+ if (stages.map((stage) => stage.name).join(',') !== expectedStages.join(',')) problems.push('stages must be triage, refine, propose, implement, integrate, review in order');
29
+ for (const stage of stages) {
30
+ for (const asset of [stage.skill, stage.command]) if (!(await exists(resolve(packageRoot, asset)))) problems.push(`missing stage asset ${asset}`);
31
+ }
32
+ for (const asset of [contract.assets?.routes, contract.assets?.gates, contract.assets?.controlPlaneSchema, contract.assets?.triageSchema]) {
33
+ if (!asset || !(await exists(resolve(packageRoot, asset)))) problems.push(`missing contract asset ${asset ?? '(empty)'}`);
34
+ }
35
+ for (const asset of ['routes.json', 'gates.json', 'schemas/routes.schema.json', 'schemas/gates.schema.json', 'schemas/control-plane.schema.json', 'schemas/triage.schema.json', 'src/triage.mjs']) {
36
+ if (!(await exists(resolve(packageRoot, asset)))) problems.push(`missing package asset ${asset}`);
37
+ }
38
+ try {
39
+ const routes = await readJson(resolve(packageRoot, 'routes.json'));
40
+ const routeIds = routes.routes?.map((route) => route.id) ?? [];
41
+ if (routeIds.join(',') !== contract.routes.join(',')) problems.push('routes.json does not match contract routes');
42
+ } catch (error) { problems.push(`Cannot read routes.json: ${error.message}`); }
43
+ try {
44
+ const gates = await readJson(resolve(packageRoot, 'gates.json'));
45
+ const gateIds = gates.gates?.map((gate) => gate.id) ?? [];
46
+ if (gateIds.join(',') !== contract.gates.join(',')) problems.push('gates.json does not match contract gates');
47
+ } catch (error) { problems.push(`Cannot read gates.json: ${error.message}`); }
48
+ for (const command of ['status', ...expectedStages]) if (!(await exists(resolve(packageRoot, `commands/${command}.md`)))) problems.push(`missing command asset ${command}`);
49
+ for (const metaCommand of contract.metaCommands ?? []) if (!(await exists(resolve(packageRoot, `meta-commands/${metaCommand}.md`)))) problems.push(`missing meta-command asset ${metaCommand}`);
50
+ return problems;
51
+ }
52
+
53
+ function option(args, name) {
54
+ const index = args.indexOf(name);
55
+ if (index !== -1) return args[index + 1];
56
+ const prefix = `${name}=`;
57
+ return args.find((arg) => arg.startsWith(prefix))?.slice(prefix.length);
58
+ }
59
+
60
+ function triageText(report) {
61
+ const lines = [
62
+ `Triage: ${report.status}`,
63
+ `Work item: ${report.workItem?.id ?? '(unknown)'} — ${report.workItem?.title ?? '(untitled)'}`,
64
+ `Size: ${report.assessment.size}`,
65
+ `Urgency: ${report.assessment.urgency}`,
66
+ `Impact: ${report.assessment.impact}`,
67
+ `Risk: ${report.assessment.risk}`,
68
+ `Blast radius: ${report.assessment.blastRadius}`,
69
+ `Route: ${report.assessment.route ?? '(unknown)'}`,
70
+ `Transition: ${report.transition.from ?? '(unknown)'} -> ${report.transition.to ?? '(unknown)'}`,
71
+ `Labels: ${report.labels.proposed.join(', ') || '(none)'}`,
72
+ ];
73
+ if (report.missingEvidence.length) lines.push(`Missing evidence: ${report.missingEvidence.join(', ')}`);
74
+ if (report.unavailableChecks.length) lines.push(`Unavailable checks: ${report.unavailableChecks.join(', ')}`);
75
+ if (report.pendingReview.length) lines.push(`Pending review: ${report.pendingReview.join(', ')}`);
76
+ for (const error of report.errors) lines.push(`Error: ${error.message}`);
77
+ lines.push(report.applied ? 'Applied: yes' : 'Applied: no (read-only)');
78
+ return `${lines.join('\n')}\n`;
79
+ }
80
+
81
+ async function runTriage(args) {
82
+ const inputPath = option(args, '--input');
83
+ const configPath = option(args, '--config');
84
+ const labelsPath = option(args, '--labels-file');
85
+ const format = option(args, '--format') ?? 'text';
86
+ const apply = args.includes('--apply');
87
+ if (!inputPath || !['text', 'json'].includes(format) || (apply && !labelsPath)) {
88
+ console.error('Usage: lighthouse-sdlc triage --input <file> [--config <file>] [--format <text|json>] [--apply --labels-file <file>]');
89
+ process.exit(2);
90
+ }
91
+ const input = await readJson(resolve(process.cwd(), inputPath));
92
+ if (configPath) input.configuration = await readJson(resolve(process.cwd(), configPath));
93
+ const report = buildTriageReport(input);
94
+ report.applied = false;
95
+ if (apply && report.canApply) {
96
+ const labelsFile = resolve(process.cwd(), labelsPath);
97
+ const current = await readJson(labelsFile);
98
+ const expected = JSON.stringify(report.labels.current);
99
+ if (!Array.isArray(current) || JSON.stringify([...new Set(current)].sort()) !== expected) throw new Error('labels file does not match currentLabels; refusing to apply the triage projection');
100
+ await writeFile(labelsFile, `${JSON.stringify(applyTriageLabels(report), null, 2)}\n`);
101
+ report.applied = true;
102
+ }
103
+ if (format === 'json') console.log(JSON.stringify(report, null, 2));
104
+ else process.stdout.write(triageText(report));
105
+ if (report.status !== 'ready') process.exit(1);
106
+ }
107
+
108
+ const command = process.argv[2] ?? 'verify';
109
+ if (!['contract', 'verify', 'triage'].includes(command)) {
110
+ console.error('Usage: lighthouse-sdlc <contract|verify|triage>');
111
+ process.exit(2);
112
+ }
113
+ if (command === 'contract') {
114
+ console.log(await readFile(contractPath, 'utf8'));
115
+ } else if (command === 'triage') {
116
+ await runTriage(process.argv.slice(3));
117
+ } else {
118
+ const problems = await verifyContract();
119
+ if (problems.length) {
120
+ for (const problem of problems) console.error(problem);
121
+ process.exit(1);
122
+ }
123
+ console.log('Lighthouse SDLC package contract is valid.');
124
+ }
125
+
126
+ export { contractPath, runTriage, verifyContract };
package/src/triage.mjs ADDED
@@ -0,0 +1,284 @@
1
+ const DIMENSIONS = Object.freeze({
2
+ size: ['S', 'M', 'L', 'XL'],
3
+ urgency: ['normal', 'asap', 'now'],
4
+ impact: ['low', 'medium', 'high'],
5
+ risk: ['low', 'medium', 'high'],
6
+ 'blast-radius': ['low', 'medium', 'high'],
7
+ });
8
+
9
+ const CONFIDENCE = new Set(['low', 'medium', 'high']);
10
+ const ROLES = new Set(['state', 'command', 'classification']);
11
+ const ROUTES = new Set(['supervised', 'plan-only', 'unattended', 'hotfix', 'stacked']);
12
+ const DEPENDENCY_STATES = new Set(['resolved', 'open', 'unknown']);
13
+ const EVIDENCE_STATES = new Set(['provided', 'unavailable', 'pending-review']);
14
+ const HEX = /^[0-9a-f]{6}$/iu;
15
+ const EVIDENCE_FIELDS = ['size', 'urgency', 'impact', 'risk', 'blast-radius'];
16
+
17
+ export const suggestedLabelColors = Object.freeze({
18
+ 'size-s': 'BFE3FF',
19
+ 'size-m': '80C2FF',
20
+ 'size-l': '4DA3FF',
21
+ 'size-xl': '1F6FEB',
22
+ 'urgency-normal': '8B949E',
23
+ 'urgency-asap': 'D29922',
24
+ 'urgency-now': 'CF222E',
25
+ 'impact-low': 'E9D5FF',
26
+ 'impact-medium': 'A371F7',
27
+ 'impact-high': '6E40C9',
28
+ 'risk-low': 'FFEBE9',
29
+ 'risk-medium': 'F0883E',
30
+ 'risk-high': 'CF222E',
31
+ 'blast-radius-low': 'C7F0E8',
32
+ 'blast-radius-medium': '4FB3A4',
33
+ 'blast-radius-high': '0F766E',
34
+ });
35
+
36
+ function isRecord(value) {
37
+ return value && typeof value === 'object' && !Array.isArray(value);
38
+ }
39
+
40
+ function normalizedValue(dimension, value) {
41
+ if (dimension === 'size') return String(value ?? '').toUpperCase();
42
+ return String(value ?? '').toLowerCase();
43
+ }
44
+
45
+ function labelFor(dimension, value) {
46
+ return `${dimension}-${normalizedValue(dimension, value).toLowerCase()}`;
47
+ }
48
+
49
+ function issue(code, message, field) {
50
+ return field ? { code, field, message } : { code, message };
51
+ }
52
+
53
+ function addIssue(issues, code, message, field) {
54
+ issues.push(issue(code, message, field));
55
+ }
56
+
57
+ function validateConfiguration(configuration) {
58
+ const errors = [];
59
+ const labels = Array.isArray(configuration?.labels) ? configuration.labels : null;
60
+ const byName = new Map();
61
+ const classifications = new Map();
62
+ const stateLabels = new Set();
63
+ const commandLabels = new Set();
64
+
65
+ if (!labels) addIssue(errors, 'configuration.labels', 'configuration.labels must be an array', 'configuration.labels');
66
+ for (const descriptor of labels ?? []) {
67
+ if (!isRecord(descriptor) || typeof descriptor.name !== 'string' || !descriptor.name.trim()) {
68
+ addIssue(errors, 'label.invalid', 'every label descriptor needs a non-empty name', 'configuration.labels');
69
+ continue;
70
+ }
71
+ const name = descriptor.name.trim();
72
+ if (byName.has(name)) addIssue(errors, 'label.duplicate', `label ${name} is declared more than once`, 'configuration.labels');
73
+ byName.set(name, descriptor);
74
+ if (!ROLES.has(descriptor.role)) {
75
+ addIssue(errors, 'label.role', `label ${name} must use role state, command or classification`, 'configuration.labels');
76
+ continue;
77
+ }
78
+ if (descriptor.role === 'state') stateLabels.add(name);
79
+ if (descriptor.role === 'command') commandLabels.add(name);
80
+ if (descriptor.role !== 'classification') continue;
81
+ const dimension = String(descriptor.dimension ?? '').toLowerCase();
82
+ const value = normalizedValue(dimension, descriptor.value);
83
+ if (!Object.hasOwn(DIMENSIONS, dimension) || !DIMENSIONS[dimension].includes(value)) {
84
+ addIssue(errors, 'label.classification', `label ${name} has an invalid classification dimension or value`, 'configuration.labels');
85
+ continue;
86
+ }
87
+ if (labelFor(dimension, value) !== name) {
88
+ addIssue(errors, 'label.syntax', `classification label ${name} must use the ${labelFor(dimension, value)} syntax`, 'configuration.labels');
89
+ }
90
+ const key = `${dimension}:${value}`;
91
+ if (classifications.has(key)) addIssue(errors, 'label.classification-duplicate', `classification ${key} is declared more than once`, 'configuration.labels');
92
+ classifications.set(key, name);
93
+ if (descriptor.color !== undefined && (typeof descriptor.color !== 'string' || !HEX.test(descriptor.color))) {
94
+ addIssue(errors, 'label.color', `classification label ${name} color must be six hexadecimal characters`, 'configuration.labels');
95
+ }
96
+ }
97
+
98
+ const transitions = Array.isArray(configuration?.transitions) ? configuration.transitions : [];
99
+ const transitionSet = new Set();
100
+ for (const transition of transitions) {
101
+ if (!isRecord(transition) || typeof transition.from !== 'string' || typeof transition.to !== 'string') {
102
+ addIssue(errors, 'transition.invalid', 'every transition needs from and to labels', 'configuration.transitions');
103
+ continue;
104
+ }
105
+ const key = `${transition.from}->${transition.to}`;
106
+ if (transitionSet.has(key)) addIssue(errors, 'transition.duplicate', `transition ${key} is declared more than once`, 'configuration.transitions');
107
+ transitionSet.add(key);
108
+ }
109
+
110
+ return { errors, byName, classifications, stateLabels, commandLabels, transitionSet };
111
+ }
112
+
113
+ function validateWorkItem(workItem, errors) {
114
+ if (!isRecord(workItem)) {
115
+ addIssue(errors, 'work-item.invalid', 'workItem must be an object', 'workItem');
116
+ return;
117
+ }
118
+ for (const field of ['id', 'title']) {
119
+ if (typeof workItem[field] !== 'string' || !workItem[field].trim()) addIssue(errors, `work-item.${field}`, `workItem.${field} must be a non-empty string`, `workItem.${field}`);
120
+ }
121
+ if (workItem.type !== undefined && (typeof workItem.type !== 'string' || !workItem.type.trim())) addIssue(errors, 'work-item.type', 'workItem.type must be a non-empty string when provided', 'workItem.type');
122
+ }
123
+
124
+ function validateAssessment(assessment, errors) {
125
+ if (!isRecord(assessment)) {
126
+ addIssue(errors, 'assessment.invalid', 'assessment must be an object', 'assessment');
127
+ return;
128
+ }
129
+ for (const dimension of Object.keys(DIMENSIONS)) {
130
+ const value = normalizedValue(dimension, assessment[dimension === 'blast-radius' ? 'blastRadius' : dimension]);
131
+ if (!DIMENSIONS[dimension].includes(value) && !(dimension === 'blast-radius' && value === 'unknown')) {
132
+ addIssue(errors, `assessment.${dimension}`, `${dimension} has an unsupported value`, `assessment.${dimension}`);
133
+ }
134
+ }
135
+ if (!ROUTES.has(assessment.route)) addIssue(errors, 'assessment.route', `route must be one of ${[...ROUTES].join(', ')}`, 'assessment.route');
136
+ if (typeof assessment.nextState !== 'string' || !assessment.nextState.trim()) addIssue(errors, 'assessment.next-state', 'nextState must be a non-empty string', 'assessment.nextState');
137
+ if (typeof assessment.owner !== 'string' || !assessment.owner.trim()) addIssue(errors, 'assessment.owner', 'owner must be a non-empty string', 'assessment.owner');
138
+ if (!isRecord(assessment.confidence)) {
139
+ addIssue(errors, 'assessment.confidence', 'confidence must be an object with one value per triage dimension', 'assessment.confidence');
140
+ } else {
141
+ for (const dimension of EVIDENCE_FIELDS) if (!CONFIDENCE.has(assessment.confidence[dimension])) addIssue(errors, `assessment.confidence.${dimension}`, `${dimension} confidence must be low, medium or high`, `assessment.confidence.${dimension}`);
142
+ }
143
+ if (!Array.isArray(assessment.dependencies)) {
144
+ addIssue(errors, 'assessment.dependencies', 'dependencies must be an array', 'assessment.dependencies');
145
+ } else {
146
+ for (const dependency of assessment.dependencies) {
147
+ if (!isRecord(dependency) || typeof dependency.id !== 'string' || !DEPENDENCY_STATES.has(dependency.status)) addIssue(errors, 'assessment.dependency', 'each dependency needs an id and a resolved, open or unknown status', 'assessment.dependencies');
148
+ }
149
+ }
150
+ if (!Array.isArray(assessment.evidence)) {
151
+ addIssue(errors, 'assessment.evidence', 'evidence must be an array', 'assessment.evidence');
152
+ } else {
153
+ const fields = new Set();
154
+ for (const evidence of assessment.evidence) {
155
+ const status = evidence?.status ?? 'provided';
156
+ const valid = isRecord(evidence) && EVIDENCE_FIELDS.includes(evidence.field) && EVIDENCE_STATES.has(status);
157
+ if (!valid) {
158
+ addIssue(errors, 'assessment.evidence-item', 'each evidence item needs a known field and a provided, unavailable or pending-review status', 'assessment.evidence');
159
+ continue;
160
+ }
161
+ if (fields.has(evidence.field)) addIssue(errors, 'assessment.evidence-duplicate', `${evidence.field} has more than one evidence status`, 'assessment.evidence');
162
+ fields.add(evidence.field);
163
+ if (status === 'provided' && (typeof evidence.source !== 'string' || !evidence.source.trim())) addIssue(errors, 'assessment.evidence-source', 'provided evidence needs a non-empty source', 'assessment.evidence');
164
+ if (status !== 'provided' && (typeof evidence.reason !== 'string' || !evidence.reason.trim())) addIssue(errors, 'assessment.evidence-reason', `${status} evidence needs a non-empty reason`, 'assessment.evidence');
165
+ }
166
+ }
167
+ }
168
+
169
+ function currentState(labels, configuration) {
170
+ const states = labels.filter((label) => configuration.stateLabels.has(label));
171
+ return { states, value: states.length === 1 ? states[0] : null };
172
+ }
173
+
174
+ function classificationState(labels, configuration) {
175
+ const byDimension = new Map();
176
+ for (const label of labels) {
177
+ const descriptor = configuration.byName.get(label);
178
+ if (!descriptor || descriptor.role !== 'classification') continue;
179
+ const list = byDimension.get(descriptor.dimension) ?? [];
180
+ list.push(label);
181
+ byDimension.set(descriptor.dimension, list);
182
+ }
183
+ return byDimension;
184
+ }
185
+
186
+ function proposedLabels(currentLabels, assessment, configuration) {
187
+ const result = new Set(currentLabels.filter((label) => {
188
+ const descriptor = configuration.byName.get(label);
189
+ return !descriptor || (descriptor.role !== 'state' && descriptor.role !== 'classification' && descriptor.role !== 'command');
190
+ }));
191
+ for (const label of configuration.stateLabels) if (currentLabels.includes(label)) result.add(label);
192
+ for (const label of configuration.commandLabels) if (currentLabels.includes(label)) result.delete(label);
193
+ const state = currentState(currentLabels, configuration).value;
194
+ if (state) result.delete(state);
195
+ if (assessment.nextState) result.add(assessment.nextState);
196
+ for (const dimension of Object.keys(DIMENSIONS)) {
197
+ const key = dimension === 'blast-radius' ? 'blastRadius' : dimension;
198
+ const value = normalizedValue(dimension, assessment[key]);
199
+ for (const label of configuration.classifications.values()) {
200
+ const descriptor = configuration.byName.get(label);
201
+ if (descriptor?.dimension === dimension) result.delete(label);
202
+ }
203
+ if (value !== 'UNKNOWN') {
204
+ const label = configuration.classifications.get(`${dimension}:${value}`);
205
+ if (label) result.add(label);
206
+ }
207
+ }
208
+ return [...result].sort((a, b) => a.localeCompare(b));
209
+ }
210
+
211
+ export function buildTriageReport(input) {
212
+ const errors = [];
213
+ if (!isRecord(input)) return { schemaVersion: 1, status: 'invalid', valid: false, canApply: false, errors: [issue('input.invalid', 'triage input must be an object')], warnings: [], missingEvidence: [], unavailableChecks: [], pendingReview: [], labels: { current: [], proposed: [] } };
214
+ if (input.schemaVersion !== 1) addIssue(errors, 'input.schema-version', 'schemaVersion must be 1', 'schemaVersion');
215
+ const configuration = validateConfiguration(input.configuration);
216
+ errors.push(...configuration.errors);
217
+ const currentLabels = Array.isArray(input.currentLabels) ? [...new Set(input.currentLabels.filter((label) => typeof label === 'string'))].sort() : [];
218
+ if (!Array.isArray(input.currentLabels)) addIssue(errors, 'labels.invalid', 'currentLabels must be an array', 'currentLabels');
219
+ validateWorkItem(input.workItem, errors);
220
+ validateAssessment(input.assessment, errors);
221
+
222
+ const state = currentState(currentLabels, configuration);
223
+ if (state.states.length > 1) addIssue(errors, 'state.ambiguous', `work item has multiple workflow state labels: ${state.states.join(', ')}`, 'currentLabels');
224
+ if (state.states.length === 0) addIssue(errors, 'state.missing', 'work item needs one configured workflow state label', 'currentLabels');
225
+ const assessment = isRecord(input.assessment) ? input.assessment : {};
226
+ if (state.value && typeof assessment.nextState === 'string' && configuration.stateLabels.has(assessment.nextState)) {
227
+ if (state.value !== assessment.nextState && !configuration.transitionSet.has(`${state.value}->${assessment.nextState}`)) addIssue(errors, 'transition.illegal', `transition ${state.value}->${assessment.nextState} is not configured`, 'assessment.nextState');
228
+ } else if (typeof assessment.nextState === 'string' && assessment.nextState && !configuration.stateLabels.has(assessment.nextState)) {
229
+ addIssue(errors, 'transition.unconfigured', `next state ${assessment.nextState} is not configured`, 'assessment.nextState');
230
+ }
231
+ const currentClassifications = classificationState(currentLabels, configuration);
232
+ for (const [dimension, values] of currentClassifications) if (values.length > 1) addIssue(errors, 'classification.ambiguous', `work item has multiple ${dimension} classification labels: ${values.join(', ')}`, 'currentLabels');
233
+ for (const dimension of Object.keys(DIMENSIONS)) {
234
+ const value = normalizedValue(dimension, assessment[dimension === 'blast-radius' ? 'blastRadius' : dimension]);
235
+ if (value !== 'UNKNOWN' && configuration.classifications.has(`${dimension}:${value}`) === false) addIssue(errors, 'classification.unconfigured', `${labelFor(dimension, value)} is not configured`, `assessment.${dimension}`);
236
+ }
237
+
238
+ const evidence = Array.isArray(assessment.evidence) ? assessment.evidence : [];
239
+ const evidenceFields = new Set(evidence.filter(isRecord).map((item) => item.field));
240
+ const missingEvidence = EVIDENCE_FIELDS.filter((field) => !evidenceFields.has(field));
241
+ const unavailableChecks = evidence.filter((item) => isRecord(item) && item.status === 'unavailable').map((item) => item.field);
242
+ const pendingReview = evidence.filter((item) => isRecord(item) && item.status === 'pending-review').map((item) => item.field);
243
+ const warnings = [
244
+ ...missingEvidence.map((field) => issue('evidence.missing', `${field} has no evidence record`, field)),
245
+ ...unavailableChecks.map((field) => issue('evidence.unavailable', `${field} could not be checked`, field)),
246
+ ...pendingReview.map((field) => issue('evidence.pending-review', `${field} is waiting for judgement`, field)),
247
+ ];
248
+ const proposed = proposedLabels(currentLabels, assessment, configuration);
249
+ const status = errors.length ? 'invalid' : warnings.length ? 'blocked' : 'ready';
250
+ return {
251
+ schemaVersion: 1,
252
+ status,
253
+ valid: errors.length === 0,
254
+ canApply: status === 'ready',
255
+ workItem: input.workItem ?? null,
256
+ assessment: {
257
+ size: normalizedValue('size', assessment.size),
258
+ urgency: normalizedValue('urgency', assessment.urgency),
259
+ impact: normalizedValue('impact', assessment.impact),
260
+ risk: normalizedValue('risk', assessment.risk),
261
+ blastRadius: normalizedValue('blast-radius', assessment.blastRadius),
262
+ dependencies: assessment.dependencies ?? [],
263
+ route: assessment.route ?? null,
264
+ owner: assessment.owner ?? null,
265
+ confidence: assessment.confidence ?? {},
266
+ evidence,
267
+ },
268
+ transition: { from: state.value, to: assessment.nextState ?? null },
269
+ labels: { current: currentLabels, proposed },
270
+ missingEvidence,
271
+ unavailableChecks,
272
+ pendingReview,
273
+ suggestedColors: suggestedLabelColors,
274
+ errors,
275
+ warnings,
276
+ };
277
+ }
278
+
279
+ export function applyTriageLabels(report) {
280
+ if (!report?.canApply) throw new Error('Triage report is not ready to apply. Resolve its errors or outstanding evidence state first.');
281
+ return [...report.labels.proposed];
282
+ }
283
+
284
+ export { DIMENSIONS, ROUTES, validateConfiguration };