@polymorphism-tech/morph-spec 8.1.1 → 8.2.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 +2 -2
- package/bin/morph-spec.js +15 -0
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +6 -4
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +6 -3
- package/framework/MORPH.md +8 -2
- package/framework/hooks/claude-code/core/validate-completion.js +2 -1
- package/framework/hooks/claude-code/core/worktree-context.js +43 -0
- package/framework/hooks/shared/build-check.js +97 -24
- package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
- package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
- package/framework/skills/core/morph-apply/SKILL.md +2 -2
- package/framework/skills/core/morph-brainstorming/SKILL.md +1 -1
- package/framework/skills/core/morph-eval/SKILL.md +1 -1
- package/framework/skills/core/morph-hotfix/SKILL.md +1 -1
- package/framework/skills/core/morph-implement/SKILL.md +1 -1
- package/framework/skills/core/morph-plan/SKILL.md +1 -1
- package/framework/skills/core/morph-proposal/SKILL.md +1 -1
- package/framework/skills/core/morph-review/SKILL.md +1 -1
- package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
- package/framework/skills/core/morph-standards/SKILL.md +1 -1
- package/package.json +1 -1
- package/src/commands/project/fleet.js +54 -0
- package/src/commands/project/worktree.js +56 -7
- package/src/commands/state/advance.js +8 -0
- package/src/commands/state/create.js +6 -0
- package/src/commands/state/handoff.js +49 -0
- package/src/lib/generators/handoff-generator.js +267 -0
- package/src/lib/generators/index.js +1 -3
- package/src/lib/validators/verify-runner.js +24 -7
- package/src/lib/worktree/env-allocator.js +149 -0
- package/src/lib/worktree/fleet-scanner.js +156 -0
- package/src/lib/context/context-builder.js +0 -289
- package/src/lib/generators/context-generator.js +0 -519
- package/src/lib/generators/mandate-generator.js +0 -468
- package/src/lib/generators/recap-generator.js +0 -285
|
@@ -1,468 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Implementation Mandate Generator
|
|
3
|
-
*
|
|
4
|
-
* Synthesizes the plan-phase outputs (spec.md, tasks.json, decisions.md) into
|
|
5
|
-
* a single task-structured implementation mandate. Contracts are read from
|
|
6
|
-
* spec.md (v5 keeps contracts inside the spec, not in separate files).
|
|
7
|
-
*
|
|
8
|
-
* The mandate is the primary context artifact for the implement phase —
|
|
9
|
-
* each task section is self-contained with acceptance criteria, contracts,
|
|
10
|
-
* file paths, and architectural constraints.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import { readFileSync, existsSync } from 'fs';
|
|
14
|
-
import { join } from 'path';
|
|
15
|
-
import { validateTasksJson } from '../tasks/tasks-json-parser.js';
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Resolve a single task object from parsed tasks.json data.
|
|
19
|
-
* FR references are extracted from the task `description` (v5 convention:
|
|
20
|
-
* tasks reference their FR(s) inline in the description).
|
|
21
|
-
*
|
|
22
|
-
* @param {object} tasksData - Parsed tasks.json structure
|
|
23
|
-
* @param {string} taskId - Task id (e.g. 'T1')
|
|
24
|
-
* @returns {{ id: string, title: string, description: string, frs: string[] }|null}
|
|
25
|
-
*/
|
|
26
|
-
function resolveTask(tasksData, taskId) {
|
|
27
|
-
const task = tasksData.tasks.find(t => t.id === taskId);
|
|
28
|
-
if (!task) return null;
|
|
29
|
-
const frRefs = [...(task.description || '').matchAll(/\bFR-?\d+\b/g)].map(m => m[0]);
|
|
30
|
-
return {
|
|
31
|
-
id: task.id,
|
|
32
|
-
title: task.title || '',
|
|
33
|
-
description: task.description || '',
|
|
34
|
-
frs: [...new Set(frRefs)],
|
|
35
|
-
files: Array.isArray(task.outputs) ? task.outputs : [],
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Extract all FR sections from spec.md.
|
|
41
|
-
*
|
|
42
|
-
* @param {string} specContent - Full spec.md content
|
|
43
|
-
* @returns {Map<string, { title: string, content: string, criteria: string[] }>}
|
|
44
|
-
*/
|
|
45
|
-
function parseFRSections(specContent) {
|
|
46
|
-
const sections = new Map();
|
|
47
|
-
const frPattern = /^##\s+(FR\d+)[:\s—–-]+(.+?)$/gm;
|
|
48
|
-
let match;
|
|
49
|
-
const matches = [];
|
|
50
|
-
|
|
51
|
-
while ((match = frPattern.exec(specContent)) !== null) {
|
|
52
|
-
matches.push({
|
|
53
|
-
id: match[1],
|
|
54
|
-
title: match[2].trim(),
|
|
55
|
-
start: match.index,
|
|
56
|
-
headerEnd: match.index + match[0].length,
|
|
57
|
-
});
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
for (let i = 0; i < matches.length; i++) {
|
|
61
|
-
const current = matches[i];
|
|
62
|
-
const nextStart = i + 1 < matches.length ? matches[i + 1].start : specContent.length;
|
|
63
|
-
const body = specContent.slice(current.headerEnd, nextStart).trim();
|
|
64
|
-
|
|
65
|
-
// Extract bullet points as acceptance criteria
|
|
66
|
-
const criteria = [];
|
|
67
|
-
const bulletPattern = /^[\s]*[-*]\s+(.+)/gm;
|
|
68
|
-
let bulletMatch;
|
|
69
|
-
while ((bulletMatch = bulletPattern.exec(body)) !== null) {
|
|
70
|
-
criteria.push(bulletMatch[1].trim());
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
sections.set(current.id, {
|
|
74
|
-
title: current.title,
|
|
75
|
-
content: body,
|
|
76
|
-
criteria,
|
|
77
|
-
});
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
return sections;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Extract contract types/interfaces from content. Matches C# records, classes,
|
|
85
|
-
* interfaces and TypeScript types/interfaces — v5 contracts are defined inside
|
|
86
|
-
* spec.md, so this scans the spec content.
|
|
87
|
-
*
|
|
88
|
-
* @param {string} contractsContent - Content to scan for contract definitions
|
|
89
|
-
* @returns {Map<string, string>} Map of type name → definition
|
|
90
|
-
*/
|
|
91
|
-
function parseContracts(contractsContent) {
|
|
92
|
-
const types = new Map();
|
|
93
|
-
|
|
94
|
-
// C# sealed records: sealed record Foo(...)
|
|
95
|
-
const csRecordPattern = /^\s*((?:public\s+)?sealed\s+record\s+\w+\([^)]*\)\s*;)/gm;
|
|
96
|
-
let match;
|
|
97
|
-
while ((match = csRecordPattern.exec(contractsContent)) !== null) {
|
|
98
|
-
const nameMatch = match[1].match(/record\s+(\w+)/);
|
|
99
|
-
if (nameMatch) {
|
|
100
|
-
types.set(nameMatch[1], match[1].trim());
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// C# interfaces: public interface IFoo { ... }
|
|
105
|
-
const csInterfacePattern = /^\s*((?:public\s+)?interface\s+\w+\s*\{[^}]*\})/gms;
|
|
106
|
-
while ((match = csInterfacePattern.exec(contractsContent)) !== null) {
|
|
107
|
-
const nameMatch = match[1].match(/interface\s+(\w+)/);
|
|
108
|
-
if (nameMatch) {
|
|
109
|
-
types.set(nameMatch[1], match[1].trim());
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// TypeScript types/interfaces
|
|
114
|
-
const tsTypePattern = /^\s*(export\s+(?:type|interface)\s+\w+[\s\S]*?(?:\};?|=[^;]+;))/gm;
|
|
115
|
-
while ((match = tsTypePattern.exec(contractsContent)) !== null) {
|
|
116
|
-
const nameMatch = match[1].match(/(?:type|interface)\s+(\w+)/);
|
|
117
|
-
if (nameMatch) {
|
|
118
|
-
types.set(nameMatch[1], match[1].trim());
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
return types;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* Extract architectural constraints from decisions.md.
|
|
127
|
-
*
|
|
128
|
-
* @param {string} decisionsContent - Full decisions.md content
|
|
129
|
-
* @returns {string[]} One-line constraint summaries
|
|
130
|
-
*/
|
|
131
|
-
function parseDecisions(decisionsContent) {
|
|
132
|
-
const constraints = [];
|
|
133
|
-
const adrPattern = /^###?\s+(ADR-\d+)[:\s—–-]+(.+?)$/gm;
|
|
134
|
-
let match;
|
|
135
|
-
|
|
136
|
-
while ((match = adrPattern.exec(decisionsContent)) !== null) {
|
|
137
|
-
constraints.push(`${match[1]}: ${match[2].trim()}`);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
return constraints;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* Extract all task IDs from parsed tasks.json data, in declared order.
|
|
145
|
-
*
|
|
146
|
-
* @param {object} tasksData - Parsed tasks.json structure
|
|
147
|
-
* @returns {string[]} Task IDs in order (e.g., ['T1', 'T2', ...])
|
|
148
|
-
*/
|
|
149
|
-
function extractTaskIds(tasksData) {
|
|
150
|
-
return tasksData.tasks.map(t => t.id);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* Find contract types relevant to an FR or task.
|
|
155
|
-
* Matches by: type name in content, or FR title keywords in type name.
|
|
156
|
-
*
|
|
157
|
-
* @param {string} searchText - FR content + task title/description
|
|
158
|
-
* @param {Map<string, string>} contracts - Parsed contracts
|
|
159
|
-
* @returns {string[]} Matching contract definitions
|
|
160
|
-
*/
|
|
161
|
-
function findRelevantContracts(searchText, contracts) {
|
|
162
|
-
const relevant = [];
|
|
163
|
-
const searchLower = searchText.toLowerCase();
|
|
164
|
-
|
|
165
|
-
for (const [typeName, definition] of contracts) {
|
|
166
|
-
// Direct match: type name appears in the search text
|
|
167
|
-
if (searchText.includes(typeName)) {
|
|
168
|
-
relevant.push(definition);
|
|
169
|
-
continue;
|
|
170
|
-
}
|
|
171
|
-
// Keyword match: split type name into words (e.g., UserRegisterRequest → user, register, request)
|
|
172
|
-
// and check if the significant words appear in the search text
|
|
173
|
-
const typeWords = typeName
|
|
174
|
-
.replace(/([a-z])([A-Z])/g, '$1 $2')
|
|
175
|
-
.toLowerCase()
|
|
176
|
-
.split(/\s+/)
|
|
177
|
-
.filter(w => w.length > 3 && !['request', 'response', 'result'].includes(w));
|
|
178
|
-
if (typeWords.length > 0 && typeWords.every(w => searchLower.includes(w))) {
|
|
179
|
-
relevant.push(definition);
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
return relevant;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
/**
|
|
186
|
-
* Generate an implementation mandate from spec phase outputs.
|
|
187
|
-
*
|
|
188
|
-
* @param {string} projectPath - Project root
|
|
189
|
-
* @param {string} featureName - Feature name
|
|
190
|
-
* @returns {{ mandate: string, stats: { frCount: number, taskCount: number, totalChars: number } }|null}
|
|
191
|
-
*/
|
|
192
|
-
export function generateMandate(projectPath, featureName) {
|
|
193
|
-
const featureDir = join(projectPath, '.morph/features', featureName);
|
|
194
|
-
|
|
195
|
-
// Read required files
|
|
196
|
-
const tasksPath = join(featureDir, '2-plan/tasks.json');
|
|
197
|
-
const specPath = join(featureDir, '2-plan/spec.md');
|
|
198
|
-
|
|
199
|
-
if (!existsSync(tasksPath) || !existsSync(specPath)) return null;
|
|
200
|
-
|
|
201
|
-
let tasksData;
|
|
202
|
-
try {
|
|
203
|
-
tasksData = JSON.parse(readFileSync(tasksPath, 'utf-8'));
|
|
204
|
-
} catch {
|
|
205
|
-
return null;
|
|
206
|
-
}
|
|
207
|
-
if (!validateTasksJson(tasksData).valid) return null;
|
|
208
|
-
const specContent = readFileSync(specPath, 'utf-8');
|
|
209
|
-
|
|
210
|
-
// Read optional files
|
|
211
|
-
const decisionsPath = join(featureDir, '2-plan/decisions.md');
|
|
212
|
-
const decisionsContent = existsSync(decisionsPath) ? readFileSync(decisionsPath, 'utf-8') : '';
|
|
213
|
-
|
|
214
|
-
const proposalPath = join(featureDir, '0-proposal/proposal.md');
|
|
215
|
-
const proposalContent = existsSync(proposalPath) ? readFileSync(proposalPath, 'utf-8') : '';
|
|
216
|
-
|
|
217
|
-
// Read project config
|
|
218
|
-
let stack = '';
|
|
219
|
-
let architecture = '';
|
|
220
|
-
try {
|
|
221
|
-
const configPath = join(projectPath, '.morph/config/config.json');
|
|
222
|
-
if (existsSync(configPath)) {
|
|
223
|
-
const config = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
224
|
-
stack = config?.project?.stack || '';
|
|
225
|
-
architecture = config?.architecture?.style || '';
|
|
226
|
-
}
|
|
227
|
-
} catch { /* ignore */ }
|
|
228
|
-
|
|
229
|
-
// Parse all inputs. v5: contracts live inside spec.md (no separate
|
|
230
|
-
// contracts.cs/.ts file) — parseContracts scans the spec content.
|
|
231
|
-
const frSections = parseFRSections(specContent);
|
|
232
|
-
const contracts = parseContracts(specContent);
|
|
233
|
-
const decisions = parseDecisions(decisionsContent);
|
|
234
|
-
const taskIds = extractTaskIds(tasksData);
|
|
235
|
-
|
|
236
|
-
// Build mandate
|
|
237
|
-
const lines = [];
|
|
238
|
-
|
|
239
|
-
// Header
|
|
240
|
-
lines.push(`# Implementation Mandate: ${featureName}`);
|
|
241
|
-
lines.push('');
|
|
242
|
-
lines.push(`**Generated:** ${new Date().toISOString().split('T')[0]} | **FRs:** ${frSections.size} | **Tasks:** ${taskIds.length}`);
|
|
243
|
-
if (stack || architecture) {
|
|
244
|
-
lines.push(`**Stack:** ${stack || 'unspecified'} | **Architecture:** ${architecture || 'unspecified'}`);
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
// Overview from proposal
|
|
248
|
-
if (proposalContent) {
|
|
249
|
-
lines.push('');
|
|
250
|
-
lines.push('## Overview');
|
|
251
|
-
// Extract first paragraph after the title
|
|
252
|
-
const paragraphs = proposalContent
|
|
253
|
-
.split(/\n\n+/)
|
|
254
|
-
.filter(p => p.trim() && !p.trim().startsWith('#'))
|
|
255
|
-
.slice(0, 1);
|
|
256
|
-
if (paragraphs.length > 0) {
|
|
257
|
-
lines.push(paragraphs[0].trim());
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
// Architectural constraints
|
|
262
|
-
if (decisions.length > 0) {
|
|
263
|
-
lines.push('');
|
|
264
|
-
lines.push('## Architectural Constraints');
|
|
265
|
-
for (const d of decisions) {
|
|
266
|
-
lines.push(`- ${d}`);
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
// Task sections
|
|
271
|
-
for (const taskId of taskIds) {
|
|
272
|
-
const task = resolveTask(tasksData, taskId);
|
|
273
|
-
if (!task) continue;
|
|
274
|
-
|
|
275
|
-
lines.push('');
|
|
276
|
-
lines.push('---');
|
|
277
|
-
lines.push('');
|
|
278
|
-
lines.push(`## ${taskId}: ${task.title}`);
|
|
279
|
-
|
|
280
|
-
// Metadata line
|
|
281
|
-
const metaParts = [];
|
|
282
|
-
if (task.frs.length > 0) metaParts.push(`**FRs:** ${task.frs.join(', ')}`);
|
|
283
|
-
if (task.files.length > 0) metaParts.push(`**Files:** ${task.files.join(', ')}`);
|
|
284
|
-
if (metaParts.length > 0) lines.push(metaParts.join(' | '));
|
|
285
|
-
|
|
286
|
-
// Acceptance criteria from FR sections
|
|
287
|
-
const allCriteria = [];
|
|
288
|
-
const allFRContracts = [];
|
|
289
|
-
for (const frId of task.frs) {
|
|
290
|
-
const fr = frSections.get(frId);
|
|
291
|
-
if (fr) {
|
|
292
|
-
allCriteria.push(...fr.criteria);
|
|
293
|
-
// Search for contracts in FR content + task title for broader matching
|
|
294
|
-
const searchText = `${fr.title} ${fr.content} ${task.title} ${task.description}`;
|
|
295
|
-
allFRContracts.push(...findRelevantContracts(searchText, contracts));
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
if (allCriteria.length > 0) {
|
|
300
|
-
lines.push('');
|
|
301
|
-
lines.push('### Acceptance Criteria');
|
|
302
|
-
for (const c of allCriteria) {
|
|
303
|
-
lines.push(`- [ ] ${c}`);
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
// Contracts
|
|
308
|
-
const uniqueContracts = [...new Set(allFRContracts)];
|
|
309
|
-
if (uniqueContracts.length > 0) {
|
|
310
|
-
lines.push('');
|
|
311
|
-
lines.push('### Contracts');
|
|
312
|
-
lines.push('```');
|
|
313
|
-
lines.push(uniqueContracts.join('\n'));
|
|
314
|
-
lines.push('```');
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
// Task description as notes
|
|
318
|
-
if (task.description) {
|
|
319
|
-
lines.push('');
|
|
320
|
-
lines.push('### Notes');
|
|
321
|
-
lines.push(task.description);
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
// Verification summary
|
|
326
|
-
lines.push('');
|
|
327
|
-
lines.push('---');
|
|
328
|
-
lines.push('');
|
|
329
|
-
lines.push('## Verification');
|
|
330
|
-
lines.push('```bash');
|
|
331
|
-
if (stack.includes('dotnet')) {
|
|
332
|
-
lines.push('dotnet build');
|
|
333
|
-
lines.push('dotnet test');
|
|
334
|
-
} else {
|
|
335
|
-
lines.push('npm run build');
|
|
336
|
-
lines.push('npm test');
|
|
337
|
-
}
|
|
338
|
-
lines.push('```');
|
|
339
|
-
|
|
340
|
-
const mandate = lines.join('\n');
|
|
341
|
-
|
|
342
|
-
return {
|
|
343
|
-
mandate,
|
|
344
|
-
stats: {
|
|
345
|
-
frCount: frSections.size,
|
|
346
|
-
taskCount: taskIds.length,
|
|
347
|
-
totalChars: mandate.length,
|
|
348
|
-
},
|
|
349
|
-
};
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
/**
|
|
353
|
-
* Extract a single task's section from a generated mandate.
|
|
354
|
-
*
|
|
355
|
-
* @param {string} mandateContent - Full mandate.md content
|
|
356
|
-
* @param {string} taskId - Task ID (e.g., 'T003')
|
|
357
|
-
* @returns {string|null} Task section text or null if not found
|
|
358
|
-
*/
|
|
359
|
-
export function extractTaskSection(mandateContent, taskId) {
|
|
360
|
-
// Find the start of this task's section
|
|
361
|
-
const headerPattern = new RegExp(`^## ${taskId}:`, 'm');
|
|
362
|
-
const headerMatch = headerPattern.exec(mandateContent);
|
|
363
|
-
if (!headerMatch) return null;
|
|
364
|
-
|
|
365
|
-
const start = headerMatch.index;
|
|
366
|
-
|
|
367
|
-
// Find the end: next task header, Verification section, or end of content
|
|
368
|
-
const rest = mandateContent.slice(start + 1);
|
|
369
|
-
const endPatterns = [
|
|
370
|
-
/\n## T\d+:/,
|
|
371
|
-
/\n## Verification/,
|
|
372
|
-
];
|
|
373
|
-
|
|
374
|
-
let end = mandateContent.length;
|
|
375
|
-
for (const pattern of endPatterns) {
|
|
376
|
-
const endMatch = pattern.exec(rest);
|
|
377
|
-
if (endMatch) {
|
|
378
|
-
const candidate = start + 1 + endMatch.index;
|
|
379
|
-
if (candidate < end) end = candidate;
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
// Extract and trim, removing trailing --- separators
|
|
384
|
-
let section = mandateContent.slice(start, end).trim();
|
|
385
|
-
section = section.replace(/\n---\s*$/, '').trim();
|
|
386
|
-
|
|
387
|
-
return section;
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
/**
|
|
391
|
-
* Extract multiple task sections (for agent dispatch).
|
|
392
|
-
*
|
|
393
|
-
* @param {string} mandateContent - Full mandate.md content
|
|
394
|
-
* @param {string[]} taskIds - Task IDs (e.g., ['T001', 'T002', 'T003'])
|
|
395
|
-
* @returns {string} Combined task sections
|
|
396
|
-
*/
|
|
397
|
-
export function extractTaskSections(mandateContent, taskIds) {
|
|
398
|
-
const sections = [];
|
|
399
|
-
for (const taskId of taskIds) {
|
|
400
|
-
const section = extractTaskSection(mandateContent, taskId);
|
|
401
|
-
if (section) sections.push(section);
|
|
402
|
-
}
|
|
403
|
-
return sections.join('\n\n---\n\n');
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
/**
|
|
407
|
-
* Generate a mandate overview (for session start injection).
|
|
408
|
-
* Shows FR list with task mapping and completion status.
|
|
409
|
-
*
|
|
410
|
-
* @param {string} mandateContent - Full mandate.md content
|
|
411
|
-
* @param {Object} taskStatus - Map of taskId → status ('completed'|'in_progress'|'pending')
|
|
412
|
-
* @returns {string} Overview text
|
|
413
|
-
*/
|
|
414
|
-
export function generateMandateOverview(mandateContent, taskStatus = {}) {
|
|
415
|
-
const lines = [];
|
|
416
|
-
|
|
417
|
-
// Extract task-to-FR mapping from mandate
|
|
418
|
-
const taskFRMap = new Map();
|
|
419
|
-
const frTaskMap = new Map();
|
|
420
|
-
|
|
421
|
-
const taskPattern = /^## (T\d+): .+$/gm;
|
|
422
|
-
const frLinePattern = /^\*\*FRs:\*\*\s+(.+?)(?:\s*\||$)/gm;
|
|
423
|
-
|
|
424
|
-
let match;
|
|
425
|
-
let lastTaskId = null;
|
|
426
|
-
|
|
427
|
-
while ((match = taskPattern.exec(mandateContent)) !== null) {
|
|
428
|
-
lastTaskId = match[1];
|
|
429
|
-
// Look for FRs line right after task header
|
|
430
|
-
const afterHeader = mandateContent.slice(match.index + match[0].length, match.index + match[0].length + 200);
|
|
431
|
-
const frMatch = afterHeader.match(/\*\*FRs:\*\*\s+([^\n|]+)/);
|
|
432
|
-
if (frMatch) {
|
|
433
|
-
const frs = frMatch[1].split(',').map(f => f.trim()).filter(Boolean);
|
|
434
|
-
taskFRMap.set(lastTaskId, frs);
|
|
435
|
-
for (const fr of frs) {
|
|
436
|
-
if (!frTaskMap.has(fr)) frTaskMap.set(fr, []);
|
|
437
|
-
frTaskMap.get(fr).push(lastTaskId);
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
// Build overview
|
|
443
|
-
lines.push('Implementation Mandate Overview:');
|
|
444
|
-
|
|
445
|
-
for (const [frId, taskIds] of frTaskMap) {
|
|
446
|
-
const taskStatuses = taskIds.map(tid => {
|
|
447
|
-
const status = taskStatus[tid] || 'pending';
|
|
448
|
-
if (status === 'completed') return `${tid} ✓`;
|
|
449
|
-
if (status === 'in_progress') return `${tid} →`;
|
|
450
|
-
return tid;
|
|
451
|
-
});
|
|
452
|
-
lines.push(` ${frId} → ${taskStatuses.join(', ')}`);
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
// Next task
|
|
456
|
-
const allTaskIds = [...taskFRMap.keys()];
|
|
457
|
-
const nextTask = allTaskIds.find(tid => {
|
|
458
|
-
const status = taskStatus[tid];
|
|
459
|
-
return status === 'in_progress' || !status || status === 'pending';
|
|
460
|
-
});
|
|
461
|
-
if (nextTask) {
|
|
462
|
-
const status = taskStatus[nextTask];
|
|
463
|
-
const verb = status === 'in_progress' ? 'In progress' : 'Next';
|
|
464
|
-
lines.push(` ${verb}: ${nextTask}`);
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
return lines.join('\n');
|
|
468
|
-
}
|