@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 +9 -1
- package/commands/control-plane.md +3 -0
- package/commands/decompose.md +3 -0
- package/commands/github-decompose.md +3 -0
- package/commands/github-labels.md +3 -0
- package/commands/github-triage.md +3 -0
- package/commands/launch.md +3 -0
- package/commands/memory.md +3 -0
- package/commands/reconcile-labels.md +3 -0
- package/commands/route.md +3 -0
- package/commands/stacked.md +3 -0
- package/commands/stage-result.md +3 -0
- package/commands/state.md +3 -0
- package/contract.json +8 -3
- package/package.json +17 -2
- package/schemas/capabilities.schema.json +27 -0
- package/schemas/control-plane.schema.json +18 -3
- package/schemas/decomposition.schema.json +13 -0
- package/schemas/sdlc-contract.schema.json +7 -2
- package/schemas/stage-result.schema.json +22 -0
- package/schemas/stages.schema.json +27 -0
- package/schemas/workflow-state.schema.json +22 -0
- package/skills/implement/SKILL.md +2 -2
- package/skills/integrate/SKILL.md +2 -2
- package/skills/propose/SKILL.md +2 -2
- package/skills/refine/SKILL.md +2 -2
- package/skills/review/SKILL.md +2 -2
- package/skills/triage/SKILL.md +2 -2
- package/src/contracts.mjs +39 -0
- package/src/control-plane.mjs +51 -0
- package/src/decompose.mjs +53 -0
- package/src/github.mjs +67 -0
- package/src/memory.mjs +22 -0
- package/src/reconcile.mjs +165 -0
- package/src/routes.mjs +22 -0
- package/src/sdlc.mjs +238 -7
- package/src/stacked.mjs +50 -0
- package/src/triage.mjs +2 -2
- package/src/workflow.mjs +66 -0
- package/stages.json +12 -0
package/src/workflow.mjs
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
export const WORKFLOW_STATES = Object.freeze(['ready', 'running', 'waiting-human', 'blocked', 'succeeded', 'failed', 'cancelled']);
|
|
2
|
+
|
|
3
|
+
const transitions = Object.freeze({
|
|
4
|
+
ready: ['running', 'cancelled'],
|
|
5
|
+
running: ['waiting-human', 'blocked', 'succeeded', 'failed', 'cancelled'],
|
|
6
|
+
'waiting-human': ['running', 'blocked', 'cancelled'],
|
|
7
|
+
blocked: ['ready', 'cancelled'],
|
|
8
|
+
failed: ['ready', 'cancelled'],
|
|
9
|
+
succeeded: [],
|
|
10
|
+
cancelled: [],
|
|
11
|
+
});
|
|
12
|
+
const stateSet = new Set(WORKFLOW_STATES);
|
|
13
|
+
|
|
14
|
+
function isRecord(value) {
|
|
15
|
+
return value && typeof value === 'object' && !Array.isArray(value);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function problem(code, message, field) {
|
|
19
|
+
return field ? { code, message, field } : { code, message };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function allowedTransitions(state) {
|
|
23
|
+
return [...(transitions[state] ?? [])];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function validateStateEvent(event, previousState = null) {
|
|
27
|
+
const errors = [];
|
|
28
|
+
if (!isRecord(event)) return [problem('state-event.invalid', 'state event must be an object')];
|
|
29
|
+
if (event.schemaVersion !== 1) errors.push(problem('state-event.schema-version', 'schemaVersion must be 1', 'schemaVersion'));
|
|
30
|
+
for (const field of ['eventId', 'runId', 'route', 'stage', 'actor', 'timestamp', 'reason']) if (typeof event[field] !== 'string' || !event[field].trim()) errors.push(problem(`state-event.${field}`, `${field} must be a non-empty string`, field));
|
|
31
|
+
if (!stateSet.has(event.to)) errors.push(problem('state-event.to', `to must be one of ${WORKFLOW_STATES.join(', ')}`, 'to'));
|
|
32
|
+
if (event.from !== null && !stateSet.has(event.from)) errors.push(problem('state-event.from', `from must be one of ${WORKFLOW_STATES.join(', ')} or null`, 'from'));
|
|
33
|
+
if (previousState !== null && event.from !== previousState) errors.push(problem('state-event.previous', `event from ${event.from} does not match current state ${previousState}`, 'from'));
|
|
34
|
+
if (event.from !== null && !allowedTransitions(event.from).includes(event.to)) errors.push(problem('state-event.transition', `transition ${event.from}->${event.to} is not allowed`, 'to'));
|
|
35
|
+
if (event.from === null && event.to !== 'ready') errors.push(problem('state-event.initial', 'the first event must enter ready', 'to'));
|
|
36
|
+
if (!Array.isArray(event.evidence)) errors.push(problem('state-event.evidence', 'evidence must be an array', 'evidence'));
|
|
37
|
+
return errors;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function replayWorkflow(events) {
|
|
41
|
+
if (!Array.isArray(events)) return { valid: false, errors: [problem('state.events', 'events must be an array')], runs: {} };
|
|
42
|
+
const runs = new Map();
|
|
43
|
+
const errors = [];
|
|
44
|
+
const eventIds = new Set();
|
|
45
|
+
for (const [index, event] of events.entries()) {
|
|
46
|
+
const previous = runs.get(event?.runId)?.state ?? null;
|
|
47
|
+
const eventErrors = validateStateEvent(event, previous);
|
|
48
|
+
if (eventErrors.length) {
|
|
49
|
+
errors.push(...eventErrors.map((error) => ({ ...error, index })));
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
const current = runs.get(event.runId);
|
|
53
|
+
if (eventIds.has(event.eventId)) {
|
|
54
|
+
errors.push(problem('state-event.duplicate', `event ${event.eventId} appears more than once`, `events[${index}]`));
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
eventIds.add(event.eventId);
|
|
58
|
+
runs.set(event.runId, { state: event.to, route: event.route, stage: event.stage, events: [...(current?.events ?? []), event] });
|
|
59
|
+
}
|
|
60
|
+
const result = Object.fromEntries([...runs.entries()].sort(([left], [right]) => left.localeCompare(right)).map(([runId, run]) => [runId, { ...run, events: run.events.map((event) => ({ ...event })) }]));
|
|
61
|
+
return { valid: errors.length === 0, errors, runs: result };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function createStateEvent({ eventId, runId, route, stage, from, to, actor, timestamp, reason, evidence = [], parentRunId }) {
|
|
65
|
+
return { schemaVersion: 1, eventId, runId, ...(parentRunId ? { parentRunId } : {}), route, stage, from, to, actor, timestamp, reason, evidence };
|
|
66
|
+
}
|
package/stages.json
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "./schemas/stages.schema.json",
|
|
3
|
+
"version": 1,
|
|
4
|
+
"stages": [
|
|
5
|
+
{ "id": "triage", "gate": "triaged", "input": "work-item", "output": "triage-report", "mutation": "explicit" },
|
|
6
|
+
{ "id": "refine", "gate": "ready", "input": "triage-report", "output": "ready-change", "mutation": "none" },
|
|
7
|
+
{ "id": "propose", "gate": "planned", "input": "ready-change", "output": "change-artifacts", "mutation": "explicit" },
|
|
8
|
+
{ "id": "implement", "gate": "done", "input": "change-artifacts", "output": "implementation-evidence", "mutation": "workspace" },
|
|
9
|
+
{ "id": "integrate", "gate": "done", "input": "implementation-evidence", "output": "integration-evidence", "mutation": "explicit" },
|
|
10
|
+
{ "id": "review", "gate": null, "input": "integration-evidence", "output": "review-report", "mutation": "explicit" }
|
|
11
|
+
]
|
|
12
|
+
}
|