@juangcarmona/lighthouse-sdlc 0.3.0 → 0.4.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
@@ -2,7 +2,7 @@
2
2
 
3
3
  This package provides the provider-neutral delivery contract for Lighthouse adopters.
4
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.
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
7
  It does not run agents, store transcripts, dispatch work between repositories or replace ProductShape, ArcShape, OpenSpec or an agent host.
8
8
 
@@ -10,6 +10,14 @@ Install it with the Lighthouse CLI or run `lighthouse-sdlc verify` from the pack
10
10
 
11
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
12
 
13
+ 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
+
15
+ 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
+
17
+ 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.
18
+
19
+ Declare selected adoption capabilities in a JSON file that follows `schemas/capabilities.schema.json`. `reconcile-labels` previews and optionally applies the first local label projection; `github-labels` reads the repository labels through `gh`, creates only missing labels after an explicit `--apply`, preserves existing values and records `.lighthouse/reconciliation/labels.json`.
20
+
13
21
  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
22
 
15
23
  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:control-plane`
2
+
3
+ Validate the repository-owned control-plane declaration for package ownership, routes, state, memory scopes, roles, tools and adapters.
@@ -0,0 +1,3 @@
1
+ # `/lighthouse:decompose`
2
+
3
+ Produce a read-only dependency-ordered slice plan. Apply it only after parent identity, independent acceptance evidence, dependencies and cycle checks pass.
@@ -0,0 +1,3 @@
1
+ # `/lighthouse:github-decompose`
2
+
3
+ Project an accepted dependency-ordered slice plan into GitHub child issues. Preview first and use `--apply` only after the parent and slices have been reviewed.
@@ -0,0 +1,3 @@
1
+ # Reconcile GitHub labels
2
+
3
+ Run `lighthouse-sdlc github-labels --repo <owner/repo> --input .lighthouse/capabilities.json` to preview the repository label plan. Add `--apply` only after reviewing it; the command creates missing labels, preserves existing labels and writes reconciliation evidence.
@@ -0,0 +1,3 @@
1
+ # `/lighthouse:github-triage`
2
+
3
+ Read one GitHub issue into the provider-neutral triage contract. Keep the operation read-only unless the adopter explicitly requests a validated label projection.
@@ -0,0 +1,3 @@
1
+ # `/lighthouse:launch`
2
+
3
+ Validate a bounded parent or child agent launch request against the declared role, tools, write scope, output schema, limits and stop conditions without executing an agent.
@@ -0,0 +1,3 @@
1
+ # `/lighthouse:memory`
2
+
3
+ Validate one typed memory record against its declared scope without storing it or making it a product, architecture or approval authority.
@@ -0,0 +1,3 @@
1
+ # Reconcile labels
2
+
3
+ Run `lighthouse-sdlc reconcile-labels --input .lighthouse/capabilities.json --existing labels.json` to preview the label capability plan. Add `--apply` only after reviewing it; the command adds missing labels to the local fixture, preserves existing values and writes reconciliation evidence.
@@ -0,0 +1,3 @@
1
+ # `/lighthouse:route`
2
+
3
+ Inspect a named delivery route, its ordered stages, prerequisites, human gates and next decision before allowing a mutation.
@@ -0,0 +1,3 @@
1
+ # `/lighthouse:stacked`
2
+
3
+ Produce a dependency-ordered pull-request plan with isolated branches and explicit base changes. Apply only after the slice plan is accepted.
@@ -0,0 +1,3 @@
1
+ # `/lighthouse:stage-result`
2
+
3
+ Validate the structured result of a lifecycle stage before a route consumes its evidence or advances its gate.
@@ -0,0 +1,3 @@
1
+ # `/lighthouse:state`
2
+
3
+ Replay append-only workflow events and report the current state for each run. Reject illegal transitions and duplicate event identifiers.
package/contract.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "./schemas/sdlc-contract.schema.json",
3
3
  "name": "@juangcarmona/lighthouse-sdlc",
4
- "version": "0.1.0",
4
+ "version": "0.2.0",
5
5
  "stages": [
6
6
  { "name": "triage", "skill": "skills/triage/SKILL.md", "command": "commands/triage.md", "gate": "triaged" },
7
7
  { "name": "refine", "skill": "skills/refine/SKILL.md", "command": "commands/refine.md", "gate": "ready" },
@@ -10,14 +10,19 @@
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"],
13
+ "utilities": ["status", "route", "state", "stage-result", "control-plane", "memory", "launch", "decompose", "stacked", "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"],
17
17
  "assets": {
18
18
  "routes": "routes.json",
19
19
  "gates": "gates.json",
20
+ "stages": "stages.json",
20
21
  "controlPlaneSchema": "schemas/control-plane.schema.json",
21
- "triageSchema": "schemas/triage.schema.json"
22
+ "triageSchema": "schemas/triage.schema.json",
23
+ "stageResultSchema": "schemas/stage-result.schema.json",
24
+ "stateSchema": "schemas/workflow-state.schema.json",
25
+ "decompositionSchema": "schemas/decomposition.schema.json",
26
+ "capabilitiesSchema": "schemas/capabilities.schema.json"
22
27
  }
23
28
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juangcarmona/lighthouse-sdlc",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "type": "module",
5
5
  "description": "Provider-neutral SDLC skills, commands and route contracts for Lighthouse adopters",
6
6
  "repository": {
@@ -26,6 +26,7 @@
26
26
  "meta-commands",
27
27
  "README.md",
28
28
  "routes.json",
29
+ "stages.json",
29
30
  "schemas",
30
31
  "skills",
31
32
  "src"
@@ -36,8 +37,22 @@
36
37
  "exports": {
37
38
  ".": "./src/sdlc.mjs",
38
39
  "./triage": "./src/triage.mjs",
40
+ "./contracts": "./src/contracts.mjs",
41
+ "./control-plane": "./src/control-plane.mjs",
42
+ "./decompose": "./src/decompose.mjs",
43
+ "./github": "./src/github.mjs",
44
+ "./memory": "./src/memory.mjs",
45
+ "./routes": "./src/routes.mjs",
46
+ "./reconcile": "./src/reconcile.mjs",
47
+ "./stacked": "./src/stacked.mjs",
48
+ "./workflow": "./src/workflow.mjs",
39
49
  "./contract.json": "./contract.json",
40
50
  "./schemas/control-plane.schema.json": "./schemas/control-plane.schema.json",
41
- "./schemas/triage.schema.json": "./schemas/triage.schema.json"
51
+ "./schemas/triage.schema.json": "./schemas/triage.schema.json",
52
+ "./schemas/stages.schema.json": "./schemas/stages.schema.json",
53
+ "./schemas/stage-result.schema.json": "./schemas/stage-result.schema.json",
54
+ "./schemas/workflow-state.schema.json": "./schemas/workflow-state.schema.json",
55
+ "./schemas/decomposition.schema.json": "./schemas/decomposition.schema.json",
56
+ "./schemas/capabilities.schema.json": "./schemas/capabilities.schema.json"
42
57
  }
43
58
  }
@@ -0,0 +1,27 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://github.com/juangcarmona/lighthouse/schemas/lighthouse-capabilities.schema.json",
4
+ "title": "Lighthouse capability declaration",
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "required": ["schemaVersion", "capabilities"],
8
+ "properties": {
9
+ "$schema": { "type": "string" },
10
+ "schemaVersion": { "const": 1 },
11
+ "capabilities": {
12
+ "type": "array",
13
+ "minItems": 1,
14
+ "items": {
15
+ "type": "object",
16
+ "additionalProperties": false,
17
+ "required": ["id", "provider", "version"],
18
+ "properties": {
19
+ "id": { "type": "string", "minLength": 1 },
20
+ "provider": { "type": "string", "minLength": 1 },
21
+ "version": { "type": "string", "minLength": 1 },
22
+ "configuration": { "type": "object" }
23
+ }
24
+ }
25
+ }
26
+ }
27
+ }
@@ -19,9 +19,24 @@
19
19
  },
20
20
  "routes": { "type": "array", "items": { "type": "string" } },
21
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" } },
22
+ "state": { "type": "object", "additionalProperties": false, "required": ["provider"], "properties": { "provider": { "type": "string" }, "states": { "type": "array", "items": { "enum": ["ready", "running", "waiting-human", "blocked", "succeeded", "failed", "cancelled"] } } } },
23
+ "memory": { "type": "object", "additionalProperties": false, "required": ["provider", "scopes"], "properties": { "provider": { "type": "string" }, "scopes": { "type": "array", "items": { "enum": ["run", "project", "shared"] } }, "shared": { "type": "boolean" } } },
24
+ "roles": { "type": "object", "additionalProperties": { "anyOf": [{ "type": "string" }, { "$ref": "#/$defs/role" }] } },
25
25
  "adapters": { "type": "object", "additionalProperties": { "type": "string" } }
26
+ },
27
+ "$defs": {
28
+ "role": {
29
+ "type": "object",
30
+ "additionalProperties": false,
31
+ "required": ["inputSchema", "outputSchema", "tools", "writeScope"],
32
+ "properties": {
33
+ "inputSchema": { "type": "string" },
34
+ "outputSchema": { "type": "string" },
35
+ "tools": { "type": "array", "items": { "type": "string" } },
36
+ "writeScope": { "type": "array", "items": { "type": "string" } },
37
+ "limits": { "type": "object" },
38
+ "stopConditions": { "type": "array", "items": { "type": "string" } }
39
+ }
40
+ }
26
41
  }
27
42
  }
@@ -0,0 +1,13 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://github.com/juangcarmona/lighthouse/schemas/lighthouse-decomposition.schema.json",
4
+ "title": "Lighthouse decomposition input",
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "required": ["schemaVersion", "parent", "slices"],
8
+ "properties": {
9
+ "schemaVersion": { "const": 1 },
10
+ "parent": { "type": "object", "required": ["id", "title"], "properties": { "id": { "type": "string" }, "title": { "type": "string" } }, "additionalProperties": true },
11
+ "slices": { "type": "array", "minItems": 1, "items": { "type": "object", "required": ["id", "title", "acceptance", "dependencies"], "properties": { "id": { "type": "string" }, "title": { "type": "string" }, "acceptance": { "type": "array", "items": { "type": "string" } }, "dependencies": { "type": "array", "items": { "type": "string" } }, "scope": { "type": "array", "items": { "type": "string" } }, "labels": { "type": "array", "items": { "type": "string" } } }, "additionalProperties": true } }
12
+ }
13
+ }
@@ -17,12 +17,17 @@
17
17
  "assets": {
18
18
  "type": "object",
19
19
  "additionalProperties": false,
20
- "required": ["routes", "gates", "controlPlaneSchema", "triageSchema"],
20
+ "required": ["routes", "gates", "stages", "controlPlaneSchema", "triageSchema", "stageResultSchema", "stateSchema", "decompositionSchema", "capabilitiesSchema"],
21
21
  "properties": {
22
22
  "routes": { "type": "string" },
23
23
  "gates": { "type": "string" },
24
+ "stages": { "type": "string" },
24
25
  "controlPlaneSchema": { "type": "string" },
25
- "triageSchema": { "type": "string" }
26
+ "triageSchema": { "type": "string" },
27
+ "stageResultSchema": { "type": "string" },
28
+ "stateSchema": { "type": "string" },
29
+ "decompositionSchema": { "type": "string" },
30
+ "capabilitiesSchema": { "type": "string" }
26
31
  }
27
32
  }
28
33
  },
@@ -0,0 +1,22 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://github.com/juangcarmona/lighthouse/schemas/lighthouse-stage-result.schema.json",
4
+ "title": "Lighthouse SDLC stage result",
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "required": ["schemaVersion", "runId", "stage", "status", "gate", "evidence", "missingEvidence", "unavailableChecks", "pendingReview", "nextAction"],
8
+ "properties": {
9
+ "schemaVersion": { "const": 1 },
10
+ "runId": { "type": "string", "minLength": 1 },
11
+ "stage": { "enum": ["triage", "refine", "propose", "implement", "integrate", "review"] },
12
+ "status": { "enum": ["ready", "blocked", "failed", "passed"] },
13
+ "gate": { "type": ["string", "null"] },
14
+ "input": { "type": "object" },
15
+ "output": { "type": "object" },
16
+ "evidence": { "type": "array", "items": { "type": "object", "required": ["id", "status"], "properties": { "id": { "type": "string" }, "status": { "enum": ["passed", "failed", "unavailable", "unconfigured", "pending-review", "not-applicable"] }, "source": { "type": "string" }, "reason": { "type": "string" } }, "additionalProperties": false } },
17
+ "missingEvidence": { "type": "array", "items": { "type": "string" } },
18
+ "unavailableChecks": { "type": "array", "items": { "type": "string" } },
19
+ "pendingReview": { "type": "array", "items": { "type": "string" } },
20
+ "nextAction": { "type": "string", "minLength": 1 }
21
+ }
22
+ }
@@ -0,0 +1,27 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://github.com/juangcarmona/lighthouse/schemas/sdlc-stages.schema.json",
4
+ "title": "Lighthouse SDLC stages",
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "required": ["$schema", "version", "stages"],
8
+ "properties": {
9
+ "$schema": { "type": "string" },
10
+ "version": { "const": 1 },
11
+ "stages": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/stage" } }
12
+ },
13
+ "$defs": {
14
+ "stage": {
15
+ "type": "object",
16
+ "additionalProperties": false,
17
+ "required": ["id", "gate", "input", "output", "mutation"],
18
+ "properties": {
19
+ "id": { "type": "string" },
20
+ "gate": { "type": ["string", "null"] },
21
+ "input": { "type": "string" },
22
+ "output": { "type": "string" },
23
+ "mutation": { "enum": ["none", "explicit", "workspace"] }
24
+ }
25
+ }
26
+ }
27
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://github.com/juangcarmona/lighthouse/schemas/lighthouse-workflow-state.schema.json",
4
+ "title": "Lighthouse workflow state event",
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "required": ["schemaVersion", "eventId", "runId", "route", "stage", "from", "to", "actor", "timestamp", "reason", "evidence"],
8
+ "properties": {
9
+ "schemaVersion": { "const": 1 },
10
+ "eventId": { "type": "string", "minLength": 1 },
11
+ "runId": { "type": "string", "minLength": 1 },
12
+ "parentRunId": { "type": "string" },
13
+ "route": { "type": "string", "minLength": 1 },
14
+ "stage": { "type": "string", "minLength": 1 },
15
+ "from": { "type": ["string", "null"] },
16
+ "to": { "type": "string", "enum": ["ready", "running", "waiting-human", "blocked", "succeeded", "failed", "cancelled"] },
17
+ "actor": { "type": "string", "minLength": 1 },
18
+ "timestamp": { "type": "string", "minLength": 1 },
19
+ "reason": { "type": "string", "minLength": 1 },
20
+ "evidence": { "type": "array" }
21
+ }
22
+ }
@@ -13,8 +13,8 @@ Implement one planned slice at a time.
13
13
  2. Start with the smallest failing deterministic check when the stack supports red-green development.
14
14
  3. Make the smallest change that turns the check green, then run the required unit, integration and applicable higher-level checks.
15
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.
16
+ 5. Return a stage result with `stage: implement`, `gate: done`, implementation evidence as output and separate `missingEvidence`, `unavailableChecks` and `pendingReview` arrays; stop at `done` when the Definition of Done is satisfied.
17
17
 
18
18
  ## Completion
19
19
 
20
- The slice has reviewable code and evidence for every required check, with no silent scope expansion or automatic transition to integration.
20
+ The slice has reviewable code and a valid `done` stage result for every required check, with no silent scope expansion or automatic transition to integration.
@@ -13,8 +13,8 @@ Prepare a completed slice for the project's forge and branch policy.
13
13
  2. Verify the branch, commit, pull request and required CI checks against project policy.
14
14
  3. Synchronize the work item and pull request state using the project's configured labels.
15
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.
16
+ 5. Return a stage result with `stage: integrate`, the integration record as output, evidence entries and separate `missingEvidence`, `unavailableChecks` and `pendingReview` arrays without merging unless the selected route and repository policy authorize it.
17
17
 
18
18
  ## Completion
19
19
 
20
- The change is ready for its configured merge action, every required check is linked and unresolved decisions have an owner.
20
+ The change is ready for its configured merge action, every required check is linked and unresolved decisions have an owner in the integration result.
@@ -13,8 +13,8 @@ Describe the smallest implementable solution for a Ready item.
13
13
  2. Create or update the OpenSpec change artifacts through the installed OpenSpec workflow.
14
14
  3. Describe the solution, interfaces, data effects, test impact, slices, risks and Definition of Done.
15
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.
16
+ 5. Return a stage result with `stage: propose`, `gate: planned`, the change artifacts as output, evidence entries and separate `missingEvidence`, `unavailableChecks` and `pendingReview` arrays.
17
17
 
18
18
  ## Completion
19
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.
20
+ The change artifacts explain the future state, the slices are dependency ordered, the evidence plan is explicit and the `planned` decision remains visible.
@@ -13,8 +13,8 @@ Turn a triaged item into a problem that can be designed.
13
13
  2. State the problem, affected users, constraints, acceptance criteria and explicit out-of-scope work.
14
14
  3. Resolve or record dependencies, risks, assumptions and missing evidence.
15
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.
16
+ 5. Return a stage result with `stage: refine`, `gate: ready`, the bounded problem as output, evidence entries and separate `missingEvidence`, `unavailableChecks` and `pendingReview` arrays.
17
17
 
18
18
  ## Completion
19
19
 
20
- The work item has a bounded problem statement, testable acceptance criteria, resolved owners for open dependencies and a Ready decision.
20
+ The work item has a bounded problem statement, testable acceptance criteria, resolved owners for open dependencies and a `ready` stage result that names the next decision.
@@ -13,8 +13,8 @@ Close the delivery record and make the next improvement visible.
13
13
  2. Classify findings as accepted, deferred, fixed or rejected with an owner and follow-up reference.
14
14
  3. Record the evidence, outcome, useful learning and any product or architecture follow-up.
15
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.
16
+ 5. Return a stage result with `stage: review`, `gate: null`, the review record as output, evidence entries and separate `missingEvidence`, `unavailableChecks` and `pendingReview` arrays without opening another delivery gate.
17
17
 
18
18
  ## Completion
19
19
 
20
- The completed change has a durable review record, classified findings and a clear follow-up owner.
20
+ The completed change has a durable review result, classified findings and a clear follow-up owner.
@@ -14,8 +14,8 @@ Classify one work item before refinement.
14
14
  3. Record size, urgency, impact, dependencies, risk, blast radius, route and owner with confidence and evidence for each assessment.
15
15
  4. Mark each evidence field as provided, unavailable or pending-review; leave absent fields without an evidence item.
16
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.
17
+ 6. Return a stage result with `stage: triage`, `gate: triaged`, the triage record as output, evidence entries and separate `missingEvidence`, `unavailableChecks` and `pendingReview` arrays.
18
18
 
19
19
  ## Completion
20
20
 
21
- The record names the route, owner, labels, assessments, confidence, evidence and next decision, and no later stage has started.
21
+ The record names the route, owner, labels, assessments, confidence and next decision, and the stage result stops at `triaged` without starting a later stage.
@@ -0,0 +1,39 @@
1
+ export const STAGES = Object.freeze(['triage', 'refine', 'propose', 'implement', 'integrate', 'review']);
2
+ export const GATES = Object.freeze(['triaged', 'ready', 'planned', 'done']);
3
+ export const EVIDENCE_STATUSES = Object.freeze(['passed', 'failed', 'unavailable', 'unconfigured', 'pending-review', 'not-applicable']);
4
+ export const STAGE_STATUSES = Object.freeze(['ready', 'blocked', 'failed', 'passed']);
5
+
6
+ const stageSet = new Set(STAGES);
7
+ const gateSet = new Set(GATES);
8
+ const evidenceStatusSet = new Set(EVIDENCE_STATUSES);
9
+ const stageStatusSet = new Set(STAGE_STATUSES);
10
+
11
+ function issue(code, message, field) {
12
+ return field ? { code, message, field } : { code, message };
13
+ }
14
+
15
+ function isRecord(value) {
16
+ return value && typeof value === 'object' && !Array.isArray(value);
17
+ }
18
+
19
+ export function validateStageResult(value) {
20
+ const errors = [];
21
+ if (!isRecord(value)) return [issue('stage-result.invalid', 'stage result must be an object')];
22
+ if (value.schemaVersion !== 1) errors.push(issue('stage-result.schema-version', 'schemaVersion must be 1', 'schemaVersion'));
23
+ for (const field of ['runId', 'nextAction']) if (typeof value[field] !== 'string' || !value[field].trim()) errors.push(issue(`stage-result.${field}`, `${field} must be a non-empty string`, field));
24
+ if (!stageSet.has(value.stage)) errors.push(issue('stage-result.stage', `stage must be one of ${STAGES.join(', ')}`, 'stage'));
25
+ if (!stageStatusSet.has(value.status)) errors.push(issue('stage-result.status', `status must be one of ${STAGE_STATUSES.join(', ')}`, 'status'));
26
+ if (value.gate !== null && !gateSet.has(value.gate)) errors.push(issue('stage-result.gate', `gate must be one of ${GATES.join(', ')} or null`, 'gate'));
27
+ if (!Array.isArray(value.evidence)) errors.push(issue('stage-result.evidence', 'evidence must be an array', 'evidence'));
28
+ else for (const [index, evidence] of value.evidence.entries()) {
29
+ if (!isRecord(evidence) || typeof evidence.id !== 'string' || !evidence.id.trim() || !evidenceStatusSet.has(evidence.status)) errors.push(issue('stage-result.evidence-item', `evidence[${index}] needs an id and a supported status`, `evidence[${index}]`));
30
+ }
31
+ for (const field of ['missingEvidence', 'unavailableChecks', 'pendingReview']) if (!Array.isArray(value[field]) || value[field].some((item) => typeof item !== 'string' || !item.trim())) errors.push(issue(`stage-result.${field}`, `${field} must be an array of names`, field));
32
+ if (value.input !== undefined && !isRecord(value.input)) errors.push(issue('stage-result.input', 'input must be an object when present', 'input'));
33
+ if (value.output !== undefined && !isRecord(value.output)) errors.push(issue('stage-result.output', 'output must be an object when present', 'output'));
34
+ return errors;
35
+ }
36
+
37
+ export function isValidStageResult(value) {
38
+ return validateStageResult(value).length === 0;
39
+ }
@@ -0,0 +1,51 @@
1
+ import { MEMORY_SCOPES } from './memory.mjs';
2
+ import { WORKFLOW_STATES } from './workflow.mjs';
3
+
4
+ function isRecord(value) {
5
+ return value && typeof value === 'object' && !Array.isArray(value);
6
+ }
7
+
8
+ function issue(code, message, field) {
9
+ return field ? { code, message, field } : { code, message };
10
+ }
11
+
12
+ export function validateControlPlane(value) {
13
+ const errors = [];
14
+ if (!isRecord(value)) return [issue('control-plane.invalid', 'control plane must be an object')];
15
+ if (value.schemaVersion !== 1) errors.push(issue('control-plane.schema-version', 'schemaVersion must be 1', 'schemaVersion'));
16
+ if (!isRecord(value.package) || typeof value.package.name !== 'string' || typeof value.package.version !== 'string') errors.push(issue('control-plane.package', 'package needs name and version', 'package'));
17
+ for (const field of ['routes', 'gates']) if (!Array.isArray(value[field]) || value[field].some((item) => typeof item !== 'string' || !item.trim())) errors.push(issue(`control-plane.${field}`, `${field} must be an array of names`, field));
18
+ if (!isRecord(value.state) || typeof value.state.provider !== 'string' || !value.state.provider.trim()) errors.push(issue('control-plane.state', 'state.provider is required', 'state'));
19
+ if (value.state?.states !== undefined && (!Array.isArray(value.state.states) || value.state.states.some((state) => !WORKFLOW_STATES.includes(state)))) errors.push(issue('control-plane.state-states', 'state.states must contain known workflow states', 'state.states'));
20
+ if (!isRecord(value.memory) || typeof value.memory.provider !== 'string' || !value.memory.provider.trim()) errors.push(issue('control-plane.memory', 'memory.provider is required', 'memory'));
21
+ if (!Array.isArray(value.memory?.scopes) || value.memory.scopes.length === 0 || value.memory.scopes.some((scope) => !MEMORY_SCOPES.includes(scope))) errors.push(issue('control-plane.memory-scopes', `memory.scopes must contain values from ${MEMORY_SCOPES.join(', ')}`, 'memory.scopes'));
22
+ if (value.memory?.scopes?.includes('shared') && value.memory.shared !== true) errors.push(issue('control-plane.shared-memory', 'shared memory requires memory.shared=true', 'memory.shared'));
23
+ if (!isRecord(value.roles)) errors.push(issue('control-plane.roles', 'roles must be an object', 'roles'));
24
+ else for (const [name, role] of Object.entries(value.roles)) {
25
+ if (typeof role === 'string' && role.trim()) continue;
26
+ if (!isRecord(role)) errors.push(issue('control-plane.role', `role ${name} must be an object or provider reference`, `roles.${name}`));
27
+ else {
28
+ for (const field of ['inputSchema', 'outputSchema']) if (typeof role[field] !== 'string' || !role[field].trim()) errors.push(issue('control-plane.role-schema', `role ${name} needs ${field}`, `roles.${name}.${field}`));
29
+ if (!Array.isArray(role.tools)) errors.push(issue('control-plane.role-tools', `role ${name} needs a tools array`, `roles.${name}.tools`));
30
+ if (!Array.isArray(role.writeScope)) errors.push(issue('control-plane.role-write-scope', `role ${name} needs a writeScope array`, `roles.${name}.writeScope`));
31
+ }
32
+ }
33
+ if (!isRecord(value.adapters)) errors.push(issue('control-plane.adapters', 'adapters must be an object', 'adapters'));
34
+ return errors;
35
+ }
36
+
37
+ export function validateLaunchRequest(request, controlPlane) {
38
+ const errors = [];
39
+ if (!isRecord(request)) return [issue('launch.invalid', 'launch request must be an object')];
40
+ for (const field of ['runId', 'role', 'outputSchema']) if (typeof request[field] !== 'string' || !request[field].trim()) errors.push(issue(`launch.${field}`, `${field} must be a non-empty string`, field));
41
+ const role = controlPlane?.roles?.[request.role];
42
+ if (!role) errors.push(issue('launch.role', `role ${request.role} is not declared`, 'role'));
43
+ if (role && request.outputSchema !== role.outputSchema) errors.push(issue('launch.output-schema', `outputSchema does not match role ${request.role}`, 'outputSchema'));
44
+ if (!Array.isArray(request.tools)) errors.push(issue('launch.tools', 'tools must be an array', 'tools'));
45
+ else if (role) for (const tool of request.tools) if (!role.tools.includes(tool)) errors.push(issue('launch.tool', `tool ${tool} is not allowed for role ${request.role}`, 'tools'));
46
+ if (!Array.isArray(request.writeScope)) errors.push(issue('launch.write-scope', 'writeScope must be an array', 'writeScope'));
47
+ else if (role) for (const path of request.writeScope) if (!role.writeScope.includes(path)) errors.push(issue('launch.write-path', `write path ${path} is not allowed for role ${request.role}`, 'writeScope'));
48
+ if (!isRecord(request.limits)) errors.push(issue('launch.limits', 'limits must be an object', 'limits'));
49
+ if (!Array.isArray(request.stopConditions)) errors.push(issue('launch.stop-conditions', 'stopConditions must be an array', 'stopConditions'));
50
+ return errors;
51
+ }
@@ -0,0 +1,53 @@
1
+ function isRecord(value) {
2
+ return value && typeof value === 'object' && !Array.isArray(value);
3
+ }
4
+
5
+ function issue(code, message, field) {
6
+ return field ? { code, message, field } : { code, message };
7
+ }
8
+
9
+ function orderSlices(slices) {
10
+ const byId = new Map(slices.map((slice) => [slice.id, slice]));
11
+ const visiting = new Set();
12
+ const visited = new Set();
13
+ const ordered = [];
14
+ const errors = [];
15
+ function visit(id) {
16
+ if (visited.has(id)) return;
17
+ if (visiting.has(id)) { errors.push(issue('decompose.cycle', `dependency cycle includes ${id}`, 'slices')); return; }
18
+ visiting.add(id);
19
+ for (const dependency of byId.get(id)?.dependencies ?? []) if (byId.has(dependency)) visit(dependency);
20
+ visiting.delete(id);
21
+ visited.add(id);
22
+ ordered.push(id);
23
+ }
24
+ for (const slice of slices) visit(slice.id);
25
+ return { ordered, errors };
26
+ }
27
+
28
+ export function buildDecompositionPlan(input) {
29
+ const errors = [];
30
+ if (!isRecord(input)) return { schemaVersion: 1, status: 'invalid', valid: false, canApply: false, errors: [issue('decompose.invalid', 'decomposition input must be an object')] };
31
+ if (input.schemaVersion !== 1) errors.push(issue('decompose.schema-version', 'schemaVersion must be 1', 'schemaVersion'));
32
+ if (!isRecord(input.parent) || typeof input.parent.id !== 'string' || !input.parent.id.trim() || typeof input.parent.title !== 'string' || !input.parent.title.trim()) errors.push(issue('decompose.parent', 'parent needs id and title', 'parent'));
33
+ if (!Array.isArray(input.slices) || input.slices.length === 0) errors.push(issue('decompose.slices', 'slices must be a non-empty array', 'slices'));
34
+ const ids = new Set();
35
+ const slices = Array.isArray(input.slices) ? input.slices.map((slice, index) => {
36
+ if (!isRecord(slice) || typeof slice.id !== 'string' || !slice.id.trim() || typeof slice.title !== 'string' || !slice.title.trim()) { errors.push(issue('decompose.slice', `slice ${index} needs id and title`, `slices[${index}]`)); return null; }
37
+ if (ids.has(slice.id)) errors.push(issue('decompose.duplicate', `slice ${slice.id} appears more than once`, `slices[${index}].id`));
38
+ ids.add(slice.id);
39
+ if (!Array.isArray(slice.dependencies)) errors.push(issue('decompose.dependencies', `slice ${slice.id} needs a dependencies array`, `slices[${index}].dependencies`));
40
+ if (!Array.isArray(slice.acceptance) || slice.acceptance.length === 0 || slice.acceptance.some((item) => typeof item !== 'string' || !item.trim())) errors.push(issue('decompose.acceptance', `slice ${slice.id} needs independent acceptance evidence`, `slices[${index}].acceptance`));
41
+ return { id: slice.id, title: slice.title, acceptance: [...(slice.acceptance ?? [])], dependencies: [...(slice.dependencies ?? [])].sort(), scope: [...(slice.scope ?? [])].sort(), labels: [...(slice.labels ?? [])].sort() };
42
+ }).filter(Boolean) : [];
43
+ for (const slice of slices) for (const dependency of slice.dependencies) if (!ids.has(dependency)) errors.push(issue('decompose.unknown-dependency', `slice ${slice.id} depends on unknown slice ${dependency}`, 'slices'));
44
+ const order = orderSlices(slices);
45
+ errors.push(...order.errors);
46
+ const status = errors.length ? 'invalid' : 'ready';
47
+ return { schemaVersion: 1, status, valid: errors.length === 0, canApply: status === 'ready', parent: input.parent ?? null, slices, order: order.ordered, operations: status === 'ready' ? order.ordered.map((id) => ({ type: 'create-child', parentId: input.parent?.id, sliceId: id })) : [], errors };
48
+ }
49
+
50
+ export function applyDecompositionPlan(plan) {
51
+ if (!plan?.canApply) throw new Error('Decomposition plan is not ready to apply. Resolve its errors first.');
52
+ return { ...plan, applied: true };
53
+ }