@smartmemory/compose 0.1.7-beta → 0.1.8-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 +32 -5
- package/bin/compose.js +167 -5
- package/dist/assets/{_baseUniq-D-avYfn5.js → _baseUniq-3jW4HAOf.js} +1 -1
- package/dist/assets/{arc-BC4dfQ-X.js → arc-DzzDimyd.js} +1 -1
- package/dist/assets/{architectureDiagram-Q4EWVU46-BZmFXnGI.js → architectureDiagram-Q4EWVU46-CtAgwORz.js} +1 -1
- package/dist/assets/{blockDiagram-DXYQGD6D-DlfWSuux.js → blockDiagram-DXYQGD6D-Bryby0c_.js} +1 -1
- package/dist/assets/{c4Diagram-AHTNJAMY-Y__uJrRx.js → c4Diagram-AHTNJAMY-C7N9RTJ8.js} +1 -1
- package/dist/assets/channel-DDkv7DUd.js +1 -0
- package/dist/assets/{chunk-4BX2VUAB-BfMePfTp.js → chunk-4BX2VUAB-wijkFgZY.js} +1 -1
- package/dist/assets/{chunk-4TB4RGXK-BdlMSdEA.js → chunk-4TB4RGXK-zdSZGRS2.js} +1 -1
- package/dist/assets/{chunk-55IACEB6-vrQHZTdv.js → chunk-55IACEB6-6zqzTZQQ.js} +1 -1
- package/dist/assets/{chunk-EDXVE4YY-B8wioVlW.js → chunk-EDXVE4YY-frd1Vwf-.js} +1 -1
- package/dist/assets/{chunk-FMBD7UC4-Cd6Hrux2.js → chunk-FMBD7UC4-CdkRK5Hx.js} +1 -1
- package/dist/assets/{chunk-OYMX7WX6-CfrhdQXY.js → chunk-OYMX7WX6-C6bMB0cf.js} +1 -1
- package/dist/assets/{chunk-QZHKN3VN-B9JQerOU.js → chunk-QZHKN3VN-4vsxN3jq.js} +1 -1
- package/dist/assets/{chunk-YZCP3GAM-DFN9X99H.js → chunk-YZCP3GAM-DbNARKip.js} +1 -1
- package/dist/assets/classDiagram-6PBFFD2Q-J6ZTeCbW.js +1 -0
- package/dist/assets/classDiagram-v2-HSJHXN6E-J6ZTeCbW.js +1 -0
- package/dist/assets/clone-5MVZ89iV.js +1 -0
- package/dist/assets/{cose-bilkent-S5V4N54A-BAn0ap_E.js → cose-bilkent-S5V4N54A-BpXeV7Vj.js} +1 -1
- package/dist/assets/{dagre-KV5264BT-DyxnVq1g.js → dagre-KV5264BT-DQLu_W8r.js} +1 -1
- package/dist/assets/{diagram-5BDNPKRD-XCrzqski.js → diagram-5BDNPKRD-skaOoe5A.js} +1 -1
- package/dist/assets/{diagram-G4DWMVQ6-MBCAXft_.js → diagram-G4DWMVQ6-DezlfFH4.js} +1 -1
- package/dist/assets/{diagram-MMDJMWI5-DbtB2yS6.js → diagram-MMDJMWI5-BUu-v-wT.js} +1 -1
- package/dist/assets/{diagram-TYMM5635-Bb5NzX61.js → diagram-TYMM5635-CziQ6LPs.js} +1 -1
- package/dist/assets/{erDiagram-SMLLAGMA-CpIeCOh2.js → erDiagram-SMLLAGMA-BsAyOVTI.js} +1 -1
- package/dist/assets/{flowDiagram-DWJPFMVM-CHyoKnhW.js → flowDiagram-DWJPFMVM-CbYWJOLq.js} +1 -1
- package/dist/assets/{ganttDiagram-T4ZO3ILL-DErKteO_.js → ganttDiagram-T4ZO3ILL-CAwgDkLl.js} +1 -1
- package/dist/assets/{gitGraphDiagram-UUTBAWPF-KFVAtj2F.js → gitGraphDiagram-UUTBAWPF-DK4RlkjO.js} +1 -1
- package/dist/assets/{graph-CRnO_ifT.js → graph-orv1XHGx.js} +1 -1
- package/dist/assets/{index-DkRKLuNr.js → index-Ceywghsu.js} +143 -143
- package/dist/assets/{infoDiagram-42DDH7IO-BZFnuSp5.js → infoDiagram-42DDH7IO-DQyA75sK.js} +1 -1
- package/dist/assets/{ishikawaDiagram-UXIWVN3A-4Xe2Szde.js → ishikawaDiagram-UXIWVN3A-C-F_5q4k.js} +1 -1
- package/dist/assets/{journeyDiagram-VCZTEJTY-CZRByfS-.js → journeyDiagram-VCZTEJTY-Bj8UIvK-.js} +1 -1
- package/dist/assets/{kanban-definition-6JOO6SKY-B95sk6Fk.js → kanban-definition-6JOO6SKY-DZYr8Dp1.js} +1 -1
- package/dist/assets/{layout-BqNQzxWT.js → layout-CBaTKjpX.js} +1 -1
- package/dist/assets/{linear-CUh7qb64.js → linear-j1sI_SiN.js} +1 -1
- package/dist/assets/{min-wXgOS3ig.js → min-DtJISjld.js} +1 -1
- package/dist/assets/{mindmap-definition-QFDTVHPH-DB6iaAbO.js → mindmap-definition-QFDTVHPH-Bulb64RS.js} +1 -1
- package/dist/assets/{pieDiagram-DEJITSTG-CHkZHrTW.js → pieDiagram-DEJITSTG-D11keQxr.js} +1 -1
- package/dist/assets/{quadrantDiagram-34T5L4WZ-DoTEO8e3.js → quadrantDiagram-34T5L4WZ-BEcWQiEG.js} +1 -1
- package/dist/assets/{requirementDiagram-MS252O5E-Dn8peXYp.js → requirementDiagram-MS252O5E-Cbp23uDf.js} +1 -1
- package/dist/assets/{sankeyDiagram-XADWPNL6-DRXs6Ipb.js → sankeyDiagram-XADWPNL6-Dae1hMc5.js} +1 -1
- package/dist/assets/{sequenceDiagram-FGHM5R23-wBBYZ0aq.js → sequenceDiagram-FGHM5R23-C16abORi.js} +1 -1
- package/dist/assets/{stateDiagram-FHFEXIEX-DPlBNGmf.js → stateDiagram-FHFEXIEX-CbEtfhbx.js} +1 -1
- package/dist/assets/stateDiagram-v2-QKLJ7IA2-CyY84hEA.js +1 -0
- package/dist/assets/{timeline-definition-GMOUNBTQ-CbbyTlHk.js → timeline-definition-GMOUNBTQ-BV7JTNMI.js} +1 -1
- package/dist/assets/{vennDiagram-DHZGUBPP-Bj4GaFfj.js → vennDiagram-DHZGUBPP-DBZiT48j.js} +1 -1
- package/dist/assets/{wardley-RL74JXVD-RtNzq8KU.js → wardley-RL74JXVD-Cc8uoiL3.js} +37 -37
- package/dist/assets/{wardleyDiagram-NUSXRM2D-CDfE3zSj.js → wardleyDiagram-NUSXRM2D-DEYcWGo5.js} +1 -1
- package/dist/assets/{xychartDiagram-5P7HB3ND-CZXHHYD5.js → xychartDiagram-5P7HB3ND-bFhLXv2b.js} +1 -1
- package/dist/index.html +1 -1
- package/lib/build.js +193 -19
- package/lib/completion-writer.js +7 -4
- package/lib/deps.js +17 -6
- package/lib/feature-events.js +3 -0
- package/lib/feature-writer.js +34 -22
- package/lib/followup-writer.js +556 -0
- package/lib/mcp-enforcement.js +173 -0
- package/lib/migrate-roadmap.js +4 -1
- package/lib/project-paths.js +36 -0
- package/lib/review-lenses.js +23 -8
- package/lib/review-normalize.js +42 -3
- package/lib/roadmap-drift.js +54 -0
- package/lib/roadmap-gen.js +297 -27
- package/lib/roadmap-preservers.js +353 -0
- package/lib/step-prompt.js +15 -0
- package/lib/triage.js +2 -1
- package/lib/version-check.js +110 -0
- package/package.json +1 -1
- package/server/compose-mcp-tools.js +16 -2
- package/server/compose-mcp.js +24 -1
- package/server/vision-routes.js +51 -2
- package/templates/ROADMAP.md +6 -0
- package/dist/assets/channel-LRG9kHqJ.js +0 -1
- package/dist/assets/classDiagram-6PBFFD2Q-BC9a6pDE.js +0 -1
- package/dist/assets/classDiagram-v2-HSJHXN6E-BC9a6pDE.js +0 -1
- package/dist/assets/clone-dRxgFrBv.js +0 -1
- package/dist/assets/stateDiagram-v2-QKLJ7IA2-BW0ezXb4.js +0 -1
package/lib/roadmap-gen.js
CHANGED
|
@@ -7,8 +7,32 @@
|
|
|
7
7
|
import { readFileSync, writeFileSync, existsSync } from 'fs';
|
|
8
8
|
import { join } from 'path';
|
|
9
9
|
import { listFeatures } from './feature-json.js';
|
|
10
|
+
import { loadFeaturesDir } from './project-paths.js';
|
|
11
|
+
import {
|
|
12
|
+
readPhaseOverrides,
|
|
13
|
+
readAnonymousRows,
|
|
14
|
+
readPreservedSections,
|
|
15
|
+
readPreservedSectionAnchors,
|
|
16
|
+
readPhaseOrder,
|
|
17
|
+
readPhaseBlocks,
|
|
18
|
+
} from './roadmap-preservers.js';
|
|
19
|
+
import { emitDrift } from './roadmap-drift.js';
|
|
10
20
|
|
|
11
21
|
const STATUS_ORDER = ['IN_PROGRESS', 'PARTIAL', 'PLANNED', 'COMPLETE', 'SUPERSEDED', 'PARKED'];
|
|
22
|
+
const STATUS_TOKENS = ['COMPLETE', 'IN_PROGRESS', 'PARTIAL', 'PLANNED', 'SUPERSEDED', 'PARKED', 'BLOCKED', 'KILLED'];
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Extract the leading status token from override text like
|
|
26
|
+
* `PARTIAL (1a–1d COMPLETE, 2 PLANNED)` → `PARTIAL`.
|
|
27
|
+
* Returns null if no token recognized.
|
|
28
|
+
*/
|
|
29
|
+
function parseStatusToken(override) {
|
|
30
|
+
for (const t of STATUS_TOKENS) {
|
|
31
|
+
if (override === t) return t;
|
|
32
|
+
if (override.startsWith(t + ' ') || override.startsWith(t + '(')) return t;
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
12
36
|
|
|
13
37
|
/**
|
|
14
38
|
* Compute the aggregate status for a phase based on its features.
|
|
@@ -32,11 +56,27 @@ function phaseStatus(features) {
|
|
|
32
56
|
* @returns {string} - Generated ROADMAP.md content
|
|
33
57
|
*/
|
|
34
58
|
export function generateRoadmap(cwd, opts = {}) {
|
|
35
|
-
const featuresDir = opts.featuresDir ??
|
|
59
|
+
const featuresDir = opts.featuresDir ?? loadFeaturesDir(cwd);
|
|
36
60
|
const features = listFeatures(cwd, featuresDir);
|
|
37
61
|
|
|
38
|
-
// Read existing ROADMAP.md
|
|
39
|
-
const
|
|
62
|
+
// Read existing ROADMAP.md once: preamble + curated content for splice-back.
|
|
63
|
+
const roadmapPath = join(cwd, 'ROADMAP.md');
|
|
64
|
+
const existingText = existsSync(roadmapPath) ? readFileSync(roadmapPath, 'utf-8') : '';
|
|
65
|
+
const preamble = readPreamble(cwd, opts, existingText);
|
|
66
|
+
const overrides = readPhaseOverrides(existingText);
|
|
67
|
+
const anonRows = readAnonymousRows(existingText);
|
|
68
|
+
const preserved = readPreservedSections(existingText);
|
|
69
|
+
const anchors = readPreservedSectionAnchors(existingText);
|
|
70
|
+
const sourcePhaseOrder = readPhaseOrder(existingText);
|
|
71
|
+
const phaseBlocks = readPhaseBlocks(existingText);
|
|
72
|
+
|
|
73
|
+
// Build anchor → preservedIds Map for splice-back during phase emission.
|
|
74
|
+
const anchorToPreserved = new Map();
|
|
75
|
+
for (const [id, anchor] of anchors) {
|
|
76
|
+
const arr = anchorToPreserved.get(anchor) ?? [];
|
|
77
|
+
arr.push(id);
|
|
78
|
+
anchorToPreserved.set(anchor, arr);
|
|
79
|
+
}
|
|
40
80
|
|
|
41
81
|
// Group by phase
|
|
42
82
|
const phases = new Map();
|
|
@@ -54,27 +94,97 @@ export function generateRoadmap(cwd, opts = {}) {
|
|
|
54
94
|
|
|
55
95
|
const sections = [preamble.trimEnd()];
|
|
56
96
|
|
|
57
|
-
//
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
97
|
+
// Splice in any preserved sections anchored to null (top-of-file, before first phase).
|
|
98
|
+
for (const id of anchorToPreserved.get(null) ?? []) {
|
|
99
|
+
sections.push(preserved.get(id));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Build merged phase order: source order first (preserves curated sequencing
|
|
103
|
+
// for empty/legacy phases that have no feature.json), then any feature.json
|
|
104
|
+
// phases not yet in source (truly new phases) sorted by feature position.
|
|
105
|
+
const orderedPhaseIds = [...sourcePhaseOrder];
|
|
106
|
+
const seenInSource = new Set(sourcePhaseOrder);
|
|
107
|
+
const newPhases = [...phases.keys()].filter(p => !seenInSource.has(p));
|
|
108
|
+
newPhases.sort((a, b) => {
|
|
109
|
+
const minA = Math.min(...phases.get(a).map(f => f.position ?? 999));
|
|
110
|
+
const minB = Math.min(...phases.get(b).map(f => f.position ?? 999));
|
|
61
111
|
return minA - minB;
|
|
62
112
|
});
|
|
113
|
+
orderedPhaseIds.push(...newPhases);
|
|
114
|
+
|
|
115
|
+
// Render each phase. Phases with feature.json features render their tables;
|
|
116
|
+
// phases that exist only in source (override + anon rows) render heading + anon rows.
|
|
117
|
+
const emittedPreservedIds = new Set();
|
|
118
|
+
for (const phase of orderedPhaseIds) {
|
|
119
|
+
const phaseFeatures = phases.get(phase) ?? [];
|
|
120
|
+
const override = overrides.get(phase);
|
|
63
121
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
122
|
+
if (phaseFeatures.length === 0) {
|
|
123
|
+
// No feature.json features for this phase. Splice the original phase
|
|
124
|
+
// block verbatim — preserves heading override + intro prose + table
|
|
125
|
+
// (anon rows) + exit text. If the source has no block (truly new phase
|
|
126
|
+
// with no source counterpart and no features), skip.
|
|
127
|
+
const block = phaseBlocks.get(phase);
|
|
128
|
+
if (block && block.trim().length > 0) {
|
|
129
|
+
sections.push(block.trimEnd());
|
|
130
|
+
for (const id of anchorToPreserved.get(phase) ?? []) {
|
|
131
|
+
sections.push(preserved.get(id));
|
|
132
|
+
emittedPreservedIds.add(id);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Typed phase: render from feature.json with override + anon-row interleave.
|
|
139
|
+
const rollupStatus = phaseStatus(phaseFeatures);
|
|
140
|
+
let headingStatus = rollupStatus;
|
|
141
|
+
if (override) {
|
|
142
|
+
const overrideToken = parseStatusToken(override);
|
|
143
|
+
if (overrideToken && overrideToken !== rollupStatus) {
|
|
144
|
+
emitDrift(cwd, { phaseId: phase, override, computed: rollupStatus });
|
|
145
|
+
}
|
|
146
|
+
// Override always wins. We can't reliably distinguish curated overrides
|
|
147
|
+
// from previously-auto-generated rollups without explicit marking, so
|
|
148
|
+
// we preserve all overrides and let drift detection surface divergence.
|
|
149
|
+
headingStatus = override;
|
|
150
|
+
}
|
|
151
|
+
const phaseAnonRows = anonRows.get(phase) ?? [];
|
|
152
|
+
const sourceBlock = phaseBlocks.get(phase);
|
|
153
|
+
if (sourceBlock) {
|
|
154
|
+
// Splice regenerated table into the source block so curated prose
|
|
155
|
+
// (intro paragraph, exit text, links) survives.
|
|
156
|
+
sections.push(spliceTableIntoBlock(sourceBlock, phase, headingStatus, phaseFeatures, phaseAnonRows));
|
|
157
|
+
} else {
|
|
158
|
+
// Truly new phase (no source counterpart): synthesize from scratch.
|
|
159
|
+
sections.push(renderPhase(phase, headingStatus, phaseFeatures, phaseAnonRows));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
for (const id of anchorToPreserved.get(phase) ?? []) {
|
|
163
|
+
sections.push(preserved.get(id));
|
|
164
|
+
emittedPreservedIds.add(id);
|
|
165
|
+
}
|
|
68
166
|
}
|
|
69
167
|
|
|
70
168
|
// Render ungrouped features
|
|
71
169
|
if (ungrouped.length > 0) {
|
|
72
|
-
sections.push(renderPhase('Features', phaseStatus(ungrouped), ungrouped));
|
|
170
|
+
sections.push(renderPhase('Features', phaseStatus(ungrouped), ungrouped, []));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Splice any preserved sections whose anchor phase didn't survive — append at end.
|
|
174
|
+
for (const [id, anchor] of anchors) {
|
|
175
|
+
if (anchor === null) continue;
|
|
176
|
+
if (emittedPreservedIds.has(id)) continue;
|
|
177
|
+
if (!phases.has(anchor)) {
|
|
178
|
+
sections.push(preserved.get(id));
|
|
179
|
+
}
|
|
73
180
|
}
|
|
74
181
|
|
|
75
|
-
// Key documents section
|
|
76
|
-
|
|
77
|
-
if (
|
|
182
|
+
// Key documents auto-gen is suppressed when a `key-documents` preserved-section exists.
|
|
183
|
+
// The preserved section already carries the curated content (including external links).
|
|
184
|
+
if (!preserved.has('key-documents')) {
|
|
185
|
+
const keyDocs = buildKeyDocs(features, featuresDir);
|
|
186
|
+
if (keyDocs) sections.push(keyDocs);
|
|
187
|
+
}
|
|
78
188
|
|
|
79
189
|
return sections.join('\n\n---\n\n') + '\n';
|
|
80
190
|
}
|
|
@@ -83,16 +193,27 @@ export function generateRoadmap(cwd, opts = {}) {
|
|
|
83
193
|
* Read the preamble (everything before the first ## Phase/Feature section)
|
|
84
194
|
* from an existing ROADMAP.md, or generate a default one.
|
|
85
195
|
*/
|
|
86
|
-
function readPreamble(cwd, opts) {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
//
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
196
|
+
function readPreamble(cwd, opts, existingText) {
|
|
197
|
+
if (existingText && existingText.length > 0) {
|
|
198
|
+
// Find the first `## ` or `<!-- preserved-section: ... -->` marker.
|
|
199
|
+
// Preserved-section markers belong to the preservation flow, not the preamble,
|
|
200
|
+
// so they bound the preamble the same way phase headings do.
|
|
201
|
+
let firstHeadingIdx = -1;
|
|
202
|
+
for (const re of [/^## /m, /^<!--\s*preserved-section:/m]) {
|
|
203
|
+
const m = existingText.match(re);
|
|
204
|
+
if (m) {
|
|
205
|
+
const idx = existingText.indexOf(m[0]);
|
|
206
|
+
if (firstHeadingIdx === -1 || idx < firstHeadingIdx) firstHeadingIdx = idx;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
if (firstHeadingIdx > 0) {
|
|
210
|
+
// Walk back over a possible `---\n\n` separator immediately before the heading
|
|
211
|
+
// so it doesn't get duplicated against the join("\n\n---\n\n") below.
|
|
212
|
+
let cutIdx = firstHeadingIdx;
|
|
213
|
+
const tail = existingText.slice(0, cutIdx).trimEnd();
|
|
214
|
+
// If preamble ends with `---`, strip it.
|
|
215
|
+
const stripped = tail.replace(/\n---\s*$/, '').trimEnd();
|
|
216
|
+
if (stripped.length > 0) return stripped;
|
|
96
217
|
}
|
|
97
218
|
}
|
|
98
219
|
|
|
@@ -119,7 +240,131 @@ ${desc ? desc + '\n\n' : ''}<!-- Generated from feature.json — do not edit man
|
|
|
119
240
|
/**
|
|
120
241
|
* Render a phase section with its feature table.
|
|
121
242
|
*/
|
|
122
|
-
|
|
243
|
+
/**
|
|
244
|
+
* Splice a regenerated table into a source phase block so curated prose
|
|
245
|
+
* (intro paragraph, exit text, doc links) survives a no-op regen.
|
|
246
|
+
*
|
|
247
|
+
* Strategy: the source block is `<heading>\n\n<prose>\n\n<table>\n\n<trailing>`.
|
|
248
|
+
* Find the table boundaries (header row + divider through the last contiguous
|
|
249
|
+
* `|...|` row), replace heading line with the override/rollup-aware version,
|
|
250
|
+
* replace the table portion with a freshly rendered one, keep everything else.
|
|
251
|
+
*/
|
|
252
|
+
function spliceTableIntoBlock(sourceBlock, phaseName, headingStatus, features, anonRows) {
|
|
253
|
+
const lines = sourceBlock.split('\n');
|
|
254
|
+
const isTableLine = (s) => /^\s*\|.*\|\s*$/.test(s);
|
|
255
|
+
|
|
256
|
+
// Locate the first table row (header) — first `|...|` line.
|
|
257
|
+
let tableStart = -1;
|
|
258
|
+
for (let i = 0; i < lines.length; i++) {
|
|
259
|
+
if (isTableLine(lines[i])) {
|
|
260
|
+
tableStart = i;
|
|
261
|
+
break;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Locate the last contiguous table row — walk forward from tableStart over
|
|
266
|
+
// any `|...|` lines. Allow blank lines inside if the next non-blank is a
|
|
267
|
+
// table line (rare but possible). Simpler: stop at first non-table-non-blank.
|
|
268
|
+
let tableEnd = tableStart;
|
|
269
|
+
if (tableStart !== -1) {
|
|
270
|
+
let i = tableStart;
|
|
271
|
+
while (i < lines.length) {
|
|
272
|
+
if (isTableLine(lines[i])) {
|
|
273
|
+
tableEnd = i;
|
|
274
|
+
i++;
|
|
275
|
+
} else if (lines[i].trim() === '' && i + 1 < lines.length && isTableLine(lines[i + 1])) {
|
|
276
|
+
i++; // skip blank between rows
|
|
277
|
+
} else {
|
|
278
|
+
break;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Build the replacement table (header + divider + rows with anon interleave).
|
|
284
|
+
const newTable = renderTableLines(features, anonRows);
|
|
285
|
+
|
|
286
|
+
// Replace heading line (always lines[0] for a phase block).
|
|
287
|
+
const headingLine = `## ${phaseName} — ${headingStatus}`;
|
|
288
|
+
const out = [headingLine];
|
|
289
|
+
|
|
290
|
+
if (tableStart === -1) {
|
|
291
|
+
// No existing table — append the new one after the existing prose.
|
|
292
|
+
for (let i = 1; i < lines.length; i++) out.push(lines[i]);
|
|
293
|
+
if (out.length > 0 && out[out.length - 1].trim() !== '') out.push('');
|
|
294
|
+
out.push(...newTable);
|
|
295
|
+
return out.join('\n');
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Emit prose between heading and table.
|
|
299
|
+
for (let i = 1; i < tableStart; i++) out.push(lines[i]);
|
|
300
|
+
|
|
301
|
+
// Emit the regenerated table.
|
|
302
|
+
out.push(...newTable);
|
|
303
|
+
|
|
304
|
+
// Emit trailing content after the original table.
|
|
305
|
+
for (let i = tableEnd + 1; i < lines.length; i++) out.push(lines[i]);
|
|
306
|
+
|
|
307
|
+
return out.join('\n');
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Render the table portion (header + divider + rows) for a phase. Anon rows
|
|
312
|
+
* interleave by predecessorCode rules.
|
|
313
|
+
*/
|
|
314
|
+
function renderTableLines(features, anonRows) {
|
|
315
|
+
const hasSubItems = features.some(f => f.items && f.items.length > 0);
|
|
316
|
+
const lines = [];
|
|
317
|
+
|
|
318
|
+
const anonByPredecessor = new Map();
|
|
319
|
+
for (const row of anonRows) {
|
|
320
|
+
const arr = anonByPredecessor.get(row.predecessorCode) ?? [];
|
|
321
|
+
arr.push(row.rawLine);
|
|
322
|
+
anonByPredecessor.set(row.predecessorCode, arr);
|
|
323
|
+
}
|
|
324
|
+
const emitAnonAfter = (code) => {
|
|
325
|
+
for (const raw of anonByPredecessor.get(code) ?? []) lines.push(raw);
|
|
326
|
+
anonByPredecessor.delete(code);
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
if (hasSubItems) {
|
|
330
|
+
lines.push('| # | Feature | Item | Status |');
|
|
331
|
+
lines.push('|---|---------|------|--------|');
|
|
332
|
+
emitAnonAfter(null);
|
|
333
|
+
for (const f of features) {
|
|
334
|
+
if (f.items && f.items.length > 0) {
|
|
335
|
+
for (const item of f.items) {
|
|
336
|
+
const num = item.position ?? '—';
|
|
337
|
+
const desc = item.description ?? '';
|
|
338
|
+
const st = item.status ?? f.status;
|
|
339
|
+
lines.push(`| ${num} | ${f.code} | ${desc} | ${st} |`);
|
|
340
|
+
}
|
|
341
|
+
} else {
|
|
342
|
+
const num = f.position ?? '—';
|
|
343
|
+
lines.push(`| ${num} | ${f.code} | ${f.description} | ${f.status} |`);
|
|
344
|
+
}
|
|
345
|
+
emitAnonAfter(f.code);
|
|
346
|
+
}
|
|
347
|
+
} else {
|
|
348
|
+
lines.push('| # | Feature | Description | Status |');
|
|
349
|
+
lines.push('|---|---------|-------------|--------|');
|
|
350
|
+
emitAnonAfter(null);
|
|
351
|
+
for (const f of features) {
|
|
352
|
+
const num = f.position ?? '—';
|
|
353
|
+
const desc = f.description ?? '';
|
|
354
|
+
lines.push(`| ${num} | ${f.code} | ${desc} | ${f.status} |`);
|
|
355
|
+
emitAnonAfter(f.code);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Leftovers (predecessor deleted) — append at end of table.
|
|
360
|
+
for (const [, leftover] of anonByPredecessor) {
|
|
361
|
+
for (const raw of leftover) lines.push(raw);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return lines;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function renderPhase(phaseName, status, features, anonRows = []) {
|
|
123
368
|
const lines = [`## ${phaseName} — ${status}`, ''];
|
|
124
369
|
|
|
125
370
|
// Phase description from the first feature's phaseDescription if available
|
|
@@ -131,10 +376,24 @@ function renderPhase(phaseName, status, features) {
|
|
|
131
376
|
// Determine table columns based on whether features have sub-items
|
|
132
377
|
const hasSubItems = features.some(f => f.items && f.items.length > 0);
|
|
133
378
|
|
|
379
|
+
// Index anon rows by predecessorCode for interleave.
|
|
380
|
+
const anonByPredecessor = new Map();
|
|
381
|
+
for (const row of anonRows) {
|
|
382
|
+
const key = row.predecessorCode; // null = head-of-table
|
|
383
|
+
const arr = anonByPredecessor.get(key) ?? [];
|
|
384
|
+
arr.push(row.rawLine);
|
|
385
|
+
anonByPredecessor.set(key, arr);
|
|
386
|
+
}
|
|
387
|
+
const emitAnonAfter = (code) => {
|
|
388
|
+
for (const raw of anonByPredecessor.get(code) ?? []) lines.push(raw);
|
|
389
|
+
anonByPredecessor.delete(code);
|
|
390
|
+
};
|
|
391
|
+
|
|
134
392
|
if (hasSubItems) {
|
|
135
393
|
// Expanded: one row per sub-item
|
|
136
394
|
lines.push('| # | Feature | Item | Status |');
|
|
137
395
|
lines.push('|---|---------|------|--------|');
|
|
396
|
+
emitAnonAfter(null); // any head-of-table anon rows
|
|
138
397
|
for (const f of features) {
|
|
139
398
|
if (f.items && f.items.length > 0) {
|
|
140
399
|
for (const item of f.items) {
|
|
@@ -147,18 +406,29 @@ function renderPhase(phaseName, status, features) {
|
|
|
147
406
|
const num = f.position ?? '—';
|
|
148
407
|
lines.push(`| ${num} | ${f.code} | ${f.description} | ${f.status} |`);
|
|
149
408
|
}
|
|
409
|
+
emitAnonAfter(f.code);
|
|
150
410
|
}
|
|
151
411
|
} else {
|
|
152
412
|
// Simple: one row per feature
|
|
153
413
|
lines.push('| # | Feature | Description | Status |');
|
|
154
414
|
lines.push('|---|---------|-------------|--------|');
|
|
415
|
+
// COMP-MCP-MIGRATION-2-1: no truncation. Curated descriptions are
|
|
416
|
+
// often multi-sentence; truncating them at 80 chars makes regen lossy.
|
|
417
|
+
// Markdown tables tolerate long cells fine.
|
|
418
|
+
emitAnonAfter(null); // any head-of-table anon rows
|
|
155
419
|
for (const f of features) {
|
|
156
420
|
const num = f.position ?? '—';
|
|
157
|
-
const desc = f.description
|
|
421
|
+
const desc = f.description ?? '';
|
|
158
422
|
lines.push(`| ${num} | ${f.code} | ${desc} | ${f.status} |`);
|
|
423
|
+
emitAnonAfter(f.code);
|
|
159
424
|
}
|
|
160
425
|
}
|
|
161
426
|
|
|
427
|
+
// Any anon rows whose predecessor was deleted: append at the end of the table.
|
|
428
|
+
for (const [, leftover] of anonByPredecessor) {
|
|
429
|
+
for (const raw of leftover) lines.push(raw);
|
|
430
|
+
}
|
|
431
|
+
|
|
162
432
|
// Exit criteria
|
|
163
433
|
const exit = features[0]?.phaseExit;
|
|
164
434
|
if (exit) {
|