speccrew 0.7.10 → 0.7.11

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.
@@ -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
 
@@ -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}/{filename}.md
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
- // Extract filename without extension
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(`${basename}.md`);
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 document path using platformId
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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "speccrew",
3
- "version": "0.7.10",
3
+ "version": "0.7.11",
4
4
  "description": "Spec-Driven Development toolkit for AI-powered IDEs",
5
5
  "author": "charlesmu99",
6
6
  "repository": {