@smartmemory/compose 0.1.28-beta → 0.1.30-beta
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 +1 -0
- package/bin/compose.js +27 -0
- package/contracts/goal-result.json +128 -0
- package/contracts/judge-result.json +152 -0
- package/contracts/review-result.json +2 -2
- package/contracts/task-result.json +60 -0
- package/contracts/taskgraph-gsd.json +94 -0
- package/dist/assets/App-CB0U2nlm.js +768 -0
- package/dist/assets/arc-Ib57etRP.js +1 -0
- package/dist/assets/architectureDiagram-3BPJPVTR-3N4ul7Lp.js +36 -0
- package/dist/assets/blockDiagram-GPEHLZMM-iPkpmFIG.js +132 -0
- package/dist/assets/{c4Diagram-AHTNJAMY-CqzLpnSp.js → c4Diagram-AAUBKEIU-BCKTL1Dk.js} +3 -3
- package/dist/assets/channel-Bd09RsPV.js +1 -0
- package/dist/assets/chunk-2J33WTMH-Daqe8HpE.js +1 -0
- package/dist/assets/{chunk-4BX2VUAB-D27n9FGy.js → chunk-4BX2VUAB-BjnNKI2i.js} +1 -1
- package/dist/assets/chunk-55IACEB6-DRsniyrS.js +1 -0
- package/dist/assets/chunk-727SXJPM-BiShnVey.js +206 -0
- package/dist/assets/chunk-AQP2D5EJ-Dd9CkPhT.js +231 -0
- package/dist/assets/{chunk-FMBD7UC4-B-C0Qn-h.js → chunk-FMBD7UC4-CrED-oPn.js} +1 -1
- package/dist/assets/chunk-ND2GUHAM-dk4lChMO.js +1 -0
- package/dist/assets/{chunk-QZHKN3VN-DV2v0Qii.js → chunk-QZHKN3VN-a1SBIOVJ.js} +1 -1
- package/dist/assets/classDiagram-4FO5ZUOK-D_vAH5lP.js +1 -0
- package/dist/assets/classDiagram-v2-Q7XG4LA2-D_vAH5lP.js +1 -0
- package/dist/assets/cose-bilkent-S5V4N54A-CXtbzn7R.js +1 -0
- package/dist/assets/dagre-BM42HDAG-By5-LlQC.js +4 -0
- package/dist/assets/{defaultLocale-DX6XiGOO.js → defaultLocale-CrowFXzY.js} +1 -1
- package/dist/assets/diagram-2AECGRRQ-BWFSnxuX.js +43 -0
- package/dist/assets/diagram-5GNKFQAL-DtyjjvQp.js +10 -0
- package/dist/assets/diagram-KO2AKTUF-D2Y15IJ0.js +3 -0
- package/dist/assets/diagram-LMA3HP47-CE-THjCD.js +24 -0
- package/dist/assets/diagram-OG6HWLK6-Blju-noD.js +24 -0
- package/dist/assets/{erDiagram-SMLLAGMA-B1zsRPqn.js → erDiagram-TEJ5UH35-DI6ayS27.js} +1 -1
- package/dist/assets/{flowDiagram-DWJPFMVM-AvlZ6pTE.js → flowDiagram-I6XJVG4X-BQf0uOzY.js} +4 -4
- package/dist/assets/ganttDiagram-6RSMTGT7-_upG_Ajl.js +292 -0
- package/dist/assets/gitGraphDiagram-PVQCEYII-5sVMQhO3.js +106 -0
- package/dist/assets/graph-BEmEBUp_.js +1 -0
- package/dist/assets/{graph-CfEl_ohV.js → graph-DPbJeZyN.js} +2 -2
- package/dist/assets/{index-CF7jc-By.js → index-DLF-s4tP.js} +3 -3
- package/dist/assets/infoDiagram-5YYISTIA-4Hpw9ky_.js +2 -0
- package/dist/assets/{ishikawaDiagram-UXIWVN3A-COnZHJuM.js → ishikawaDiagram-YF4QCWOH-CuHQNWRf.js} +5 -5
- package/dist/assets/{journeyDiagram-VCZTEJTY-Bsssj2jr.js → journeyDiagram-JHISSGLW-PFwOct4G.js} +1 -1
- package/dist/assets/{kanban-definition-6JOO6SKY-1o5Em0Ia.js → kanban-definition-UN3LZRKU-M70jtg2M.js} +7 -7
- package/dist/assets/katex-CQk2-UhE.js +257 -0
- package/dist/assets/layout-Bw0msyOw.js +1 -0
- package/dist/assets/linear-BkYLp90p.js +1 -0
- package/dist/assets/{mindmap-definition-QFDTVHPH-79V6zmXV.js → mindmap-definition-RKZ34NQL-1KQv07E1.js} +10 -10
- package/dist/assets/{mobile-BOyZ87uL.js → mobile-CsuriFuT.js} +3 -3
- package/dist/assets/pieDiagram-4H26LBE5-B8lh9yMA.js +30 -0
- package/dist/assets/quadrantDiagram-W4KKPZXB-DUPV_-qJ.js +7 -0
- package/dist/assets/{requirementDiagram-MS252O5E-9YEiDjlT.js → requirementDiagram-4Y6WPE33-ChYWhFld.js} +3 -3
- package/dist/assets/sankeyDiagram-5OEKKPKP-HsjvNkHR.js +40 -0
- package/dist/assets/sequenceDiagram-3UESZ5HK-CR5iTMTw.js +162 -0
- package/dist/assets/stateDiagram-AJRCARHV-BwMjQVHC.js +1 -0
- package/dist/assets/stateDiagram-v2-BHNVJYJU-X1hOVHVI.js +1 -0
- package/dist/assets/{timeline-definition-GMOUNBTQ-DGpUOjs3.js → timeline-definition-PNZ67QCA-BI88FufN.js} +3 -3
- package/dist/assets/vennDiagram-CIIHVFJN-BKg5ydB8.js +34 -0
- package/dist/assets/wardley-L42UT6IY-CtYx0E7z.js +173 -0
- package/dist/assets/wardleyDiagram-YWT4CUSO-CAColVNQ.js +78 -0
- package/dist/assets/{xychartDiagram-5P7HB3ND-C6hwimqo.js → xychartDiagram-2RQKCTM6-CJqak-xY.js} +4 -4
- package/dist/index.html +2 -2
- package/lib/gsd-blackboard.js +135 -0
- package/lib/gsd-decompose-enrich.js +171 -0
- package/lib/gsd-prompt.js +82 -0
- package/lib/gsd.js +364 -0
- package/package.json +1 -1
- package/pipelines/gsd.stratum.yaml +141 -0
- package/dist/assets/App-Dj7XWWxC.js +0 -768
- package/dist/assets/_baseUniq-tNOA7dYy.js +0 -1
- package/dist/assets/arc-BAmAJ19S.js +0 -1
- package/dist/assets/architectureDiagram-Q4EWVU46-BPWGVKHW.js +0 -36
- package/dist/assets/blockDiagram-DXYQGD6D-CVlFbWKF.js +0 -132
- package/dist/assets/channel-Ddcaj0fR.js +0 -1
- package/dist/assets/chunk-4TB4RGXK-BNXf8s1x.js +0 -206
- package/dist/assets/chunk-55IACEB6-kGd4Gwx6.js +0 -1
- package/dist/assets/chunk-EDXVE4YY-Ci9gWeIv.js +0 -1
- package/dist/assets/chunk-OYMX7WX6-CosYsxuv.js +0 -231
- package/dist/assets/chunk-YZCP3GAM-BlgKQRCn.js +0 -1
- package/dist/assets/classDiagram-6PBFFD2Q-DfGxrNIN.js +0 -1
- package/dist/assets/classDiagram-v2-HSJHXN6E-DfGxrNIN.js +0 -1
- package/dist/assets/clone-DVujR_lO.js +0 -1
- package/dist/assets/cose-bilkent-S5V4N54A-CSkzhGHO.js +0 -1
- package/dist/assets/dagre-KV5264BT-zp76534d.js +0 -4
- package/dist/assets/diagram-5BDNPKRD-CAsORZBT.js +0 -10
- package/dist/assets/diagram-G4DWMVQ6-Da2z6fvR.js +0 -24
- package/dist/assets/diagram-MMDJMWI5-R9NZEWPF.js +0 -43
- package/dist/assets/diagram-TYMM5635-DXabRna8.js +0 -24
- package/dist/assets/ganttDiagram-T4ZO3ILL-Bnj-jTcM.js +0 -292
- package/dist/assets/gitGraphDiagram-UUTBAWPF-82ysfuqG.js +0 -106
- package/dist/assets/graph-6nRhlKgL.js +0 -1
- package/dist/assets/infoDiagram-42DDH7IO-DSGEEGYr.js +0 -2
- package/dist/assets/katex-DkKDou_j.js +0 -257
- package/dist/assets/layout-C6Mitjz_.js +0 -1
- package/dist/assets/linear-DoGSpDWQ.js +0 -1
- package/dist/assets/min-BqH4I4oK.js +0 -1
- package/dist/assets/pieDiagram-DEJITSTG-nXz4HiT6.js +0 -30
- package/dist/assets/quadrantDiagram-34T5L4WZ-BuwKLcii.js +0 -7
- package/dist/assets/sankeyDiagram-XADWPNL6-gX8lhWn5.js +0 -10
- package/dist/assets/sequenceDiagram-FGHM5R23-CkssrD67.js +0 -157
- package/dist/assets/stateDiagram-FHFEXIEX-DPmEy2eV.js +0 -1
- package/dist/assets/stateDiagram-v2-QKLJ7IA2-BGZzYkLq.js +0 -1
- package/dist/assets/vennDiagram-DHZGUBPP-CIvkd661.js +0 -34
- package/dist/assets/wardley-RL74JXVD-BqRTpa3K.js +0 -162
- package/dist/assets/wardleyDiagram-NUSXRM2D-B93hOd7R.js +0 -20
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
// lib/gsd-decompose-enrich.js
|
|
2
|
+
//
|
|
3
|
+
// COMP-GSD-2 T3: enrichTaskGraph(taskGraph, blueprintText) → TaskGraphGsd.
|
|
4
|
+
//
|
|
5
|
+
// Pure function. No filesystem I/O. Takes the bare TaskGraph emitted by
|
|
6
|
+
// Stratum's decompose step (tasks have id/files_owned/files_read/depends_on/
|
|
7
|
+
// description) and the blueprint text. Calls parseBoundaryMap (pure) to get
|
|
8
|
+
// slices and parse violations. Maps each slice to exactly one task by
|
|
9
|
+
// the slice's File Plan files ⊆ task.files_owned. Attaches produces/consumes
|
|
10
|
+
// arrays to each task.
|
|
11
|
+
//
|
|
12
|
+
// Purity note: the blueprint says T6/runGsd should use validateBoundaryMap
|
|
13
|
+
// (which does filesystem checks). T3 only uses parseBoundaryMap so it stays
|
|
14
|
+
// pure and deterministic — runGsd separately calls validateBoundaryMap as
|
|
15
|
+
// its lifecycle precondition.
|
|
16
|
+
//
|
|
17
|
+
// Throws on: parseBoundaryMap parseViolations, empty Boundary Map, orphaned
|
|
18
|
+
// slice (slice's File Plan files not all owned by any single task), orphaned
|
|
19
|
+
// task (no slice maps to it).
|
|
20
|
+
|
|
21
|
+
import { parseBoundaryMap } from './boundary-map.js';
|
|
22
|
+
|
|
23
|
+
// Match per-slice File Plan lines:
|
|
24
|
+
// File Plan: `path/a` (new), `path/b` (modify), ...
|
|
25
|
+
// File Plan: path/a, path/b
|
|
26
|
+
const FILEPLAN_LINE_RE = /^File Plan\s*:\s*(.+)$/;
|
|
27
|
+
const BACKTICK_PATH_RE = /`([^`]+)`/g;
|
|
28
|
+
const BARE_PATH_RE = /([^\s,()`]+)/g;
|
|
29
|
+
|
|
30
|
+
function extractSliceFilePlanFiles(blueprintText, sliceId) {
|
|
31
|
+
// Find the slice block: from `### {sliceId}` to the next `### S` heading or
|
|
32
|
+
// a `## ` heading or EOF.
|
|
33
|
+
const lines = blueprintText.split(/\r?\n/);
|
|
34
|
+
const headingRe = new RegExp(`^### ${sliceId}(?::|\\s|$)`);
|
|
35
|
+
let start = -1;
|
|
36
|
+
for (let i = 0; i < lines.length; i++) {
|
|
37
|
+
if (headingRe.test(lines[i])) { start = i + 1; break; }
|
|
38
|
+
}
|
|
39
|
+
if (start === -1) return [];
|
|
40
|
+
let end = lines.length;
|
|
41
|
+
for (let i = start; i < lines.length; i++) {
|
|
42
|
+
if (/^### S\d/.test(lines[i]) || /^## /.test(lines[i])) { end = i; break; }
|
|
43
|
+
}
|
|
44
|
+
// Find File Plan line within the slice block
|
|
45
|
+
for (let i = start; i < end; i++) {
|
|
46
|
+
const m = lines[i].match(FILEPLAN_LINE_RE);
|
|
47
|
+
if (!m) continue;
|
|
48
|
+
const tail = m[1];
|
|
49
|
+
// Prefer backtick-quoted paths; fall back to bare comma-separated if none.
|
|
50
|
+
const ticked = [...tail.matchAll(BACKTICK_PATH_RE)].map((mm) => mm[1].trim());
|
|
51
|
+
if (ticked.length > 0) return ticked;
|
|
52
|
+
// Strip parenthesized actions like " (new)" before bare-token matching.
|
|
53
|
+
const cleaned = tail.replace(/\([^)]*\)/g, '');
|
|
54
|
+
const bare = [...cleaned.matchAll(BARE_PATH_RE)].map((mm) => mm[1].trim()).filter(Boolean);
|
|
55
|
+
return bare;
|
|
56
|
+
}
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function enrichTaskGraph(taskGraph, blueprintText) {
|
|
61
|
+
if (!taskGraph || !Array.isArray(taskGraph.tasks)) {
|
|
62
|
+
throw new Error('enrichTaskGraph: taskGraph.tasks must be an array');
|
|
63
|
+
}
|
|
64
|
+
if (typeof blueprintText !== 'string') {
|
|
65
|
+
throw new Error('enrichTaskGraph: blueprintText must be a string');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const { slices, parseViolations } = parseBoundaryMap(blueprintText);
|
|
69
|
+
if (parseViolations.length > 0) {
|
|
70
|
+
const summary = parseViolations
|
|
71
|
+
.slice(0, 5)
|
|
72
|
+
.map((v) => `${v.kind}: ${v.message}`)
|
|
73
|
+
.join('; ');
|
|
74
|
+
throw new Error(`enrichTaskGraph: Boundary Map invalid (parse violations): ${summary}`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const liveSlices = slices.filter((s) => !s._duplicate);
|
|
78
|
+
if (liveSlices.length === 0) {
|
|
79
|
+
throw new Error('enrichTaskGraph: Boundary Map empty (zero slices)');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// For each slice, extract its File Plan files and map to a task whose
|
|
83
|
+
// files_owned ⊇ those files. If the slice's File Plan files are split
|
|
84
|
+
// across multiple tasks → ambiguous → throw. If no task owns all of
|
|
85
|
+
// them → orphaned slice.
|
|
86
|
+
const sliceToTask = new Map();
|
|
87
|
+
const sliceFilePlanFiles = new Map(); // sliceId → string[]
|
|
88
|
+
for (const slice of liveSlices) {
|
|
89
|
+
const filePlanFiles = extractSliceFilePlanFiles(blueprintText, slice.id);
|
|
90
|
+
if (filePlanFiles.length === 0) {
|
|
91
|
+
throw new Error(
|
|
92
|
+
`enrichTaskGraph: slice ${slice.id} has no File Plan entries. ` +
|
|
93
|
+
`Each slice must declare its File Plan files for slice→task mapping.`,
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
sliceFilePlanFiles.set(slice.id, filePlanFiles);
|
|
97
|
+
const fpSet = new Set(filePlanFiles);
|
|
98
|
+
let matchedTaskId = null;
|
|
99
|
+
let partialMatchTaskIds = [];
|
|
100
|
+
for (const task of taskGraph.tasks) {
|
|
101
|
+
const owned = new Set(task.files_owned || []);
|
|
102
|
+
const allOwned = [...fpSet].every((f) => owned.has(f));
|
|
103
|
+
const someOwned = [...fpSet].some((f) => owned.has(f));
|
|
104
|
+
if (allOwned) {
|
|
105
|
+
if (matchedTaskId) {
|
|
106
|
+
throw new Error(
|
|
107
|
+
`enrichTaskGraph: slice ${slice.id} matches multiple tasks ` +
|
|
108
|
+
`(${matchedTaskId}, ${task.id}); decomposition has overlapping files_owned`,
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
matchedTaskId = task.id;
|
|
112
|
+
} else if (someOwned) {
|
|
113
|
+
partialMatchTaskIds.push(task.id);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (!matchedTaskId) {
|
|
117
|
+
if (partialMatchTaskIds.length > 0) {
|
|
118
|
+
throw new Error(
|
|
119
|
+
`enrichTaskGraph: slice ${slice.id} File Plan files ` +
|
|
120
|
+
`[${[...fpSet].join(', ')}] are split across multiple tasks ` +
|
|
121
|
+
`(partial owners: ${partialMatchTaskIds.join(', ')}). ` +
|
|
122
|
+
`Each slice must map to exactly one task.`,
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
throw new Error(
|
|
126
|
+
`enrichTaskGraph: slice ${slice.id} is orphaned — no task owns its File Plan files [${[...fpSet].join(', ')}]`,
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
sliceToTask.set(slice.id, matchedTaskId);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Reverse map: each task must have ≥1 matching slice.
|
|
133
|
+
const taskToSlices = new Map();
|
|
134
|
+
for (const [sliceId, taskId] of sliceToTask.entries()) {
|
|
135
|
+
if (!taskToSlices.has(taskId)) taskToSlices.set(taskId, []);
|
|
136
|
+
taskToSlices.get(taskId).push(sliceId);
|
|
137
|
+
}
|
|
138
|
+
const orphanedTasks = taskGraph.tasks
|
|
139
|
+
.filter((t) => !taskToSlices.has(t.id))
|
|
140
|
+
.map((t) => t.id);
|
|
141
|
+
if (orphanedTasks.length > 0) {
|
|
142
|
+
throw new Error(
|
|
143
|
+
`enrichTaskGraph: tasks have no matching Boundary Map slice (orphaned): ${orphanedTasks.join(', ')}`,
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Build the enriched TaskGraph. For each task, concatenate produces/consumes
|
|
148
|
+
// from all matching slices.
|
|
149
|
+
const slicesById = new Map(liveSlices.map((s) => [s.id, s]));
|
|
150
|
+
const enrichedTasks = taskGraph.tasks.map((task) => {
|
|
151
|
+
const matchedSliceIds = taskToSlices.get(task.id) || [];
|
|
152
|
+
const produces = [];
|
|
153
|
+
const consumes = [];
|
|
154
|
+
for (const sid of matchedSliceIds) {
|
|
155
|
+
const s = slicesById.get(sid);
|
|
156
|
+
for (const p of s.produces) {
|
|
157
|
+
produces.push({ file: p.file, symbols: p.symbols, kind: p.kind });
|
|
158
|
+
}
|
|
159
|
+
for (const c of s.consumes) {
|
|
160
|
+
consumes.push({ from: c.from, file: c.file, symbols: c.symbols });
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return {
|
|
164
|
+
...task,
|
|
165
|
+
produces,
|
|
166
|
+
consumes,
|
|
167
|
+
};
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
return { tasks: enrichedTasks };
|
|
171
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// lib/gsd-prompt.js
|
|
2
|
+
//
|
|
3
|
+
// COMP-GSD-2 T4: buildTaskDescription — pure string assembly.
|
|
4
|
+
//
|
|
5
|
+
// Stratum's parallel_dispatch only interpolates {task.id|description|
|
|
6
|
+
// files_owned|files_read|depends_on|index} and {input.<field>}. Every other
|
|
7
|
+
// piece of context — the per-slice produces/consumes contract, the upstream
|
|
8
|
+
// tasks summary, the gate commands — must be packed inside `task.description`.
|
|
9
|
+
//
|
|
10
|
+
// This module produces the rich `description` string. It is also used by
|
|
11
|
+
// runGsd as a deterministic fallback when the decompose_gsd agent failed to
|
|
12
|
+
// bake a description into a task.
|
|
13
|
+
//
|
|
14
|
+
// Pure function. No fs, no globals.
|
|
15
|
+
|
|
16
|
+
const SECTION_DIVIDER = '---';
|
|
17
|
+
|
|
18
|
+
function formatProduces(produces) {
|
|
19
|
+
if (!produces || produces.length === 0) return '(none)';
|
|
20
|
+
const lines = produces.map((p) => {
|
|
21
|
+
const syms = (p.symbols || []).join(', ');
|
|
22
|
+
return ` ${p.file} → ${syms} (${p.kind})`;
|
|
23
|
+
});
|
|
24
|
+
return lines.join('\n');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function formatConsumes(consumes) {
|
|
28
|
+
if (!consumes || consumes.length === 0) return '(none)';
|
|
29
|
+
const lines = consumes.map((c) => {
|
|
30
|
+
const syms = (c.symbols || []).join(', ');
|
|
31
|
+
return ` from ${c.from}: ${c.file} → ${syms}`;
|
|
32
|
+
});
|
|
33
|
+
return lines.join('\n');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function formatUpstream(task, upstreamTasks) {
|
|
37
|
+
const deps = new Set(task.depends_on || []);
|
|
38
|
+
const filtered = (upstreamTasks || []).filter((t) => deps.has(t.id));
|
|
39
|
+
if (filtered.length === 0) return '(none)';
|
|
40
|
+
return filtered
|
|
41
|
+
.map((t) => {
|
|
42
|
+
const producesStr = (t.produces || [])
|
|
43
|
+
.map((p) => `${p.file} → ${(p.symbols || []).join(', ')} (${p.kind})`)
|
|
44
|
+
.join('; ');
|
|
45
|
+
return ` ${t.id}: produces ${producesStr || '(none)'}`;
|
|
46
|
+
})
|
|
47
|
+
.join('\n');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function formatGates(gateCommands) {
|
|
51
|
+
if (!gateCommands || gateCommands.length === 0) return '(none)';
|
|
52
|
+
return gateCommands.map((c) => ` - ${c}`).join('\n');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function buildTaskDescription({ task, slice, upstreamTasks, gateCommands }) {
|
|
56
|
+
if (!task || typeof task !== 'object') {
|
|
57
|
+
throw new Error('buildTaskDescription: task object required');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// The function PRODUCES task.description — it must not embed any preexisting
|
|
61
|
+
// description (which may itself be malformed; this is the repair path). The
|
|
62
|
+
// output is the fresh, canonical description.
|
|
63
|
+
const lines = [];
|
|
64
|
+
lines.push('Symbols you must produce:');
|
|
65
|
+
lines.push(formatProduces(task.produces));
|
|
66
|
+
lines.push('');
|
|
67
|
+
lines.push('Symbols you may consume from upstream tasks:');
|
|
68
|
+
lines.push(formatConsumes(task.consumes));
|
|
69
|
+
lines.push('');
|
|
70
|
+
lines.push('Boundary Map slice (the contract for this task):');
|
|
71
|
+
lines.push(typeof slice === 'string' ? slice : '(slice text not provided)');
|
|
72
|
+
lines.push('');
|
|
73
|
+
lines.push('Upstream tasks (spec-level summary; their code lands at end-of-step merge):');
|
|
74
|
+
lines.push(formatUpstream(task, upstreamTasks));
|
|
75
|
+
lines.push('');
|
|
76
|
+
lines.push('GATES — you MUST run each command and they MUST pass before declaring done:');
|
|
77
|
+
lines.push(formatGates(gateCommands));
|
|
78
|
+
lines.push('');
|
|
79
|
+
lines.push('Fix and re-run within this invocation. Do NOT declare done while gates are red.');
|
|
80
|
+
|
|
81
|
+
return lines.join('\n');
|
|
82
|
+
}
|
package/lib/gsd.js
ADDED
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
// lib/gsd.js
|
|
2
|
+
//
|
|
3
|
+
// COMP-GSD-2 T6: runGsd lifecycle entry — `compose gsd <featureCode>`.
|
|
4
|
+
//
|
|
5
|
+
// Self-contained status loop. Does NOT modify lib/build.js. Reuses primitives:
|
|
6
|
+
// - StratumMcpClient (lib/stratum-mcp-client.js) for plan/stepDone/runAgentText
|
|
7
|
+
// - executeParallelDispatchServer (lib/build.js) for the execute step
|
|
8
|
+
// - validateBoundaryMap (lib/boundary-map.js) for precondition check
|
|
9
|
+
// - enrichTaskGraph (lib/gsd-decompose-enrich.js) for decompose validation
|
|
10
|
+
// - buildTaskDescription (lib/gsd-prompt.js) for description repair fallback
|
|
11
|
+
// - gsd-blackboard.writeAll for post-step finalization
|
|
12
|
+
//
|
|
13
|
+
// V1 limitation: runtime task-to-task handoff is not implemented; tasks see
|
|
14
|
+
// only spec-level upstream context (Boundary Map declarations) per blueprint.
|
|
15
|
+
|
|
16
|
+
import { readFileSync, existsSync, readdirSync } from 'node:fs';
|
|
17
|
+
import { join, resolve, dirname } from 'node:path';
|
|
18
|
+
import { fileURLToPath } from 'node:url';
|
|
19
|
+
import { execSync } from 'node:child_process';
|
|
20
|
+
|
|
21
|
+
import { StratumMcpClient } from './stratum-mcp-client.js';
|
|
22
|
+
import { validateBoundaryMap } from './boundary-map.js';
|
|
23
|
+
import { enrichTaskGraph } from './gsd-decompose-enrich.js';
|
|
24
|
+
import { buildTaskDescription } from './gsd-prompt.js';
|
|
25
|
+
import { writeAll, validate as validateTaskResult } from './gsd-blackboard.js';
|
|
26
|
+
import { executeParallelDispatchServer, executeShipStep } from './build.js';
|
|
27
|
+
|
|
28
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
29
|
+
const PACKAGE_ROOT = resolve(__dirname, '..');
|
|
30
|
+
|
|
31
|
+
const DEFAULT_GATE_COMMANDS = ['pnpm lint', 'pnpm build', 'pnpm test'];
|
|
32
|
+
|
|
33
|
+
// ---------- Public API ----------
|
|
34
|
+
|
|
35
|
+
export async function runGsd(featureCode, opts = {}) {
|
|
36
|
+
if (!featureCode || typeof featureCode !== 'string') {
|
|
37
|
+
throw new Error('runGsd: featureCode required');
|
|
38
|
+
}
|
|
39
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
40
|
+
|
|
41
|
+
// 1. Validate preconditions: blueprint exists + Boundary Map ok
|
|
42
|
+
const blueprintPath = join(cwd, 'docs', 'features', featureCode, 'blueprint.md');
|
|
43
|
+
if (!existsSync(blueprintPath)) {
|
|
44
|
+
throw new Error(
|
|
45
|
+
`runGsd: blueprint missing at ${blueprintPath}. ` +
|
|
46
|
+
`Run \`compose build ${featureCode}\` to generate it, or author it by hand.`,
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
const blueprintText = readFileSync(blueprintPath, 'utf-8');
|
|
50
|
+
const bmResult = validateBoundaryMap({
|
|
51
|
+
blueprintText,
|
|
52
|
+
blueprintPath,
|
|
53
|
+
repoRoot: cwd,
|
|
54
|
+
});
|
|
55
|
+
if (!bmResult.ok) {
|
|
56
|
+
const summary = bmResult.violations
|
|
57
|
+
.slice(0, 5)
|
|
58
|
+
.map((v) => `${v.kind}: ${v.message}`)
|
|
59
|
+
.join('\n - ');
|
|
60
|
+
throw new Error(
|
|
61
|
+
`runGsd: Boundary Map invalid in ${blueprintPath}:\n - ${summary}`,
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// 2. Refuse to start in a dirty workspace BEFORE any Stratum side effects.
|
|
66
|
+
// v1 rationale: alternatives (baseline subtract + post-execute delta) drop
|
|
67
|
+
// legitimate edits to pre-existing dirty files. Refuse-if-dirty makes
|
|
68
|
+
// post-execute dirty set unambiguous: every entry is GSD-produced.
|
|
69
|
+
if (!opts.allowDirtyWorkspace) {
|
|
70
|
+
const startingDirty = collectChangedFiles(cwd);
|
|
71
|
+
if (startingDirty.length > 0) {
|
|
72
|
+
throw new Error(
|
|
73
|
+
`runGsd: working tree must be clean to ensure ship_gsd stages only GSD-produced changes. ` +
|
|
74
|
+
`Dirty files: ${startingDirty.slice(0, 5).join(', ')}${startingDirty.length > 5 ? `, +${startingDirty.length - 5} more` : ''}. ` +
|
|
75
|
+
`Commit or stash and re-run, or pass {allowDirtyWorkspace: true} (advanced; risks staging unrelated edits).`,
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// 3. Resolve gateCommands. loadProjectConfig() does not merge defaults, so
|
|
81
|
+
// explicit fallback here.
|
|
82
|
+
const gateCommands = resolveGateCommands(cwd, opts.gateCommands);
|
|
83
|
+
|
|
84
|
+
// 4. Load pipeline spec
|
|
85
|
+
const specPath = join(PACKAGE_ROOT, 'pipelines', 'gsd.stratum.yaml');
|
|
86
|
+
const specYaml = readFileSync(specPath, 'utf-8');
|
|
87
|
+
|
|
88
|
+
// 5. Connect Stratum + plan (only after preconditions pass)
|
|
89
|
+
const stratum = opts.stratum ?? new StratumMcpClient();
|
|
90
|
+
const ownsStratum = !opts.stratum;
|
|
91
|
+
if (ownsStratum) await stratum.connect();
|
|
92
|
+
try {
|
|
93
|
+
let response = await stratum.plan(specYaml, 'gsd', {
|
|
94
|
+
featureCode,
|
|
95
|
+
gateCommands,
|
|
96
|
+
});
|
|
97
|
+
const flowId = response.flow_id;
|
|
98
|
+
|
|
99
|
+
// Track files merged into the base cwd by the execute step so ship_gsd
|
|
100
|
+
// can stage them. executeShipStep's default filter only stages feature
|
|
101
|
+
// docs unless context.filesChanged is provided.
|
|
102
|
+
const stepCtx = {
|
|
103
|
+
stratum, cwd, featureCode, blueprintText, gateCommands,
|
|
104
|
+
filesChanged: [],
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
// 5. Status loop
|
|
108
|
+
while (response.status !== 'complete' && response.status !== 'killed') {
|
|
109
|
+
response = await runOneStep(response, stepCtx);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// 6. Post-step blackboard finalization — read each task's TaskResult JSON
|
|
113
|
+
// and write the consolidated blackboard.
|
|
114
|
+
const blackboard = collectBlackboard(cwd, featureCode);
|
|
115
|
+
if (Object.keys(blackboard).length > 0) {
|
|
116
|
+
await writeAll(featureCode, blackboard, { cwd });
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
status: response.status,
|
|
121
|
+
flowId,
|
|
122
|
+
blackboardEntries: Object.keys(blackboard).length,
|
|
123
|
+
};
|
|
124
|
+
} finally {
|
|
125
|
+
if (ownsStratum) {
|
|
126
|
+
try { await stratum.disconnect?.(); } catch { /* best-effort */ }
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ---------- Internals ----------
|
|
132
|
+
|
|
133
|
+
export function resolveGateCommands(cwd, override) {
|
|
134
|
+
if (Array.isArray(override) && override.length > 0) return override;
|
|
135
|
+
// loadProjectConfig() returns raw .compose/compose.json — does NOT merge
|
|
136
|
+
// defaults — so we must do our own fallback.
|
|
137
|
+
const configPath = join(cwd, '.compose', 'compose.json');
|
|
138
|
+
if (existsSync(configPath)) {
|
|
139
|
+
try {
|
|
140
|
+
const cfg = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
141
|
+
if (Array.isArray(cfg.gateCommands) && cfg.gateCommands.length > 0) {
|
|
142
|
+
return cfg.gateCommands;
|
|
143
|
+
}
|
|
144
|
+
} catch {
|
|
145
|
+
/* fall through to default */
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return [...DEFAULT_GATE_COMMANDS];
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function runOneStep(response, ctx) {
|
|
152
|
+
const { stratum, cwd, featureCode, blueprintText, gateCommands } = ctx;
|
|
153
|
+
const flowId = response.flow_id;
|
|
154
|
+
const stepId = response.step_id;
|
|
155
|
+
const stepType = response.type ?? response.step_type;
|
|
156
|
+
|
|
157
|
+
if (response.status === 'execute_step') {
|
|
158
|
+
// parallel_dispatch step (the `execute` step)
|
|
159
|
+
if (stepType === 'parallel_dispatch' || response.tasks) {
|
|
160
|
+
const outcome = await executeParallelDispatchServer(
|
|
161
|
+
response,
|
|
162
|
+
stratum,
|
|
163
|
+
{ cwd, featureCode },
|
|
164
|
+
null, // progress
|
|
165
|
+
{ write: () => {} }, // streamWriter — no-op for v1
|
|
166
|
+
cwd,
|
|
167
|
+
);
|
|
168
|
+
// After diffs are merged, capture the touched files for ship_gsd
|
|
169
|
+
// staging. The clean-workspace precondition above guarantees every
|
|
170
|
+
// file in the post-execute dirty set is genuinely a GSD-produced change.
|
|
171
|
+
ctx.filesChanged = collectChangedFiles(cwd);
|
|
172
|
+
// executeParallelDispatchServer returns the next-step dispatch envelope
|
|
173
|
+
return outcome;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ship_gsd: delegate to executeShipStep with filesChanged from execute step
|
|
177
|
+
// so source files are staged. Agent sandbox blocks git, so commit must
|
|
178
|
+
// run in-process (mirrors runBuild's special case at lib/build.js:963-981).
|
|
179
|
+
if (stepId === 'ship_gsd') {
|
|
180
|
+
const shipResult = await executeShipStep(
|
|
181
|
+
featureCode,
|
|
182
|
+
cwd,
|
|
183
|
+
cwd,
|
|
184
|
+
{ cwd, featureCode, mode: 'feature', filesChanged: ctx.filesChanged ?? [] },
|
|
185
|
+
'',
|
|
186
|
+
null,
|
|
187
|
+
);
|
|
188
|
+
// executeShipStep stages + commits but does NOT push. Push is a
|
|
189
|
+
// user-facing operation deferred to the user in v1; runBuild's ship
|
|
190
|
+
// step doesn't auto-push either. Document via ship intent later.
|
|
191
|
+
return await stratum.stepDone(flowId, stepId, shipResult);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Single-agent step: dispatch via runAgentText. The agent returns text;
|
|
195
|
+
// we expect JSON matching the step's output_contract.
|
|
196
|
+
const prompt = response.intent ?? '';
|
|
197
|
+
const text = await stratum.runAgentText(response.agent ?? 'claude', prompt, { cwd });
|
|
198
|
+
let result;
|
|
199
|
+
try {
|
|
200
|
+
result = parseJsonFromText(text);
|
|
201
|
+
} catch (err) {
|
|
202
|
+
throw new Error(
|
|
203
|
+
`runGsd: step ${stepId} agent did not return parseable JSON: ${err.message}`,
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// T6 step 7: validate decompose_gsd output and repair missing descriptions.
|
|
208
|
+
if (stepId === 'decompose_gsd') {
|
|
209
|
+
result = validateAndRepairTaskGraph(result, blueprintText, gateCommands);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return await stratum.stepDone(flowId, stepId, result);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (response.status === 'await_gate') {
|
|
216
|
+
// GSD has no gates in v1. If we hit one, surface it.
|
|
217
|
+
throw new Error(
|
|
218
|
+
`runGsd: unexpected gate at step ${stepId}. v1 has no gates in the gsd flow.`,
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
throw new Error(`runGsd: unknown response status: ${response.status}`);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export function validateAndRepairTaskGraph(taskGraph, blueprintText, gateCommands) {
|
|
226
|
+
// Structural check via enrichTaskGraph. Throws on orphan slice/task —
|
|
227
|
+
// that's a "fail loudly" case (no reliable repair path).
|
|
228
|
+
const enriched = enrichTaskGraph(taskGraph, blueprintText);
|
|
229
|
+
|
|
230
|
+
// Per-task description check. The agent must produce a description with
|
|
231
|
+
// all six required sections (per T4 prompt contract). If ANY section
|
|
232
|
+
// marker is missing, repair via buildTaskDescription. Length-only would
|
|
233
|
+
// miss long-but-malformed strings.
|
|
234
|
+
const enrichedById = new Map(enriched.tasks.map((t) => [t.id, t]));
|
|
235
|
+
const repairedTasks = enriched.tasks.map((task) => {
|
|
236
|
+
if (typeof task.description === 'string' && hasAllRequiredSections(task.description)) {
|
|
237
|
+
return task;
|
|
238
|
+
}
|
|
239
|
+
// Repair: synthesize a fresh description.
|
|
240
|
+
const sliceText = extractSliceTextForTask(blueprintText, task);
|
|
241
|
+
const upstream = (task.depends_on || [])
|
|
242
|
+
.map((dep) => enrichedById.get(dep))
|
|
243
|
+
.filter(Boolean);
|
|
244
|
+
const fresh = buildTaskDescription({
|
|
245
|
+
task,
|
|
246
|
+
slice: sliceText,
|
|
247
|
+
upstreamTasks: upstream,
|
|
248
|
+
gateCommands,
|
|
249
|
+
});
|
|
250
|
+
return { ...task, description: fresh };
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
return { tasks: repairedTasks };
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const REQUIRED_DESCRIPTION_SECTIONS = [
|
|
257
|
+
'Symbols you must produce',
|
|
258
|
+
'Symbols you may consume from upstream tasks',
|
|
259
|
+
'Boundary Map slice',
|
|
260
|
+
'Upstream tasks',
|
|
261
|
+
'GATES',
|
|
262
|
+
];
|
|
263
|
+
|
|
264
|
+
function hasAllRequiredSections(description) {
|
|
265
|
+
for (const marker of REQUIRED_DESCRIPTION_SECTIONS) {
|
|
266
|
+
if (!description.includes(marker)) return false;
|
|
267
|
+
}
|
|
268
|
+
return true;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function extractSliceTextForTask(blueprintText, task) {
|
|
272
|
+
// Find any Boundary Map slice whose File Plan files match the task's
|
|
273
|
+
// files_owned. We don't have a sliceId here, so we scan slice blocks for
|
|
274
|
+
// the first one whose File Plan ⊆ task.files_owned. Best-effort — only
|
|
275
|
+
// used in the description-repair path.
|
|
276
|
+
const lines = blueprintText.split(/\r?\n/);
|
|
277
|
+
const owned = new Set(task.files_owned || []);
|
|
278
|
+
const blocks = [];
|
|
279
|
+
let cur = null;
|
|
280
|
+
for (let i = 0; i < lines.length; i++) {
|
|
281
|
+
const m = lines[i].match(/^### (S\d{2,})/);
|
|
282
|
+
if (m) {
|
|
283
|
+
if (cur) blocks.push(cur);
|
|
284
|
+
cur = { id: m[1], start: i, end: lines.length };
|
|
285
|
+
} else if (cur && /^### S\d/.test(lines[i])) {
|
|
286
|
+
cur.end = i;
|
|
287
|
+
blocks.push(cur);
|
|
288
|
+
cur = null;
|
|
289
|
+
} else if (cur && /^## /.test(lines[i])) {
|
|
290
|
+
cur.end = i;
|
|
291
|
+
blocks.push(cur);
|
|
292
|
+
cur = null;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
if (cur) blocks.push(cur);
|
|
296
|
+
for (const b of blocks) {
|
|
297
|
+
const block = lines.slice(b.start, b.end).join('\n');
|
|
298
|
+
const fpMatch = block.match(/^File Plan\s*:\s*(.+)$/m);
|
|
299
|
+
if (!fpMatch) continue;
|
|
300
|
+
const files = [...fpMatch[1].matchAll(/`([^`]+)`/g)].map((mm) => mm[1].trim());
|
|
301
|
+
if (files.length > 0 && files.every((f) => owned.has(f))) {
|
|
302
|
+
return block;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
return '';
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function parseJsonFromText(text) {
|
|
309
|
+
// Strip code fences if present.
|
|
310
|
+
const trimmed = text.trim();
|
|
311
|
+
const fenced = trimmed.match(/```(?:json)?\s*\n?([\s\S]*?)\n?```/);
|
|
312
|
+
const body = fenced ? fenced[1] : trimmed;
|
|
313
|
+
return JSON.parse(body);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function collectChangedFiles(cwd) {
|
|
317
|
+
try {
|
|
318
|
+
const tracked = execSync('git diff --name-only HEAD', {
|
|
319
|
+
cwd, encoding: 'utf-8', timeout: 5000, stdio: ['ignore', 'pipe', 'ignore'],
|
|
320
|
+
}).trim();
|
|
321
|
+
const untracked = execSync('git ls-files --others --exclude-standard', {
|
|
322
|
+
cwd, encoding: 'utf-8', timeout: 5000, stdio: ['ignore', 'pipe', 'ignore'],
|
|
323
|
+
}).trim();
|
|
324
|
+
const all = [
|
|
325
|
+
...tracked.split('\n').filter(Boolean),
|
|
326
|
+
...untracked.split('\n').filter(Boolean),
|
|
327
|
+
];
|
|
328
|
+
return [...new Set(all)];
|
|
329
|
+
} catch {
|
|
330
|
+
return [];
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function collectBlackboard(cwd, featureCode) {
|
|
335
|
+
const dir = join(cwd, '.compose', 'gsd', featureCode, 'results');
|
|
336
|
+
if (!existsSync(dir)) return {};
|
|
337
|
+
const files = readdirSync(dir).filter((f) => f.endsWith('.json'));
|
|
338
|
+
const out = {};
|
|
339
|
+
const failures = [];
|
|
340
|
+
for (const f of files) {
|
|
341
|
+
const taskId = f.replace(/\.json$/, '');
|
|
342
|
+
let parsed;
|
|
343
|
+
try {
|
|
344
|
+
parsed = JSON.parse(readFileSync(join(dir, f), 'utf-8'));
|
|
345
|
+
} catch (err) {
|
|
346
|
+
failures.push(`${f}: unreadable JSON (${err.message})`);
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
const v = validateTaskResult(parsed);
|
|
350
|
+
if (v.ok) {
|
|
351
|
+
out[taskId] = parsed;
|
|
352
|
+
} else {
|
|
353
|
+
failures.push(`${f}: ${v.errors.join('; ')}`);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
if (failures.length > 0) {
|
|
357
|
+
// Plan T6 acceptance: blackboard must contain one VALIDATED entry per task.
|
|
358
|
+
// A partial blackboard is worse than no blackboard — fail loudly.
|
|
359
|
+
throw new Error(
|
|
360
|
+
`runGsd: ${failures.length} TaskResult file(s) failed validation; refusing to write partial blackboard:\n - ${failures.join('\n - ')}`,
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
return out;
|
|
364
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@smartmemory/compose",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.30-beta",
|
|
4
4
|
"description": "Structured AI dev pipeline — goal-to-product orchestration with gates, iteration loops, and feature lifecycle management.",
|
|
5
5
|
"author": "SmartMemory",
|
|
6
6
|
"license": "MIT",
|