speccrew 0.7.5 → 0.7.6

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.
@@ -15,6 +15,7 @@
15
15
  <field name="completed_dir" required="true" type="string" desc="Marker files output directory"/>
16
16
  <field name="sourceFile" required="true" type="string" desc="Source features JSON file name"/>
17
17
  <field name="language" required="true" type="string" desc="Target language for content"/>
18
+ <field name="workspace_path" required="true" type="string" desc="Workspace root path"/>
18
19
  </block>
19
20
 
20
21
  <!-- ==================== CONSTRAINT RULES ==================== -->
@@ -248,7 +249,7 @@
248
249
  <field name="text">
249
250
  The output document file MUST be created at the EXACT path specified by ${documentPath} input parameter.
250
251
  DO NOT use the template file name (e.g., FEATURE-DETAIL-TEMPLATE-*.md) as the output file name.
251
- The documentPath already contains the correct target path including file name (e.g., speccrew-workspace/knowledges/bizs/backend-system/admin/cache/cache_controller.md).
252
+ The documentPath already contains the correct target path including file name (e.g., speccrew-workspace/knowledges/bizs/backend-fastapi/admin/cache/cache_controller.md).
252
253
  Before creating the file, ensure the parent directory exists (create if necessary).
253
254
  </field>
254
255
  </block>
@@ -478,17 +479,125 @@
478
479
  <field name="verify" value="file.exists(${completed_dir}/${markerName}.done.json)"/>
479
480
  </block>
480
481
 
481
- <!-- Dispatch to Graph Skill for .graph.json -->
482
- <block type="task" id="B32" action="dispatch-to-worker" desc="Dispatch graph generation">
483
- <field name="skill">speccrew-knowledge-bizs-api-graph</field>
484
- <field name="parameters">
485
- <field name="controllerFile">${fileName}</field>
486
- <field name="sourcePath">${sourcePath}</field>
487
- <field name="endpoints">${endpoints}</field>
488
- <field name="completed_dir">${completed_dir}</field>
489
- <field name="markerName">${markerName}</field>
482
+ <!-- Step 7b: Construct and Append Graph Data -->
483
+ <!-- Construct Graph Nodes from API Analysis -->
484
+ <block type="task" id="B32a" action="analyze" desc="Construct API endpoint nodes">
485
+ <field name="endpoints" value="${endpoints}"/>
486
+ <field name="module" value="${module}"/>
487
+ <field name="sourcePath" value="${sourcePath}"/>
488
+ <field name="documentPath" value="${documentPath}"/>
489
+ <field name="output" var="apiNodes"/>
490
+ </block>
491
+
492
+ <block type="task" id="B32b" action="analyze" desc="Construct service nodes">
493
+ <field name="services" value="${services}"/>
494
+ <field name="module" value="${module}"/>
495
+ <field name="sourcePath" value="${sourcePath}"/>
496
+ <field name="documentPath" value="${documentPath}"/>
497
+ <field name="output" var="serviceNodes"/>
498
+ </block>
499
+
500
+ <block type="task" id="B32c" action="analyze" desc="Construct table nodes">
501
+ <field name="tables" value="${databaseTables}"/>
502
+ <field name="module" value="${module}"/>
503
+ <field name="output" var="tableNodes"/>
504
+ </block>
505
+
506
+ <!-- Construct Graph Edges -->
507
+ <block type="task" id="B32d" action="analyze" desc="Construct API-to-Service edges">
508
+ <field name="endpoints" value="${endpoints}"/>
509
+ <field name="module" value="${module}"/>
510
+ <field name="output" var="invokesEdges"/>
511
+ </block>
512
+
513
+ <block type="task" id="B32e" action="analyze" desc="Construct API-to-Table edges">
514
+ <field name="endpoints" value="${endpoints}"/>
515
+ <field name="tables" value="${databaseTables}"/>
516
+ <field name="module" value="${module}"/>
517
+ <field name="output" var="operatesEdges"/>
518
+ </block>
519
+
520
+ <!-- Append Graph Data to nodes.json and edges.json -->
521
+ <block type="task" id="B32f" action="run-script" desc="Append graph nodes to nodes.json">
522
+ <field name="command">node -e "
523
+ const fs = require('fs');
524
+ const path = require('path');
525
+ const graphDir = path.join('${workspace_path}', 'speccrew-workspace', 'knowledges', 'bizs', 'graph');
526
+ const nodesFile = path.join(graphDir, 'nodes.json');
527
+
528
+ // Ensure directory exists
529
+ if (!fs.existsSync(graphDir)) {
530
+ fs.mkdirSync(graphDir, { recursive: true });
531
+ }
532
+
533
+ // Read existing nodes or initialize empty array
534
+ let existingNodes = [];
535
+ if (fs.existsSync(nodesFile)) {
536
+ try {
537
+ existingNodes = JSON.parse(fs.readFileSync(nodesFile, 'utf8'));
538
+ if (!Array.isArray(existingNodes)) existingNodes = [];
539
+ } catch (e) {
540
+ existingNodes = [];
541
+ }
542
+ }
543
+
544
+ // Parse new nodes from input
545
+ const newNodes = JSON.parse('${apiNodes}' || '[]').concat(JSON.parse('${serviceNodes}' || '[]')).concat(JSON.parse('${tableNodes}' || '[]'));
546
+
547
+ // Deduplicate by id
548
+ const nodeMap = new Map();
549
+ existingNodes.forEach(n => nodeMap.set(n.id, n));
550
+ newNodes.forEach(n => nodeMap.set(n.id, n));
551
+
552
+ // Write back
553
+ fs.writeFileSync(nodesFile, JSON.stringify(Array.from(nodeMap.values()), null, 2));
554
+ console.log('Nodes appended:', newNodes.length);
555
+ "
490
556
  </field>
491
557
  </block>
558
+
559
+ <block type="task" id="B32g" action="run-script" desc="Append graph edges to edges.json">
560
+ <field name="command">node -e "
561
+ const fs = require('fs');
562
+ const path = require('path');
563
+ const graphDir = path.join('${workspace_path}', 'speccrew-workspace', 'knowledges', 'bizs', 'graph');
564
+ const edgesFile = path.join(graphDir, 'edges.json');
565
+
566
+ // Read existing edges or initialize empty array
567
+ let existingEdges = [];
568
+ if (fs.existsSync(edgesFile)) {
569
+ try {
570
+ existingEdges = JSON.parse(fs.readFileSync(edgesFile, 'utf8'));
571
+ if (!Array.isArray(existingEdges)) existingEdges = [];
572
+ } catch (e) {
573
+ existingEdges = [];
574
+ }
575
+ }
576
+
577
+ // Parse new edges from input
578
+ const newEdges = JSON.parse('${invokesEdges}' || '[]').concat(JSON.parse('${operatesEdges}' || '[]'));
579
+
580
+ // Deduplicate by composite key (source+target+type)
581
+ const edgeMap = new Map();
582
+ existingEdges.forEach(e => {
583
+ const key = e.source + '|' + e.target + '|' + e.type;
584
+ edgeMap.set(key, e);
585
+ });
586
+ newEdges.forEach(e => {
587
+ const key = e.source + '|' + e.target + '|' + e.type;
588
+ edgeMap.set(key, e);
589
+ });
590
+
591
+ // Write back
592
+ fs.writeFileSync(edgesFile, JSON.stringify(Array.from(edgeMap.values()), null, 2));
593
+ console.log('Edges appended:', newEdges.length);
594
+ "
595
+ </field>
596
+ </block>
597
+
598
+ <block type="checkpoint" id="CP-graph" name="graph-written" desc="Graph data written">
599
+ <field name="verify" value="true"/>
600
+ </block>
492
601
 
493
602
  <block type="event" id="E10" action="log" level="info" desc="Log marker status">
494
603
  <field name="message" value="Step 7 Status: COMPLETED - Marker file written to ${completed_dir}"/>
@@ -1001,11 +1001,11 @@ Requirements:
1001
1001
  "fileName": "UserController",
1002
1002
  "sourcePath": "controller/admin/user/UserController.java",
1003
1003
  "module": "user",
1004
- "documentPath": "speccrew-workspace/knowledges/bizs/backend-system/user/UserController.md",
1004
+ "documentPath": "speccrew-workspace/knowledges/bizs/backend-spring/user/UserController.md",
1005
1005
  "platformType": "backend",
1006
1006
  "platformSubtype": "spring",
1007
- "platformId": "backend-system",
1008
- "sourceFile": "features-backend-system.json"
1007
+ "platformId": "backend-spring",
1008
+ "sourceFile": "features-backend-spring.json"
1009
1009
  }
1010
1010
  ]
1011
1011
  }
@@ -26,7 +26,7 @@ All generated documents must match the user's language. Detect the language from
26
26
 
27
27
  | Parameter | Type | Required | Description |
28
28
  |-----------|------|----------|-------------|
29
- | `platformId` | string | Yes | Platform identifier (e.g., "backend-system", "frontend-web", "mobile-app") |
29
+ | `platformId` | string | Yes | Platform identifier (e.g., "backend-fastapi", "web-vue3", "mobile-uniapp") |
30
30
  | `platformName` | string | Yes | Platform display name |
31
31
  | `platformType` | string | Yes | Platform type: backend, web, mobile |
32
32
  | `platformSubtype` | string | No | Platform subtype (e.g., vue, react, uniapp) |
@@ -0,0 +1,289 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * generate-inventory.js
4
+ *
5
+ * Generate features.json inventory for a single platform.
6
+ * This script is called by speccrew-knowledge-bizs-init-features workflow.
7
+ *
8
+ * Usage:
9
+ * node generate-inventory.js --entryDirsFile <path> --outputDir <path>
10
+ *
11
+ * Arguments:
12
+ * --entryDirsFile Path to entry-dirs JSON file
13
+ * --outputDir Output directory for features.json
14
+ */
15
+
16
+ const fs = require('fs');
17
+ const path = require('path');
18
+
19
+ // Parse command line arguments
20
+ function parseArgs() {
21
+ const args = process.argv.slice(2);
22
+ const parsed = {};
23
+
24
+ for (let i = 0; i < args.length; i++) {
25
+ const arg = args[i];
26
+ if (arg.startsWith('--')) {
27
+ const key = arg.slice(2);
28
+ const value = args[i + 1];
29
+ if (value && !value.startsWith('--')) {
30
+ parsed[key] = value;
31
+ i++;
32
+ } else {
33
+ parsed[key] = true;
34
+ }
35
+ }
36
+ }
37
+
38
+ return parsed;
39
+ }
40
+
41
+ // Generate timestamp in format YYYY-MM-DD-HHMMSS
42
+ function generateTimestamp() {
43
+ const now = new Date();
44
+ const year = now.getFullYear();
45
+ const month = String(now.getMonth() + 1).padStart(2, '0');
46
+ const day = String(now.getDate()).padStart(2, '0');
47
+ const hours = String(now.getHours()).padStart(2, '0');
48
+ const minutes = String(now.getMinutes()).padStart(2, '0');
49
+ const seconds = String(now.getSeconds()).padStart(2, '0');
50
+ return `${year}-${month}-${day}-${hours}${minutes}${seconds}`;
51
+ }
52
+
53
+ // Convert absolute path to project-relative path
54
+ function toRelativePath(absolutePath, projectRoot) {
55
+ // Normalize paths
56
+ const normalizedAbs = path.normalize(absolutePath).replace(/\\/g, '/');
57
+ const normalizedRoot = path.normalize(projectRoot).replace(/\\/g, '/');
58
+
59
+ if (normalizedAbs.startsWith(normalizedRoot)) {
60
+ const relative = normalizedAbs.slice(normalizedRoot.length).replace(/^\/+/, '');
61
+ return relative;
62
+ }
63
+ return normalizedAbs;
64
+ }
65
+
66
+ // Generate document path for a feature
67
+ // Format: speccrew-workspace/knowledges/bizs/{platformId}/{module}/{subpath}/{filename}.md
68
+ function generateDocumentPath(platformId, module, sourcePath, projectRoot) {
69
+ // Extract filename without extension
70
+ const basename = path.basename(sourcePath, path.extname(sourcePath));
71
+
72
+ // Get directory relative to module root
73
+ const relativePath = toRelativePath(sourcePath, projectRoot);
74
+
75
+ // Parse the source path to extract module and subpath
76
+ // Expected format: {platformSourceRoot}/{module}/{subpath}/{file}
77
+ const pathParts = relativePath.split('/');
78
+
79
+ // Find module index
80
+ let moduleIndex = -1;
81
+ for (let i = 0; i < pathParts.length; i++) {
82
+ if (pathParts[i] === module) {
83
+ moduleIndex = i;
84
+ break;
85
+ }
86
+ }
87
+
88
+ // Build subpath (everything between module and filename)
89
+ let subpath = '';
90
+ if (moduleIndex >= 0 && moduleIndex < pathParts.length - 2) {
91
+ // There are directories between module and filename
92
+ subpath = pathParts.slice(moduleIndex + 1, pathParts.length - 1).join('/');
93
+ }
94
+
95
+ // Construct document path using platformId (which follows {platformType}-{techStack} format)
96
+ // e.g., backend-fastapi, web-vue3, mobile-uniapp
97
+ const docPathParts = ['speccrew-workspace', 'knowledges', 'bizs', platformId, module];
98
+
99
+ if (subpath) {
100
+ docPathParts.push(subpath);
101
+ }
102
+
103
+ docPathParts.push(`${basename}.md`);
104
+
105
+ return docPathParts.join('/');
106
+ }
107
+
108
+ // Main function
109
+ function main() {
110
+ const args = parseArgs();
111
+
112
+ // Validate required arguments
113
+ if (!args.entryDirsFile) {
114
+ console.error('Error: --entryDirsFile is required');
115
+ process.exit(1);
116
+ }
117
+
118
+ if (!args.outputDir) {
119
+ console.error('Error: --outputDir is required');
120
+ process.exit(1);
121
+ }
122
+
123
+ const entryDirsFile = path.resolve(args.entryDirsFile);
124
+ const outputDir = path.resolve(args.outputDir);
125
+
126
+ // Check if entry-dirs file exists
127
+ if (!fs.existsSync(entryDirsFile)) {
128
+ console.error(`Error: Entry-dirs file not found: ${entryDirsFile}`);
129
+ process.exit(1);
130
+ }
131
+
132
+ // Read and parse entry-dirs JSON
133
+ let entryDirsData;
134
+ try {
135
+ const content = fs.readFileSync(entryDirsFile, 'utf-8');
136
+ entryDirsData = JSON.parse(content);
137
+ } catch (error) {
138
+ console.error(`Error: Failed to parse entry-dirs file: ${error.message}`);
139
+ process.exit(1);
140
+ }
141
+
142
+ // Validate entry-dirs structure
143
+ if (!entryDirsData.modules || !Array.isArray(entryDirsData.modules) || entryDirsData.modules.length === 0) {
144
+ console.error('Error: entry-dirs JSON must have non-empty modules array');
145
+ process.exit(1);
146
+ }
147
+
148
+ // Extract platform info from entry-dirs data
149
+ const platformId = entryDirsData.platformId || 'unknown-platform';
150
+ const platformName = entryDirsData.platformName || platformId;
151
+ const platformType = entryDirsData.platformType || 'unknown';
152
+ const platformSubtype = entryDirsData.platformSubtype || '';
153
+ const techStack = entryDirsData.techStack || [];
154
+ const sourceRoot = entryDirsData.sourcePath || '';
155
+
156
+ // Project root is the parent of speccrew-workspace
157
+ const projectRoot = path.resolve(outputDir, '..', '..', '..');
158
+
159
+ // Generate features for each module
160
+ const features = [];
161
+ const modules = [];
162
+
163
+ for (const moduleData of entryDirsData.modules) {
164
+ const moduleName = moduleData.name;
165
+ const entryDirs = moduleData.entryDirs || [];
166
+
167
+ let moduleFeatureCount = 0;
168
+
169
+ for (const entryDir of entryDirs) {
170
+ // Scan files in entry directory
171
+ const scanDirectory = (dir) => {
172
+ const files = [];
173
+
174
+ try {
175
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
176
+
177
+ for (const entry of entries) {
178
+ const fullPath = path.join(dir, entry.name);
179
+
180
+ if (entry.isDirectory()) {
181
+ // Recursively scan subdirectories
182
+ files.push(...scanDirectory(fullPath));
183
+ } else if (entry.isFile()) {
184
+ // Check if file is a source file based on platform type
185
+ const ext = path.extname(entry.name).toLowerCase();
186
+ const isSourceFile = isValidSourceFile(ext, platformType);
187
+
188
+ if (isSourceFile) {
189
+ files.push(fullPath);
190
+ }
191
+ }
192
+ }
193
+ } catch (error) {
194
+ console.warn(`Warning: Failed to scan directory ${dir}: ${error.message}`);
195
+ }
196
+
197
+ return files;
198
+ };
199
+
200
+ const sourceFiles = scanDirectory(entryDir);
201
+
202
+ for (const sourceFile of sourceFiles) {
203
+ const relativeSourcePath = toRelativePath(sourceFile, projectRoot);
204
+ const fileName = path.basename(sourceFile, path.extname(sourceFile));
205
+
206
+ // Generate document path using platformId
207
+ const documentPath = generateDocumentPath(platformId, moduleName, sourceFile, projectRoot);
208
+
209
+ features.push({
210
+ fileName: fileName,
211
+ sourcePath: relativeSourcePath,
212
+ documentPath: documentPath,
213
+ module: moduleName,
214
+ analyzed: false,
215
+ startedAt: null,
216
+ completedAt: null,
217
+ analysisNotes: null
218
+ });
219
+
220
+ moduleFeatureCount++;
221
+ }
222
+ }
223
+
224
+ modules.push({
225
+ name: moduleName,
226
+ featureCount: moduleFeatureCount
227
+ });
228
+ }
229
+
230
+ // Build output JSON
231
+ const outputData = {
232
+ platformName: platformName,
233
+ platformType: platformType,
234
+ platformSubtype: platformSubtype,
235
+ platformId: platformId,
236
+ sourcePath: sourceRoot,
237
+ techStack: techStack,
238
+ modules: modules,
239
+ totalFiles: features.length,
240
+ analyzedCount: 0,
241
+ pendingCount: features.length,
242
+ generatedAt: generateTimestamp(),
243
+ features: features
244
+ };
245
+
246
+ // Ensure output directory exists
247
+ if (!fs.existsSync(outputDir)) {
248
+ fs.mkdirSync(outputDir, { recursive: true });
249
+ }
250
+
251
+ // Write output file
252
+ const outputFile = path.join(outputDir, `features-${platformId}.json`);
253
+ try {
254
+ fs.writeFileSync(outputFile, JSON.stringify(outputData, null, 2), 'utf-8');
255
+ console.log(`Generated: ${outputFile}`);
256
+ console.log(` Platform: ${platformName} (${platformId})`);
257
+ console.log(` Type: ${platformType}${platformSubtype ? '/' + platformSubtype : ''}`);
258
+ console.log(` Features: ${features.length}`);
259
+ console.log(` Modules: ${modules.map(m => m.name).join(', ')}`);
260
+ } catch (error) {
261
+ console.error(`Error: Failed to write output file: ${error.message}`);
262
+ process.exit(1);
263
+ }
264
+ }
265
+
266
+ // Check if file extension is valid for the platform type
267
+ function isValidSourceFile(ext, platformType) {
268
+ const backendExts = ['.java', '.kt', '.py', '.go', '.rs', '.cs', '.php', '.rb'];
269
+ const webExts = ['.vue', '.tsx', '.jsx', '.ts', '.js', '.svelte'];
270
+ const mobileExts = ['.vue', '.tsx', '.jsx', '.ts', '.js', '.dart', '.swift', '.kt', '.java'];
271
+ const desktopExts = ['.vue', '.tsx', '.jsx', '.ts', '.js', '.cs', '.xaml'];
272
+
273
+ switch (platformType) {
274
+ case 'backend':
275
+ return backendExts.includes(ext);
276
+ case 'web':
277
+ return webExts.includes(ext);
278
+ case 'mobile':
279
+ return mobileExts.includes(ext);
280
+ case 'desktop':
281
+ return desktopExts.includes(ext);
282
+ default:
283
+ // Accept all common source file extensions
284
+ return [...backendExts, ...webExts, ...mobileExts, ...desktopExts].includes(ext);
285
+ }
286
+ }
287
+
288
+ // Run main function
289
+ main();
@@ -3,7 +3,7 @@
3
3
  * reindex-modules.js - Deterministic script to re-extract module names from existing features JSON with updated exclude_dirs
4
4
  *
5
5
  * Usage:
6
- * node reindex-modules.js --featuresFile "path/to/features-backend-system.json" --projectRoot "d:\dev\ruoyi-vue-pro"
6
+ * node reindex-modules.js --featuresFile "path/to/features-backend-fastapi.json" --projectRoot "d:\dev\ruoyi-vue-pro"
7
7
  *
8
8
  * Optional parameters:
9
9
  * --platformType "backend" - If not provided, read from features JSON's platformType field
@@ -376,12 +376,179 @@
376
376
  <block type="checkpoint" id="CP7" name="marker-written" desc="Marker file written">
377
377
  <field name="verify" value="file.exists(${completed_dir}/${markerName}.done.json)"/>
378
378
  </block>
379
-
379
+
380
+ <!-- Step 7b: Construct and Append Graph Data -->
381
+ <!-- Construct Graph Nodes from UI Analysis -->
382
+ <block type="task" id="B32a" action="analyze" desc="Construct page node">
383
+ <field name="type" value="page"/>
384
+ <field name="id" value="page-${module}-${fileName}"/>
385
+ <field name="name" value="${fileName}"/>
386
+ <field name="module" value="${module}"/>
387
+ <field name="sourcePath" value="${sourcePath}"/>
388
+ <field name="documentPath" value="${documentPath}"/>
389
+ <field name="platform" value="${platform_type}-${platform_subtype}"/>
390
+ <field name="output" var="pageNode"/>
391
+ </block>
392
+
393
+ <block type="task" id="B32b" action="analyze" desc="Construct component nodes">
394
+ <field name="components" value="${analysisResult.components}"/>
395
+ <field name="module" value="${module}"/>
396
+ <field name="documentPath" value="${documentPath}"/>
397
+ <field name="output" var="componentNodes"/>
398
+ </block>
399
+
400
+ <block type="task" id="B32c" action="analyze" desc="Construct route nodes">
401
+ <field name="routes" value="${analysisResult.routes}"/>
402
+ <field name="module" value="${module}"/>
403
+ <field name="sourcePath" value="${sourcePath}"/>
404
+ <field name="output" var="routeNodes"/>
405
+ </block>
406
+
407
+ <!-- Construct Graph Edges -->
408
+ <block type="task" id="B32d" action="analyze" desc="Construct navigates edges">
409
+ <field name="pageId" value="page-${module}-${fileName}"/>
410
+ <field name="navigations" value="${analysisResult.navigations}"/>
411
+ <field name="module" value="${module}"/>
412
+ <field name="output" var="navigatesEdges"/>
413
+ </block>
414
+
415
+ <block type="task" id="B32e" action="analyze" desc="Construct contains edges">
416
+ <field name="pageId" value="page-${module}-${fileName}"/>
417
+ <field name="components" value="${analysisResult.components}"/>
418
+ <field name="module" value="${module}"/>
419
+ <field name="output" var="containsEdges"/>
420
+ </block>
421
+
422
+ <block type="task" id="B32f" action="analyze" desc="Construct calls-api edges">
423
+ <field name="pageId" value="page-${module}-${fileName}"/>
424
+ <field name="apiCalls" value="${analysisResult.apis}"/>
425
+ <field name="module" value="${module}"/>
426
+ <field name="output" var="callsApiEdges"/>
427
+ </block>
428
+
429
+ <!-- Append Graph Data to nodes.json and edges.json -->
430
+ <block type="task" id="B32g" action="run-script" desc="Append graph nodes to nodes.json">
431
+ <field name="command">node -e "
432
+ const fs = require('fs');
433
+ const path = require('path');
434
+ const graphDir = path.join('${workspace_path}', 'speccrew-workspace', 'knowledges', 'bizs', 'graph');
435
+ const nodesFile = path.join(graphDir, 'nodes.json');
436
+
437
+ // Ensure directory exists
438
+ if (!fs.existsSync(graphDir)) {
439
+ fs.mkdirSync(graphDir, { recursive: true });
440
+ }
441
+
442
+ // Read existing nodes or initialize empty array
443
+ let existingNodes = [];
444
+ if (fs.existsSync(nodesFile)) {
445
+ try {
446
+ existingNodes = JSON.parse(fs.readFileSync(nodesFile, 'utf8'));
447
+ if (!Array.isArray(existingNodes)) existingNodes = [];
448
+ } catch (e) {
449
+ existingNodes = [];
450
+ }
451
+ }
452
+
453
+ // Parse new nodes from input
454
+ const newNodes = [];
455
+ if ('${pageNode}') {
456
+ try {
457
+ const page = JSON.parse('${pageNode}');
458
+ if (page && page.id) newNodes.push(page);
459
+ } catch (e) {}
460
+ }
461
+ if ('${componentNodes}') {
462
+ try {
463
+ const comps = JSON.parse('${componentNodes}');
464
+ if (Array.isArray(comps)) newNodes.push(...comps);
465
+ } catch (e) {}
466
+ }
467
+ if ('${routeNodes}') {
468
+ try {
469
+ const routes = JSON.parse('${routeNodes}');
470
+ if (Array.isArray(routes)) newNodes.push(...routes);
471
+ } catch (e) {}
472
+ }
473
+
474
+ // Deduplicate by id
475
+ const nodeMap = new Map();
476
+ existingNodes.forEach(n => nodeMap.set(n.id, n));
477
+ newNodes.forEach(n => nodeMap.set(n.id, n));
478
+
479
+ // Write back
480
+ fs.writeFileSync(nodesFile, JSON.stringify(Array.from(nodeMap.values()), null, 2));
481
+ console.log('Nodes appended:', newNodes.length);
482
+ "
483
+ </field>
484
+ </block>
485
+
486
+ <block type="task" id="B32h" action="run-script" desc="Append graph edges to edges.json">
487
+ <field name="command">node -e "
488
+ const fs = require('fs');
489
+ const path = require('path');
490
+ const graphDir = path.join('${workspace_path}', 'speccrew-workspace', 'knowledges', 'bizs', 'graph');
491
+ const edgesFile = path.join(graphDir, 'edges.json');
492
+
493
+ // Read existing edges or initialize empty array
494
+ let existingEdges = [];
495
+ if (fs.existsSync(edgesFile)) {
496
+ try {
497
+ existingEdges = JSON.parse(fs.readFileSync(edgesFile, 'utf8'));
498
+ if (!Array.isArray(existingEdges)) existingEdges = [];
499
+ } catch (e) {
500
+ existingEdges = [];
501
+ }
502
+ }
503
+
504
+ // Parse new edges from input
505
+ const newEdges = [];
506
+ if ('${navigatesEdges}') {
507
+ try {
508
+ const edges = JSON.parse('${navigatesEdges}');
509
+ if (Array.isArray(edges)) newEdges.push(...edges);
510
+ } catch (e) {}
511
+ }
512
+ if ('${containsEdges}') {
513
+ try {
514
+ const edges = JSON.parse('${containsEdges}');
515
+ if (Array.isArray(edges)) newEdges.push(...edges);
516
+ } catch (e) {}
517
+ }
518
+ if ('${callsApiEdges}') {
519
+ try {
520
+ const edges = JSON.parse('${callsApiEdges}');
521
+ if (Array.isArray(edges)) newEdges.push(...edges);
522
+ } catch (e) {}
523
+ }
524
+
525
+ // Deduplicate by composite key (source+target+type)
526
+ const edgeMap = new Map();
527
+ existingEdges.forEach(e => {
528
+ const key = (e.source || '') + '|' + (e.target || '') + '|' + (e.type || '');
529
+ edgeMap.set(key, e);
530
+ });
531
+ newEdges.forEach(e => {
532
+ const key = (e.source || '') + '|' + (e.target || '') + '|' + (e.type || '');
533
+ edgeMap.set(key, e);
534
+ });
535
+
536
+ // Write back
537
+ fs.writeFileSync(edgesFile, JSON.stringify(Array.from(edgeMap.values()), null, 2));
538
+ console.log('Edges appended:', newEdges.length);
539
+ "
540
+ </field>
541
+ </block>
542
+
543
+ <block type="checkpoint" id="CP-graph" name="graph-written" desc="Graph data written">
544
+ <field name="verify" value="true"/>
545
+ </block>
546
+
380
547
  <block type="event" id="E10" action="log" level="info" desc="Log marker status">
381
- <field name="message" value="Step 7 Status: COMPLETED - Done marker file written to ${completed_dir}/${markerName}.done.json"/>
548
+ <field name="message" value="Step 7 Status: COMPLETED - Done marker and graph data written to ${completed_dir}/${markerName}.done.json"/>
382
549
  </block>
383
550
 
384
- <!-- ==================== FINAL OUTPUT ==================== -->
551
+ <!-- ==================== FINAL OUTPUT ================= === -->
385
552
  <block type="output" id="O1" desc="UI feature analysis output results">
386
553
  <field name="status" value="success"/>
387
554
  <field name="feature_name" from="${fileName}"/>
@@ -25,7 +25,7 @@ Dispatch Agent (speccrew-knowledge-dispatch)
25
25
  | Variable | Type | Description | Example |
26
26
  |----------|------|-------------|---------|
27
27
  | `{{action}}` | string | Write action to perform | `"batch-write"`, `"init-module"`, `"update-node"`, `"remove-node"` |
28
- | `{{platformId}}` | string | Platform identifier for directory segregation | `"backend-system"`, `"backend-ai"`, `"web-vue"`, `"mobile-uniapp"` |
28
+ | `{{platformId}}` | string | Platform identifier for directory segregation | `"backend-fastapi"`, `"backend-spring"`, `"web-vue"`, `"mobile-uniapp"` |
29
29
  | `{{module}}` | string | Target business module | `"system"`, `"trade"`, `"infra"` |
30
30
  | `{{graphData}}` | object | Graph data from skill output (for batch-write) | `{ "nodes": [...], "edges": [...] }` |
31
31
  | `{{nodeId}}` | string | Node ID (for update-node / remove-node) | `"api-system-user-list"` |
@@ -39,7 +39,7 @@ Generate analyze task plan for a single business module. Reads features-*.json,
39
39
  | `features_file` | string | Yes | Path to the platform's features-{platform}.json file |
40
40
  | `output_path` | string | Yes | Knowledge base output root path (e.g., speccrew-workspace/knowledges) |
41
41
  | `completed_dir` | string | Yes | Marker file output directory for api-analyze .done.json markers. Value from PM Agent: `{sync_state_bizs_dir}/completed` |
42
- | `sourceFile` | string | Yes | Features JSON filename (e.g., "features-backend-system.json"), used for api-analyze marking |
42
+ | `sourceFile` | string | Yes | Features JSON filename (e.g., "features-backend-fastapi.json"), used for api-analyze marking |
43
43
  | `language` | string | Yes | Output language (zh / en) |
44
44
  | `workspace_path` | string | Yes | Workspace root path for constructing absolute paths |
45
45
 
@@ -14,7 +14,7 @@
14
14
  <field name="features_file" required="true" type="string" desc="Path to the platform's features-{platform}.json file"/>
15
15
  <field name="output_path" required="true" type="string" desc="Knowledge base output root path"/>
16
16
  <field name="completed_dir" required="true" type="string" desc="Marker file output directory for api-analyze .done.json markers"/>
17
- <field name="sourceFile" required="true" type="string" desc="Features JSON filename (e.g., features-backend-system.json)"/>
17
+ <field name="sourceFile" required="true" type="string" desc="Features JSON filename (e.g., features-backend-fastapi.json)"/>
18
18
  <field name="language" required="true" type="string" desc="Output language (zh / en)"/>
19
19
  <field name="workspace_path" required="true" type="string" desc="Workspace root path for constructing absolute paths"/>
20
20
  </block>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "speccrew",
3
- "version": "0.7.5",
3
+ "version": "0.7.6",
4
4
  "description": "Spec-Driven Development toolkit for AI-powered IDEs",
5
5
  "author": "charlesmu99",
6
6
  "repository": {