speccrew 0.7.10 → 0.7.12
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/.speccrew/skills/speccrew-knowledge-bizs-api-analyze/workflow.agentflow.xml +6 -0
- package/.speccrew/skills/speccrew-knowledge-bizs-dispatch/scripts/batch-orchestrator.js +88 -2
- package/.speccrew/skills/speccrew-knowledge-bizs-init-features/scripts/generate-inventory.js +43 -7
- package/.speccrew/skills/speccrew-knowledge-bizs-ui-analyze/workflow.agentflow.xml +6 -0
- package/package.json +1 -1
|
@@ -489,6 +489,8 @@
|
|
|
489
489
|
<field name="module" value="${module}"/>
|
|
490
490
|
<field name="sourcePath" value="${sourcePath}"/>
|
|
491
491
|
<field name="documentPath" value="${documentPath}"/>
|
|
492
|
+
<field name="text">Each node MUST have: id (string, unique), type (string), label (string, human-readable name, REQUIRED), properties (object)</field>
|
|
493
|
+
<field name="text">Example node: {"id": "api-dept-list", "type": "api-endpoint", "label": "GET /system/dept/list - 获取部门列表", "properties": {"method": "GET", "path": "/system/dept/list"}}</field>
|
|
492
494
|
<field name="output" var="apiNodes"/>
|
|
493
495
|
</block>
|
|
494
496
|
|
|
@@ -497,12 +499,16 @@
|
|
|
497
499
|
<field name="module" value="${module}"/>
|
|
498
500
|
<field name="sourcePath" value="${sourcePath}"/>
|
|
499
501
|
<field name="documentPath" value="${documentPath}"/>
|
|
502
|
+
<field name="text">Each node MUST have: id (string, unique), type (string), label (string, human-readable name, REQUIRED), properties (object)</field>
|
|
503
|
+
<field name="text">Example node: {"id": "svc-dept", "type": "service", "label": "DeptService - 部门服务", "properties": {"className": "DeptService", "methods": ["list", "get", "save", "delete"]}}</field>
|
|
500
504
|
<field name="output" var="serviceNodes"/>
|
|
501
505
|
</block>
|
|
502
506
|
|
|
503
507
|
<block type="task" id="B32c" action="analyze" desc="Construct table nodes">
|
|
504
508
|
<field name="tables" value="${databaseTables}"/>
|
|
505
509
|
<field name="module" value="${module}"/>
|
|
510
|
+
<field name="text">Each node MUST have: id (string, unique), type (string), label (string, human-readable name, REQUIRED), properties (object)</field>
|
|
511
|
+
<field name="text">Example node: {"id": "tbl-sys-dept", "type": "database-table", "label": "sys_dept - 部门表", "properties": {"tableName": "sys_dept", "schema": "public"}}</field>
|
|
506
512
|
<field name="output" var="tableNodes"/>
|
|
507
513
|
</block>
|
|
508
514
|
|
|
@@ -155,6 +155,78 @@ function getBatch(args) {
|
|
|
155
155
|
}
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
+
// Parse featureId to extract platformId, module, and fileName
|
|
159
|
+
// featureId format: {platformId}-{module}-{fileNameWithoutExt}
|
|
160
|
+
function parseFeatureId(featureId) {
|
|
161
|
+
const parts = featureId.split('-');
|
|
162
|
+
if (parts.length >= 3) {
|
|
163
|
+
const platformId = parts[0];
|
|
164
|
+
const module = parts[1];
|
|
165
|
+
const fileNameWithoutExt = parts.slice(2).join('-'); // Handle fileName with hyphens
|
|
166
|
+
return { platformId, module, fileNameWithoutExt };
|
|
167
|
+
}
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Update features JSON file with analysis results
|
|
172
|
+
function updateFeaturesJson(syncStatePath, featureId, doneData) {
|
|
173
|
+
const parsed = parseFeatureId(featureId);
|
|
174
|
+
if (!parsed) {
|
|
175
|
+
console.error(JSON.stringify({ warning: `Cannot parse featureId: ${featureId}` }));
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const { platformId, module, fileNameWithoutExt } = parsed;
|
|
180
|
+
const featuresFilePath = path.join(syncStatePath, `features-${platformId}.json`);
|
|
181
|
+
|
|
182
|
+
if (!fs.existsSync(featuresFilePath)) {
|
|
183
|
+
console.error(JSON.stringify({ warning: `Features file not found: ${featuresFilePath}` }));
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const featuresData = readJsonSafe(featuresFilePath);
|
|
188
|
+
if (!featuresData || !Array.isArray(featuresData.features)) {
|
|
189
|
+
console.error(JSON.stringify({ warning: `Invalid features data in: ${featuresFilePath}` }));
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Find matching feature
|
|
194
|
+
let found = false;
|
|
195
|
+
for (const feature of featuresData.features) {
|
|
196
|
+
const featureFileName = feature.fileName || feature.sourceFile || 'unknown';
|
|
197
|
+
const featureFileNameWithoutExt = featureFileName.replace(/\.[^.]+$/, '');
|
|
198
|
+
|
|
199
|
+
// Match by module and fileName (platformId already matched by filename)
|
|
200
|
+
if (feature.module === module && featureFileNameWithoutExt === fileNameWithoutExt) {
|
|
201
|
+
// Skip if already analyzed (idempotency)
|
|
202
|
+
if (feature.analyzed === true) {
|
|
203
|
+
found = true;
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Update feature fields
|
|
208
|
+
feature.analyzed = true;
|
|
209
|
+
feature.startedAt = doneData.startedAt || doneData.timestamp || null;
|
|
210
|
+
feature.completedAt = doneData.completedAt || doneData.timestamp || null;
|
|
211
|
+
feature.analysisNotes = doneData.notes || 'completed';
|
|
212
|
+
found = true;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (!found) {
|
|
217
|
+
console.error(JSON.stringify({ warning: `Feature not found in ${featuresFilePath}: ${featureId}` }));
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Update top-level counts
|
|
222
|
+
featuresData.analyzedCount = featuresData.features.filter(f => f.analyzed).length;
|
|
223
|
+
featuresData.pendingCount = featuresData.features.filter(f => !f.analyzed).length;
|
|
224
|
+
|
|
225
|
+
// Write back to file (atomic write)
|
|
226
|
+
fs.writeFileSync(featuresFilePath, JSON.stringify(featuresData, null, 2));
|
|
227
|
+
return true;
|
|
228
|
+
}
|
|
229
|
+
|
|
158
230
|
// process-results subcommand
|
|
159
231
|
function processResults(args) {
|
|
160
232
|
const { syncStatePath, graphRoot, completedDir } = args;
|
|
@@ -165,9 +237,10 @@ function processResults(args) {
|
|
|
165
237
|
let success = 0;
|
|
166
238
|
let failed = 0;
|
|
167
239
|
let graphUpdated = false;
|
|
240
|
+
let featuresUpdated = 0;
|
|
168
241
|
|
|
169
242
|
if (!fs.existsSync(completedDir)) {
|
|
170
|
-
console.log(JSON.stringify({ success, failed, graphUpdated }));
|
|
243
|
+
console.log(JSON.stringify({ success, failed, graphUpdated, featuresUpdated }));
|
|
171
244
|
return;
|
|
172
245
|
}
|
|
173
246
|
|
|
@@ -178,14 +251,26 @@ function processResults(args) {
|
|
|
178
251
|
for (const file of doneFiles) {
|
|
179
252
|
const filePath = path.join(completedDir, file);
|
|
180
253
|
const data = readJsonSafe(filePath);
|
|
254
|
+
|
|
255
|
+
// Extract featureId from filename: {featureId}.done.json
|
|
256
|
+
const featureId = file.replace('.done.json', '');
|
|
257
|
+
|
|
181
258
|
if (data) {
|
|
182
259
|
if (data.status === 'success' || data.status === 'completed') {
|
|
183
260
|
success++;
|
|
261
|
+
// Update features JSON for successful analysis
|
|
262
|
+
if (updateFeaturesJson(syncStatePath, featureId, data)) {
|
|
263
|
+
featuresUpdated++;
|
|
264
|
+
}
|
|
184
265
|
} else if (data.status === 'failed' || data.status === 'error') {
|
|
185
266
|
failed++;
|
|
186
267
|
} else {
|
|
187
268
|
// Default to success if no status field
|
|
188
269
|
success++;
|
|
270
|
+
// Update features JSON for successful analysis
|
|
271
|
+
if (updateFeaturesJson(syncStatePath, featureId, data)) {
|
|
272
|
+
featuresUpdated++;
|
|
273
|
+
}
|
|
189
274
|
}
|
|
190
275
|
} else {
|
|
191
276
|
// Invalid JSON, assume success
|
|
@@ -260,7 +345,8 @@ function processResults(args) {
|
|
|
260
345
|
console.log(JSON.stringify({
|
|
261
346
|
success,
|
|
262
347
|
failed,
|
|
263
|
-
graphUpdated
|
|
348
|
+
graphUpdated,
|
|
349
|
+
featuresUpdated
|
|
264
350
|
}));
|
|
265
351
|
}
|
|
266
352
|
|
package/.speccrew/skills/speccrew-knowledge-bizs-init-features/scripts/generate-inventory.js
CHANGED
|
@@ -63,13 +63,25 @@ function toRelativePath(absolutePath, projectRoot) {
|
|
|
63
63
|
return normalizedAbs;
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
// Generate unique feature ID from source path
|
|
67
|
+
// Converts path separators to hyphens, removes extension
|
|
68
|
+
// e.g., 'src/views/bpm/form/data.ts' -> 'src-views-bpm-form-data'
|
|
69
|
+
// e.g., 'ruoyi-fastapi-app/src/pages/common/agreement/index.vue' -> 'ruoyi-fastapi-app-src-pages-common-agreement-index'
|
|
70
|
+
function generateFeatureId(relativeSourcePath) {
|
|
71
|
+
// Remove file extension
|
|
72
|
+
const pathWithoutExt = relativeSourcePath.replace(/\.[^.]+$/, '');
|
|
73
|
+
|
|
74
|
+
// Replace path separators with hyphens
|
|
75
|
+
const id = pathWithoutExt.replace(/\//g, '-').replace(/\\/g, '-');
|
|
76
|
+
|
|
77
|
+
return id;
|
|
78
|
+
}
|
|
79
|
+
|
|
66
80
|
// Generate document path for a feature
|
|
67
|
-
// Format: speccrew-workspace/knowledges/bizs/{platformId}/{module}/{subpath}/{
|
|
81
|
+
// Format: speccrew-workspace/knowledges/bizs/{platformId}/{module}/{subpath}/{uniqueName}.md
|
|
82
|
+
// The documentPath must be unique to avoid overwriting
|
|
68
83
|
function generateDocumentPath(platformId, module, sourcePath, projectRoot) {
|
|
69
|
-
//
|
|
70
|
-
const basename = path.basename(sourcePath, path.extname(sourcePath));
|
|
71
|
-
|
|
72
|
-
// Get directory relative to module root
|
|
84
|
+
// Get the relative path from project root
|
|
73
85
|
const relativePath = toRelativePath(sourcePath, projectRoot);
|
|
74
86
|
|
|
75
87
|
// Parse the source path to extract module and subpath
|
|
@@ -92,6 +104,22 @@ function generateDocumentPath(platformId, module, sourcePath, projectRoot) {
|
|
|
92
104
|
subpath = pathParts.slice(moduleIndex + 1, pathParts.length - 1).join('/');
|
|
93
105
|
}
|
|
94
106
|
|
|
107
|
+
// Extract filename without extension
|
|
108
|
+
const basename = path.basename(sourcePath, path.extname(sourcePath));
|
|
109
|
+
|
|
110
|
+
// Generate unique document name to avoid collisions
|
|
111
|
+
// Use the path after module to create a unique name
|
|
112
|
+
let uniqueDocName;
|
|
113
|
+
if (moduleIndex >= 0 && moduleIndex < pathParts.length - 1) {
|
|
114
|
+
// Get all path parts after module (excluding extension)
|
|
115
|
+
const pathAfterModule = pathParts.slice(moduleIndex + 1);
|
|
116
|
+
// Join with hyphens to create unique name
|
|
117
|
+
uniqueDocName = pathAfterModule.join('-').replace(/\.[^.]+$/, '');
|
|
118
|
+
} else {
|
|
119
|
+
// Fallback: use basename
|
|
120
|
+
uniqueDocName = basename;
|
|
121
|
+
}
|
|
122
|
+
|
|
95
123
|
// Construct document path using platformId (which follows {platformType}-{techStack} format)
|
|
96
124
|
// e.g., backend-fastapi, web-vue3, mobile-uniapp
|
|
97
125
|
const docPathParts = ['speccrew-workspace', 'knowledges', 'bizs', platformId, module];
|
|
@@ -100,7 +128,7 @@ function generateDocumentPath(platformId, module, sourcePath, projectRoot) {
|
|
|
100
128
|
docPathParts.push(subpath);
|
|
101
129
|
}
|
|
102
130
|
|
|
103
|
-
docPathParts.push(`${
|
|
131
|
+
docPathParts.push(`${uniqueDocName}.md`);
|
|
104
132
|
|
|
105
133
|
return docPathParts.join('/');
|
|
106
134
|
}
|
|
@@ -203,10 +231,14 @@ function main() {
|
|
|
203
231
|
const relativeSourcePath = toRelativePath(sourceFile, projectRoot);
|
|
204
232
|
const fileName = path.basename(sourceFile, path.extname(sourceFile));
|
|
205
233
|
|
|
206
|
-
// Generate
|
|
234
|
+
// Generate unique feature ID based on full relative path
|
|
235
|
+
const featureId = generateFeatureId(relativeSourcePath);
|
|
236
|
+
|
|
237
|
+
// Generate document path using platformId (now with unique filename)
|
|
207
238
|
const documentPath = generateDocumentPath(platformId, moduleName, sourceFile, projectRoot);
|
|
208
239
|
|
|
209
240
|
features.push({
|
|
241
|
+
id: featureId,
|
|
210
242
|
fileName: fileName,
|
|
211
243
|
sourcePath: relativeSourcePath,
|
|
212
244
|
documentPath: documentPath,
|
|
@@ -227,6 +259,9 @@ function main() {
|
|
|
227
259
|
});
|
|
228
260
|
}
|
|
229
261
|
|
|
262
|
+
// Determine analysis method based on platform type
|
|
263
|
+
const analysisMethod = platformType === 'backend' ? 'api-based' : 'ui-based';
|
|
264
|
+
|
|
230
265
|
// Build output JSON
|
|
231
266
|
const outputData = {
|
|
232
267
|
platformName: platformName,
|
|
@@ -235,6 +270,7 @@ function main() {
|
|
|
235
270
|
platformId: platformId,
|
|
236
271
|
sourcePath: sourceRoot,
|
|
237
272
|
techStack: techStack,
|
|
273
|
+
analysisMethod: analysisMethod,
|
|
238
274
|
modules: modules,
|
|
239
275
|
totalFiles: features.length,
|
|
240
276
|
analyzedCount: 0,
|
|
@@ -395,6 +395,8 @@
|
|
|
395
395
|
<field name="sourcePath" value="${sourcePath}"/>
|
|
396
396
|
<field name="documentPath" value="${documentPath}"/>
|
|
397
397
|
<field name="platform" value="${platform_type}-${platform_subtype}"/>
|
|
398
|
+
<field name="text">Each node MUST have: id (string, unique), type (string), label (string, human-readable name, REQUIRED), properties (object)</field>
|
|
399
|
+
<field name="text">Example node: {"id": "page-system-user", "type": "page", "label": "用户管理页面 - User Management", "properties": {"route": "/system/user", "platform": "web-vue"}}</field>
|
|
398
400
|
<field name="output" var="pageNode"/>
|
|
399
401
|
</block>
|
|
400
402
|
|
|
@@ -402,6 +404,8 @@
|
|
|
402
404
|
<field name="components" value="${analysisResult.components}"/>
|
|
403
405
|
<field name="module" value="${module}"/>
|
|
404
406
|
<field name="documentPath" value="${documentPath}"/>
|
|
407
|
+
<field name="text">Each node MUST have: id (string, unique), type (string), label (string, human-readable name, REQUIRED), properties (object)</field>
|
|
408
|
+
<field name="text">Example node: {"id": "comp-user-table", "type": "component", "label": "UserTable - 用户列表组件", "properties": {"componentName": "UserTable", "props": ["data", "loading"]}}</field>
|
|
405
409
|
<field name="output" var="componentNodes"/>
|
|
406
410
|
</block>
|
|
407
411
|
|
|
@@ -409,6 +413,8 @@
|
|
|
409
413
|
<field name="routes" value="${analysisResult.routes}"/>
|
|
410
414
|
<field name="module" value="${module}"/>
|
|
411
415
|
<field name="sourcePath" value="${sourcePath}"/>
|
|
416
|
+
<field name="text">Each node MUST have: id (string, unique), type (string), label (string, human-readable name, REQUIRED), properties (object)</field>
|
|
417
|
+
<field name="text">Example node: {"id": "route-user-edit", "type": "route", "label": "/system/user/edit - 用户编辑路由", "properties": {"path": "/system/user/edit", "name": "UserEdit"}}</field>
|
|
412
418
|
<field name="output" var="routeNodes"/>
|
|
413
419
|
</block>
|
|
414
420
|
|