speccrew 0.1.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/.speccrew/agents/speccrew-feature-designer.md +142 -0
- package/.speccrew/agents/speccrew-product-manager.md +61 -0
- package/.speccrew/agents/speccrew-system-designer.md +200 -0
- package/.speccrew/agents/speccrew-system-developer.md +238 -0
- package/.speccrew/agents/speccrew-task-worker.md +80 -0
- package/.speccrew/agents/speccrew-team-leader.md +92 -0
- package/.speccrew/agents/speccrew-test-manager.md +313 -0
- package/.speccrew/skills/speccrew-create-agents/SKILL.md +98 -0
- package/.speccrew/skills/speccrew-create-agents/templates/agents/designer-agent.md +54 -0
- package/.speccrew/skills/speccrew-create-agents/templates/agents/dev-agent.md +79 -0
- package/.speccrew/skills/speccrew-create-agents/templates/agents/test-agent.md +80 -0
- package/.speccrew/skills/speccrew-dev-backend/SKILL.md +205 -0
- package/.speccrew/skills/speccrew-dev-backend/templates/TASK-RECORD-TEMPLATE.md +118 -0
- package/.speccrew/skills/speccrew-dev-desktop/SKILL.md +258 -0
- package/.speccrew/skills/speccrew-dev-desktop/templates/TASK-RECORD-TEMPLATE.md +161 -0
- package/.speccrew/skills/speccrew-dev-frontend/SKILL.md +202 -0
- package/.speccrew/skills/speccrew-dev-frontend/templates/TASK-RECORD-TEMPLATE.md +115 -0
- package/.speccrew/skills/speccrew-dev-mobile/SKILL.md +200 -0
- package/.speccrew/skills/speccrew-dev-mobile/templates/TASK-RECORD-TEMPLATE.md +125 -0
- package/.speccrew/skills/speccrew-fd-api-contract/SKILL.md +73 -0
- package/.speccrew/skills/speccrew-fd-api-contract/templates/API-CONTRACT-TEMPLATE.md +96 -0
- package/.speccrew/skills/speccrew-fd-feature-design/SKILL.md +395 -0
- package/.speccrew/skills/speccrew-fd-feature-design/templates/FEATURE-SPEC-TEMPLATE.md +387 -0
- package/.speccrew/skills/speccrew-get-timestamp/SKILL.md +80 -0
- package/.speccrew/skills/speccrew-get-timestamp/scripts/get-timestamp.js +35 -0
- package/.speccrew/skills/speccrew-knowledge-bizs-api-analyze/SKILL.md +1116 -0
- package/.speccrew/skills/speccrew-knowledge-bizs-api-analyze/templates/FEATURE-DETAIL-TEMPLATE-FASTAPI.md +462 -0
- package/.speccrew/skills/speccrew-knowledge-bizs-api-analyze/templates/FEATURE-DETAIL-TEMPLATE-JAVA.md +480 -0
- package/.speccrew/skills/speccrew-knowledge-bizs-api-analyze/templates/FEATURE-DETAIL-TEMPLATE-NET.md +464 -0
- package/.speccrew/skills/speccrew-knowledge-bizs-api-analyze/templates/FEATURE-DETAIL-TEMPLATE.md +480 -0
- package/.speccrew/skills/speccrew-knowledge-bizs-api-analyze/templates/MODULE-OVERVIEW-TEMPLATE.md +367 -0
- package/.speccrew/skills/speccrew-knowledge-bizs-dispatch/SKILL.md +667 -0
- package/.speccrew/skills/speccrew-knowledge-bizs-dispatch/STATUS-FORMATS.md +74 -0
- package/.speccrew/skills/speccrew-knowledge-bizs-dispatch/scripts/batch-orchestrator.js +176 -0
- package/.speccrew/skills/speccrew-knowledge-bizs-dispatch/scripts/get-next-batch.js +150 -0
- package/.speccrew/skills/speccrew-knowledge-bizs-dispatch/scripts/get-pending-features.js +106 -0
- package/.speccrew/skills/speccrew-knowledge-bizs-dispatch/scripts/mark-stale.js +249 -0
- package/.speccrew/skills/speccrew-knowledge-bizs-dispatch/scripts/process-batch-results.js +848 -0
- package/.speccrew/skills/speccrew-knowledge-bizs-dispatch/scripts/update-feature-status.js +226 -0
- package/.speccrew/skills/speccrew-knowledge-bizs-init-features/SKILL.md +264 -0
- package/.speccrew/skills/speccrew-knowledge-bizs-init-features/examples/features.json +34 -0
- package/.speccrew/skills/speccrew-knowledge-bizs-init-features/scripts/generate-inventory.js +867 -0
- package/.speccrew/skills/speccrew-knowledge-bizs-init-features/scripts/test-inventory.js +26 -0
- package/.speccrew/skills/speccrew-knowledge-bizs-module-classify/SKILL.md +165 -0
- package/.speccrew/skills/speccrew-knowledge-bizs-module-classify/scripts/apply-module-mapping.js +208 -0
- package/.speccrew/skills/speccrew-knowledge-bizs-module-classify/scripts/extract-module-summary.js +180 -0
- package/.speccrew/skills/speccrew-knowledge-bizs-module-classify/scripts/reindex-modules.js +358 -0
- package/.speccrew/skills/speccrew-knowledge-bizs-ui-analyze/SKILL.md +1055 -0
- package/.speccrew/skills/speccrew-knowledge-bizs-ui-analyze/templates/FEATURE-DETAIL-TEMPLATE-UI-DESKTOP.md +303 -0
- package/.speccrew/skills/speccrew-knowledge-bizs-ui-analyze/templates/FEATURE-DETAIL-TEMPLATE-UI-ELECTRON.md +327 -0
- package/.speccrew/skills/speccrew-knowledge-bizs-ui-analyze/templates/FEATURE-DETAIL-TEMPLATE-UI-MINIAPP.md +292 -0
- package/.speccrew/skills/speccrew-knowledge-bizs-ui-analyze/templates/FEATURE-DETAIL-TEMPLATE-UI-MOBILE.md +281 -0
- package/.speccrew/skills/speccrew-knowledge-bizs-ui-analyze/templates/FEATURE-DETAIL-TEMPLATE-UI.md +324 -0
- package/.speccrew/skills/speccrew-knowledge-bizs-ui-style-extract/SKILL.md +270 -0
- package/.speccrew/skills/speccrew-knowledge-bizs-ui-style-extract/templates/COMPONENT-PATTERN-TEMPLATE.md +33 -0
- package/.speccrew/skills/speccrew-knowledge-bizs-ui-style-extract/templates/LAYOUT-PATTERN-TEMPLATE.md +33 -0
- package/.speccrew/skills/speccrew-knowledge-bizs-ui-style-extract/templates/PAGE-TYPE-TEMPLATE.md +33 -0
- package/.speccrew/skills/speccrew-knowledge-graph-query/SKILL.md +229 -0
- package/.speccrew/skills/speccrew-knowledge-graph-query/scripts/graph-query.js +549 -0
- package/.speccrew/skills/speccrew-knowledge-graph-write/SKILL.md +181 -0
- package/.speccrew/skills/speccrew-knowledge-graph-write/scripts/graph-write.js +651 -0
- package/.speccrew/skills/speccrew-knowledge-module-summarize/SKILL.md +305 -0
- package/.speccrew/skills/speccrew-knowledge-module-summarize/templates/MODULE-OVERVIEW-TEMPLATE.md +400 -0
- package/.speccrew/skills/speccrew-knowledge-system-summarize/SKILL.md +351 -0
- package/.speccrew/skills/speccrew-knowledge-system-summarize/templates/SYSTEM-OVERVIEW-TEMPLATE.md +294 -0
- package/.speccrew/skills/speccrew-knowledge-techs-dispatch/SKILL.md +683 -0
- package/.speccrew/skills/speccrew-knowledge-techs-dispatch/STATUS-FORMATS.md +550 -0
- package/.speccrew/skills/speccrew-knowledge-techs-dispatch/templates/techs-manifest-EXAMPLE.json +35 -0
- package/.speccrew/skills/speccrew-knowledge-techs-generate/SKILL.md +1087 -0
- package/.speccrew/skills/speccrew-knowledge-techs-generate/templates/ARCHITECTURE-TEMPLATE.md +240 -0
- package/.speccrew/skills/speccrew-knowledge-techs-generate/templates/COLOR-SYSTEM-TEMPLATE.md +68 -0
- package/.speccrew/skills/speccrew-knowledge-techs-generate/templates/COMPONENT-LIBRARY-TEMPLATE.md +86 -0
- package/.speccrew/skills/speccrew-knowledge-techs-generate/templates/CONVENTIONS-BUILD-TEMPLATE.md +466 -0
- package/.speccrew/skills/speccrew-knowledge-techs-generate/templates/CONVENTIONS-DATA-TEMPLATE.md +432 -0
- package/.speccrew/skills/speccrew-knowledge-techs-generate/templates/CONVENTIONS-DESIGN-TEMPLATE.md +1209 -0
- package/.speccrew/skills/speccrew-knowledge-techs-generate/templates/CONVENTIONS-DEV-TEMPLATE.md +1433 -0
- package/.speccrew/skills/speccrew-knowledge-techs-generate/templates/CONVENTIONS-SYSTEM-TEST-TEMPLATE.md +1052 -0
- package/.speccrew/skills/speccrew-knowledge-techs-generate/templates/CONVENTIONS-UNIT-TEST-TEMPLATE.md +946 -0
- package/.speccrew/skills/speccrew-knowledge-techs-generate/templates/INDEX-TEMPLATE.md +29 -0
- package/.speccrew/skills/speccrew-knowledge-techs-generate/templates/PAGE-LAYOUTS-TEMPLATE.md +69 -0
- package/.speccrew/skills/speccrew-knowledge-techs-generate/templates/PAGE-TYPE-SUMMARY-TEMPLATE.md +74 -0
- package/.speccrew/skills/speccrew-knowledge-techs-generate/templates/TECH-STACK-TEMPLATE.md +232 -0
- package/.speccrew/skills/speccrew-knowledge-techs-generate-conventions/SKILL.md +628 -0
- package/.speccrew/skills/speccrew-knowledge-techs-generate-ui-style/SKILL.md +392 -0
- package/.speccrew/skills/speccrew-knowledge-techs-index/SKILL.md +489 -0
- package/.speccrew/skills/speccrew-knowledge-techs-index/templates/INDEX-TEMPLATE.md +243 -0
- package/.speccrew/skills/speccrew-knowledge-techs-init/SKILL.md +269 -0
- package/.speccrew/skills/speccrew-knowledge-techs-ui-analyze/SKILL.md +562 -0
- package/.speccrew/skills/speccrew-knowledge-techs-ui-analyze/templates/BUSINESS-COMPONENTS-TEMPLATE.md +171 -0
- package/.speccrew/skills/speccrew-knowledge-techs-ui-analyze/templates/COMMON-COMPONENTS-TEMPLATE.md +177 -0
- package/.speccrew/skills/speccrew-knowledge-techs-ui-analyze/templates/COMPONENT-INDIVIDUAL-TEMPLATE.md +80 -0
- package/.speccrew/skills/speccrew-knowledge-techs-ui-analyze/templates/COMPONENT-LIBRARY-TEMPLATE.md +118 -0
- package/.speccrew/skills/speccrew-knowledge-techs-ui-analyze/templates/LAYOUT-INDIVIDUAL-TEMPLATE.md +97 -0
- package/.speccrew/skills/speccrew-knowledge-techs-ui-analyze/templates/LAYOUT-PATTERNS-TEMPLATE.md +208 -0
- package/.speccrew/skills/speccrew-knowledge-techs-ui-analyze/templates/NAVIGATION-PATTERNS-TEMPLATE.md +157 -0
- package/.speccrew/skills/speccrew-knowledge-techs-ui-analyze/templates/PAGE-TYPE-INDIVIDUAL-TEMPLATE.md +123 -0
- package/.speccrew/skills/speccrew-knowledge-techs-ui-analyze/templates/PAGE-TYPE-SUMMARY-TEMPLATE.md +58 -0
- package/.speccrew/skills/speccrew-knowledge-techs-ui-analyze/templates/SPACING-TEMPLATE.md +119 -0
- package/.speccrew/skills/speccrew-knowledge-techs-ui-analyze/templates/STYLE-SYSTEM-TEMPLATE.md +117 -0
- package/.speccrew/skills/speccrew-knowledge-techs-ui-analyze/templates/TYPOGRAPHY-TEMPLATE.md +107 -0
- package/.speccrew/skills/speccrew-knowledge-techs-ui-analyze/templates/UI-STYLE-GUIDE-TEMPLATE.md +171 -0
- package/.speccrew/skills/speccrew-pm-requirement-analysis/SKILL.md +434 -0
- package/.speccrew/skills/speccrew-pm-requirement-analysis/templates/BIZS-MODELING-TEMPLATE.md +332 -0
- package/.speccrew/skills/speccrew-pm-requirement-analysis/templates/PRD-TEMPLATE.md +200 -0
- package/.speccrew/skills/speccrew-pm-requirement-assess/SKILL.md +195 -0
- package/.speccrew/skills/speccrew-project-diagnosis/SKILL.md +208 -0
- package/.speccrew/skills/speccrew-project-diagnosis/templates/DIAGNOSIS-REPORT-TEMPLATE.md +202 -0
- package/.speccrew/skills/speccrew-sd-backend/SKILL.md +188 -0
- package/.speccrew/skills/speccrew-sd-backend/templates/INDEX-TEMPLATE.md +85 -0
- package/.speccrew/skills/speccrew-sd-backend/templates/SD-BACKEND-TEMPLATE.md +269 -0
- package/.speccrew/skills/speccrew-sd-desktop/SKILL.md +192 -0
- package/.speccrew/skills/speccrew-sd-desktop/templates/INDEX-TEMPLATE.md +271 -0
- package/.speccrew/skills/speccrew-sd-desktop/templates/SD-DESKTOP-TEMPLATE.md +673 -0
- package/.speccrew/skills/speccrew-sd-frontend/SKILL.md +176 -0
- package/.speccrew/skills/speccrew-sd-frontend/templates/INDEX-TEMPLATE.md +184 -0
- package/.speccrew/skills/speccrew-sd-frontend/templates/SD-FRONTEND-TEMPLATE.md +382 -0
- package/.speccrew/skills/speccrew-sd-mobile/SKILL.md +189 -0
- package/.speccrew/skills/speccrew-sd-mobile/templates/INDEX-TEMPLATE.md +219 -0
- package/.speccrew/skills/speccrew-sd-mobile/templates/SD-MOBILE-TEMPLATE.md +534 -0
- package/.speccrew/skills/speccrew-test-case-design/SKILL.md +284 -0
- package/.speccrew/skills/speccrew-test-case-design/templates/TEST-CASE-DESIGN-TEMPLATE.md +263 -0
- package/.speccrew/skills/speccrew-test-code-gen/SKILL.md +313 -0
- package/.speccrew/skills/speccrew-test-code-gen/templates/TEST-CODE-PLAN-TEMPLATE.md +180 -0
- package/.speccrew/skills/speccrew-test-execute/SKILL.md +283 -0
- package/.speccrew/skills/speccrew-test-execute/templates/BUG-REPORT-TEMPLATE.md +50 -0
- package/.speccrew/skills/speccrew-test-execute/templates/TEST-REPORT-TEMPLATE.md +57 -0
- package/.speccrew/skills/speccrew-workflow-diagnose/SKILL.md +155 -0
- package/LICENSE +21 -0
- package/README.ar.md +318 -0
- package/README.en.md +318 -0
- package/README.es.md +318 -0
- package/README.md +340 -0
- package/bin/cli.js +62 -0
- package/lib/commands/doctor.js +138 -0
- package/lib/commands/init.js +231 -0
- package/lib/commands/list.js +114 -0
- package/lib/commands/uninstall.js +117 -0
- package/lib/commands/update.js +351 -0
- package/lib/ide-adapters.js +73 -0
- package/lib/utils.js +104 -0
- package/package.json +28 -0
- package/workspace-template/docs/configs/document-templates.json +667 -0
- package/workspace-template/docs/configs/platform-mapping.json +194 -0
- package/workspace-template/docs/configs/tech-stack-mappings.json +313 -0
- package/workspace-template/docs/configs/validation-rules.json +87 -0
- package/workspace-template/docs/rules/mermaid-rule.md +114 -0
- package/workspace-template/docs/solutions/Agent/346/212/200/350/203/275/345/256/232/344/271/211+/351/234/200/346/261/202/346/226/207/346/241/243+UML/344/275/277/347/224/250/346/250/241/346/235/277/357/274/210ISA-95/345/205/255/346/256/265/345/274/217/350/236/215/345/220/210/347/211/210/357/274/211.md +586 -0
- package/workspace-template/docs/solutions/agent-knowledge-map.md +238 -0
- package/workspace-template/docs/solutions/bizs-knowledge-pipeline.md +678 -0
- package/workspace-template/docs/solutions/harness.md +410 -0
- package/workspace-template/docs/solutions/knowledge-incremental-sync-spec.md +943 -0
- package/workspace-template/docs/solutions/techs-knowledge-pipeline.md +803 -0
- package/workspace-template/docs/solutions/workspace-structure.md +318 -0
|
@@ -0,0 +1,848 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Batch process completed analysis results.
|
|
5
|
+
*
|
|
6
|
+
* Scans the completed/ directory for .done.json and .graph.json marker files,
|
|
7
|
+
* updates feature statuses, writes graph data, updates metadata, and cleans up.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const { execFileSync } = require('child_process');
|
|
13
|
+
|
|
14
|
+
// Helper: Validate and normalize .done.json file content
|
|
15
|
+
function validateAndNormalizeDoneContent(content, doneFile) {
|
|
16
|
+
// Strip file extensions from fileName if present
|
|
17
|
+
if (content.fileName && /\.\w+$/.test(content.fileName)) {
|
|
18
|
+
const original = content.fileName;
|
|
19
|
+
content.fileName = content.fileName.replace(/\.\w+$/, '');
|
|
20
|
+
console.warn(`[WARN] .done.json file ${doneFile}: stripped extension from fileName "${original}" → "${content.fileName}"`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Validate required fields
|
|
24
|
+
if (!content.fileName) {
|
|
25
|
+
throw new Error(`Missing required field "fileName" in ${doneFile}`);
|
|
26
|
+
}
|
|
27
|
+
if (!content.sourceFile) {
|
|
28
|
+
throw new Error(`Missing required field "sourceFile" in ${doneFile}`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Validate sourcePath format if present
|
|
32
|
+
if (content.sourcePath && typeof content.sourcePath !== 'string') {
|
|
33
|
+
console.warn(`[WARN] .done.json file ${doneFile}: invalid sourcePath type, converting to string`);
|
|
34
|
+
content.sourcePath = String(content.sourcePath);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return content;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Helper: Get project root directory (traverse upward to find speccrew-workspace or .git)
|
|
41
|
+
function getProjectRoot(startPath) {
|
|
42
|
+
let currentDir = path.resolve(startPath);
|
|
43
|
+
while (currentDir !== path.dirname(currentDir)) {
|
|
44
|
+
// Check for common project root markers
|
|
45
|
+
if (fs.existsSync(path.join(currentDir, 'speccrew-workspace')) ||
|
|
46
|
+
fs.existsSync(path.join(currentDir, '.git'))) {
|
|
47
|
+
return currentDir;
|
|
48
|
+
}
|
|
49
|
+
currentDir = path.dirname(currentDir);
|
|
50
|
+
}
|
|
51
|
+
// Fallback: return the parent of syncStatePath
|
|
52
|
+
return path.dirname(path.resolve(startPath));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Helper: Find feature document path from features-{platform}.json
|
|
56
|
+
function findFeatureDocumentPath(syncStatePath, sourceFile, fileName, featureSourcePath) {
|
|
57
|
+
try {
|
|
58
|
+
// Read the source features JSON file
|
|
59
|
+
const sourceFilePath = path.join(syncStatePath, sourceFile);
|
|
60
|
+
if (!fs.existsSync(sourceFilePath)) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const content = JSON.parse(fs.readFileSync(sourceFilePath, 'utf8'));
|
|
65
|
+
if (!content.features || !Array.isArray(content.features)) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Find matching feature by fileName and sourcePath
|
|
70
|
+
const feature = content.features.find(f => {
|
|
71
|
+
const nameMatch = f.fileName === fileName;
|
|
72
|
+
const pathMatch = featureSourcePath ? f.sourcePath === featureSourcePath : true;
|
|
73
|
+
return nameMatch && pathMatch;
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// Fallback: match by fileName only if sourcePath mismatch (Worker may use different path format)
|
|
77
|
+
if (!feature && featureSourcePath) {
|
|
78
|
+
const fallbackFeature = content.features.find(f => f.fileName === fileName);
|
|
79
|
+
if (fallbackFeature) {
|
|
80
|
+
console.warn(`[WARN] sourcePath mismatch for ${fileName}: .done has "${featureSourcePath}", features JSON has "${fallbackFeature.sourcePath}". Using fileName-only match.`);
|
|
81
|
+
return fallbackFeature.documentPath || null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return feature ? feature.documentPath : null;
|
|
86
|
+
} catch (error) {
|
|
87
|
+
console.warn(`[WARN] Failed to find document path for ${fileName}: ${error.message}`);
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Helper: Check if document exists for a feature
|
|
93
|
+
function checkDocumentExists(syncStatePath, sourceFile, fileName, featureSourcePath) {
|
|
94
|
+
const documentPath = findFeatureDocumentPath(syncStatePath, sourceFile, fileName, featureSourcePath);
|
|
95
|
+
if (!documentPath) {
|
|
96
|
+
return { exists: false, path: null };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const projectRoot = getProjectRoot(syncStatePath);
|
|
100
|
+
// documentPath is already a full relative path from project root (e.g. speccrew-workspace/knowledges/bizs/backend-ai/chat/Foo.md)
|
|
101
|
+
const fullPath = path.join(projectRoot, documentPath);
|
|
102
|
+
return { exists: fs.existsSync(fullPath), path: documentPath };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Validate document existence for all analyzed features
|
|
106
|
+
function validateDocumentExistence(syncStatePath) {
|
|
107
|
+
console.error('=== Document Existence Validation ===');
|
|
108
|
+
|
|
109
|
+
const syncStateDir = path.resolve(syncStatePath);
|
|
110
|
+
if (!fs.existsSync(syncStateDir)) {
|
|
111
|
+
console.error(`SyncStatePath not found: ${syncStatePath}`);
|
|
112
|
+
process.exit(1);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Find all features-{platform}.json files
|
|
116
|
+
const files = fs.readdirSync(syncStateDir).filter(f => f.startsWith('features-') && f.endsWith('.json'));
|
|
117
|
+
|
|
118
|
+
let totalAnalyzed = 0;
|
|
119
|
+
let missingDocs = 0;
|
|
120
|
+
const missingList = [];
|
|
121
|
+
|
|
122
|
+
for (const file of files) {
|
|
123
|
+
const filePath = path.join(syncStateDir, file);
|
|
124
|
+
try {
|
|
125
|
+
const content = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
126
|
+
if (!content.features || !Array.isArray(content.features)) {
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Extract platform from filename (features-{platform}.json)
|
|
131
|
+
const platform = file.replace('features-', '').replace('.json', '');
|
|
132
|
+
const projectRoot = getProjectRoot(syncStatePath);
|
|
133
|
+
|
|
134
|
+
for (const feature of content.features) {
|
|
135
|
+
if (feature.analyzed === true || feature.analyzed === 'true') {
|
|
136
|
+
totalAnalyzed++;
|
|
137
|
+
|
|
138
|
+
const docPath = feature.documentPath;
|
|
139
|
+
// documentPath is already a full relative path from project root
|
|
140
|
+
const fullPath = docPath ? path.join(projectRoot, docPath) : null;
|
|
141
|
+
|
|
142
|
+
if (!docPath || !fs.existsSync(fullPath)) {
|
|
143
|
+
missingDocs++;
|
|
144
|
+
const missingInfo = {
|
|
145
|
+
platform: platform,
|
|
146
|
+
feature: feature.fileName || feature.id,
|
|
147
|
+
documentPath: docPath || 'N/A'
|
|
148
|
+
};
|
|
149
|
+
missingList.push(missingInfo);
|
|
150
|
+
console.warn(`[MISSING] ${missingInfo.platform}/${missingInfo.feature}: ${missingInfo.documentPath}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
} catch (error) {
|
|
155
|
+
console.warn(`[WARN] Failed to process ${file}: ${error.message}`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
console.error('\n=== Validation Summary ===');
|
|
160
|
+
console.error(`Total analyzed features: ${totalAnalyzed}`);
|
|
161
|
+
console.error(`Missing documents: ${missingDocs}`);
|
|
162
|
+
|
|
163
|
+
if (missingList.length > 0) {
|
|
164
|
+
console.error('\nMissing documents list:');
|
|
165
|
+
for (const item of missingList) {
|
|
166
|
+
console.error(` - ${item.platform}/${item.feature}: ${item.documentPath}`);
|
|
167
|
+
}
|
|
168
|
+
process.exit(1);
|
|
169
|
+
} else {
|
|
170
|
+
console.error('All analyzed features have corresponding documents.');
|
|
171
|
+
process.exit(0);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Parse command line arguments
|
|
176
|
+
function parseArgs() {
|
|
177
|
+
const args = process.argv.slice(2);
|
|
178
|
+
const result = {
|
|
179
|
+
syncStatePath: null,
|
|
180
|
+
graphRoot: null,
|
|
181
|
+
graphWriteScript: null,
|
|
182
|
+
platformId: null,
|
|
183
|
+
validateDocs: false
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
for (let i = 0; i < args.length; i++) {
|
|
187
|
+
const arg = args[i];
|
|
188
|
+
switch (arg) {
|
|
189
|
+
case '--syncStatePath':
|
|
190
|
+
case '-SyncStatePath':
|
|
191
|
+
result.syncStatePath = args[++i];
|
|
192
|
+
break;
|
|
193
|
+
case '--graphRoot':
|
|
194
|
+
case '-GraphRoot':
|
|
195
|
+
result.graphRoot = args[++i];
|
|
196
|
+
break;
|
|
197
|
+
case '--graphWriteScript':
|
|
198
|
+
case '-GraphWriteScript':
|
|
199
|
+
result.graphWriteScript = args[++i];
|
|
200
|
+
break;
|
|
201
|
+
case '--platformId':
|
|
202
|
+
case '-PlatformId':
|
|
203
|
+
result.platformId = args[++i];
|
|
204
|
+
break;
|
|
205
|
+
case '--validateDocs':
|
|
206
|
+
result.validateDocs = true;
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return result;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function main() {
|
|
215
|
+
const args = parseArgs();
|
|
216
|
+
|
|
217
|
+
// Handle --validateDocs mode: validate document existence and exit
|
|
218
|
+
if (args.validateDocs) {
|
|
219
|
+
if (!args.syncStatePath) {
|
|
220
|
+
console.error('Error: --syncStatePath is required for --validateDocs mode');
|
|
221
|
+
process.exit(1);
|
|
222
|
+
}
|
|
223
|
+
validateDocumentExistence(args.syncStatePath);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (!args.syncStatePath || !args.graphRoot || !args.graphWriteScript) {
|
|
228
|
+
console.error('Error: --syncStatePath, --graphRoot, and --graphWriteScript are required');
|
|
229
|
+
process.exit(1);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (!args.platformId) {
|
|
233
|
+
console.error('Error: --platformId is required');
|
|
234
|
+
process.exit(1);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const platformId = args.platformId;
|
|
238
|
+
|
|
239
|
+
// Ensure paths are absolute
|
|
240
|
+
const syncStatePath = path.resolve(args.syncStatePath);
|
|
241
|
+
const graphRoot = path.resolve(args.graphRoot);
|
|
242
|
+
const graphWriteScript = path.resolve(args.graphWriteScript);
|
|
243
|
+
|
|
244
|
+
if (!fs.existsSync(syncStatePath)) {
|
|
245
|
+
console.error(`SyncStatePath not found: ${args.syncStatePath}`);
|
|
246
|
+
process.exit(1);
|
|
247
|
+
}
|
|
248
|
+
if (!fs.existsSync(graphRoot)) {
|
|
249
|
+
console.error(`GraphRoot not found, creating: ${args.graphRoot}`);
|
|
250
|
+
fs.mkdirSync(graphRoot, { recursive: true });
|
|
251
|
+
}
|
|
252
|
+
if (!fs.existsSync(graphWriteScript)) {
|
|
253
|
+
console.error(`GraphWriteScript not found: ${args.graphWriteScript}`);
|
|
254
|
+
process.exit(1);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const completedDir = path.join(syncStatePath, 'completed');
|
|
258
|
+
|
|
259
|
+
// Diagnostic logging for completed directory
|
|
260
|
+
console.error(`Scanning for .done.json files in: ${completedDir}`);
|
|
261
|
+
console.error(`Directory exists: ${fs.existsSync(completedDir)}`);
|
|
262
|
+
|
|
263
|
+
// Auto-create completed directory if it doesn't exist
|
|
264
|
+
if (!fs.existsSync(completedDir)) {
|
|
265
|
+
console.error(`[ERROR] completed directory does not exist: ${completedDir}`);
|
|
266
|
+
console.error(`[INFO] Auto-creating completed directory...`);
|
|
267
|
+
try {
|
|
268
|
+
fs.mkdirSync(completedDir, { recursive: true });
|
|
269
|
+
console.error(`[INFO] Successfully created completed directory`);
|
|
270
|
+
} catch (error) {
|
|
271
|
+
console.error(`[ERROR] Failed to create completed directory: ${error.message}`);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Check for .done files and warn if none found
|
|
276
|
+
if (fs.existsSync(completedDir)) {
|
|
277
|
+
const allFiles = fs.readdirSync(completedDir);
|
|
278
|
+
const doneFiles = allFiles.filter(f => f.endsWith('.done.json'));
|
|
279
|
+
if (doneFiles.length === 0) {
|
|
280
|
+
console.warn(`[WARNING] No .done.json files found in completed directory. Workers may not have created marker files correctly.`);
|
|
281
|
+
console.warn(`[INFO] Files in completed directory: ${allFiles.length > 0 ? allFiles.join(', ') : '(empty)'}`);
|
|
282
|
+
} else {
|
|
283
|
+
console.error(`[INFO] Found ${doneFiles.length} .done.json file(s)`);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Result tracking
|
|
288
|
+
let processedCount = 0;
|
|
289
|
+
let skippedFilesCount = 0;
|
|
290
|
+
let docMissingCount = 0; // Features skipped due to missing document
|
|
291
|
+
const modulesUpdated = [];
|
|
292
|
+
const errors = [];
|
|
293
|
+
const failedOperations = [];
|
|
294
|
+
|
|
295
|
+
// Track successfully processed files for cleanup
|
|
296
|
+
const successfulDoneFiles = new Set();
|
|
297
|
+
const successfulGraphFiles = new Set();
|
|
298
|
+
|
|
299
|
+
// Helper: Write error and continue
|
|
300
|
+
function writeErrorContinue(message) {
|
|
301
|
+
errors.push(message);
|
|
302
|
+
console.warn(`Warning: ${message}`);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Helper: Record failed operation
|
|
306
|
+
function addFailedOperation(module, operation, errorMessage) {
|
|
307
|
+
failedOperations.push({
|
|
308
|
+
module: module,
|
|
309
|
+
operation: operation,
|
|
310
|
+
error: errorMessage
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Helper: Call external script (graph-write)
|
|
315
|
+
function callGraphWriteScript(action, module, tempFile, graphRootPath, platformId) {
|
|
316
|
+
const ext = path.extname(graphWriteScript).toLowerCase();
|
|
317
|
+
let command, commandArgs;
|
|
318
|
+
|
|
319
|
+
if (ext === '.ps1') {
|
|
320
|
+
command = 'powershell';
|
|
321
|
+
commandArgs = ['-File', graphWriteScript, '-Action', action, '-Module', module, '-File', tempFile, '-GraphRoot', graphRootPath, '-PlatformId', platformId];
|
|
322
|
+
} else {
|
|
323
|
+
command = 'node';
|
|
324
|
+
commandArgs = [graphWriteScript, '--action', action, '--module', module, '--file', tempFile, '--graphRoot', graphRootPath, '--platformId', platformId];
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
execFileSync(command, commandArgs, { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Helper: Update feature status using update-feature-status.js
|
|
331
|
+
function updateFeatureStatus(sourceFilePath, fileName, featureSourcePath, analyzed, setCompleted, analysisNotes) {
|
|
332
|
+
const updateStatusScript = path.join(path.dirname(__filename), 'update-feature-status.js');
|
|
333
|
+
const commandArgs = [
|
|
334
|
+
updateStatusScript,
|
|
335
|
+
'--sourceFile', sourceFilePath,
|
|
336
|
+
'--fileName', fileName,
|
|
337
|
+
'--analyzed', analyzed
|
|
338
|
+
];
|
|
339
|
+
|
|
340
|
+
if (featureSourcePath) {
|
|
341
|
+
commandArgs.push('--featureSourcePath', featureSourcePath);
|
|
342
|
+
}
|
|
343
|
+
if (setCompleted) {
|
|
344
|
+
commandArgs.push('--setCompleted');
|
|
345
|
+
}
|
|
346
|
+
if (analysisNotes) {
|
|
347
|
+
commandArgs.push('--analysisNotes', analysisNotes);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
execFileSync('node', commandArgs, { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// ── Step 1: Process .done.json files and update status ────────────────────────────
|
|
354
|
+
|
|
355
|
+
// Fallback: try to parse key=value format when JSON parsing fails
|
|
356
|
+
function parseFallbackDone(rawContent, fileName) {
|
|
357
|
+
const result = {};
|
|
358
|
+
const lines = rawContent.split(/\r?\n/);
|
|
359
|
+
for (const line of lines) {
|
|
360
|
+
const match = line.match(/^\s*(\w+)\s*[=:]\s*(.+?)\s*$/);
|
|
361
|
+
if (match) {
|
|
362
|
+
result[match[1]] = match[2].replace(/^["']|["']$/g, '');
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
if (result.fileName || result.status) {
|
|
366
|
+
console.warn(`[WARN] Parsed .done.json file ${fileName} using fallback key=value format`);
|
|
367
|
+
return result;
|
|
368
|
+
}
|
|
369
|
+
return null;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Fallback: recover minimal info from filename and context when .done.json is non-JSON
|
|
373
|
+
function recoverDoneFileFromContext(doneFile, completedDir, syncStatePath) {
|
|
374
|
+
const fileName = doneFile.replace(/\.done\.json$/, '');
|
|
375
|
+
console.warn(`[WARN] .done.json file is not valid JSON: ${doneFile}. Attempting recovery from filename...`);
|
|
376
|
+
|
|
377
|
+
// Try to get module from same-named .graph.json
|
|
378
|
+
let module = 'unknown';
|
|
379
|
+
const graphFilePath = path.join(completedDir, fileName + '.graph.json');
|
|
380
|
+
if (fs.existsSync(graphFilePath)) {
|
|
381
|
+
try {
|
|
382
|
+
const graphContent = JSON.parse(fs.readFileSync(graphFilePath, 'utf8'));
|
|
383
|
+
if (graphContent.module) {
|
|
384
|
+
module = graphContent.module;
|
|
385
|
+
}
|
|
386
|
+
} catch (e) {
|
|
387
|
+
// Ignore parse errors
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Try to match sourceFile from features-*.json files
|
|
392
|
+
let sourceFile = 'unknown';
|
|
393
|
+
try {
|
|
394
|
+
const files = fs.readdirSync(syncStatePath);
|
|
395
|
+
const featureFiles = files.filter(f => f.startsWith('features-') && f.endsWith('.json'));
|
|
396
|
+
|
|
397
|
+
for (const f of featureFiles) {
|
|
398
|
+
const filePath = path.join(syncStatePath, f);
|
|
399
|
+
try {
|
|
400
|
+
const content = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
401
|
+
if (content.features && Array.isArray(content.features)) {
|
|
402
|
+
const found = content.features.find(feat => feat.fileName === fileName);
|
|
403
|
+
if (found) {
|
|
404
|
+
sourceFile = f;
|
|
405
|
+
break;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
} catch (e) {
|
|
409
|
+
// Continue checking next file
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
} catch (e) {
|
|
413
|
+
// Ignore read errors
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
console.warn(`[WARN] Recovered minimal info: fileName=${fileName}, module=${module}, sourceFile=${sourceFile}. Some fields may be inaccurate.`);
|
|
417
|
+
|
|
418
|
+
return {
|
|
419
|
+
fileName: fileName,
|
|
420
|
+
sourceFile: sourceFile,
|
|
421
|
+
module: module,
|
|
422
|
+
sourcePath: null,
|
|
423
|
+
status: 'success',
|
|
424
|
+
analysisNotes: 'Auto-recovered from non-JSON .done.json file'
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function processDoneFiles() {
|
|
429
|
+
if (!fs.existsSync(completedDir)) {
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const doneFiles = fs.readdirSync(completedDir).filter(f => f.endsWith('.done.json'));
|
|
434
|
+
|
|
435
|
+
for (const doneFile of doneFiles) {
|
|
436
|
+
let rawContent;
|
|
437
|
+
try {
|
|
438
|
+
const doneFilePath = path.join(completedDir, doneFile);
|
|
439
|
+
rawContent = fs.readFileSync(doneFilePath, 'utf8');
|
|
440
|
+
if (!rawContent || rawContent.trim() === '') {
|
|
441
|
+
console.warn(`Empty .done.json file detected: ${doneFile} - Worker may have failed to write content`);
|
|
442
|
+
skippedFilesCount++;
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
let content = JSON.parse(rawContent);
|
|
446
|
+
|
|
447
|
+
// Validate and normalize content
|
|
448
|
+
content = validateAndNormalizeDoneContent(content, doneFile);
|
|
449
|
+
|
|
450
|
+
const fileName = content.fileName;
|
|
451
|
+
const featureSourcePath = content.sourcePath;
|
|
452
|
+
const sourceFile = content.sourceFile;
|
|
453
|
+
const module = content.module;
|
|
454
|
+
let analysisNotes = content.analysisNotes;
|
|
455
|
+
|
|
456
|
+
if (!fileName || !sourceFile) {
|
|
457
|
+
console.warn(`Invalid .done.json file format: ${doneFile}`);
|
|
458
|
+
console.warn(` Expected fields: fileName (got: '${fileName}'), sourceFile (got: '${sourceFile}')`);
|
|
459
|
+
console.warn(` File content preview: ${rawContent.substring(0, Math.min(200, rawContent.length))}`);
|
|
460
|
+
continue;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// Warn if module field is missing or empty (non-blocking)
|
|
464
|
+
if (!module) {
|
|
465
|
+
console.warn(`[WARN] module field missing from .done.json file: ${doneFile}`);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// Check if document exists before marking as analyzed
|
|
469
|
+
const docCheck = checkDocumentExists(syncStatePath, sourceFile, fileName, featureSourcePath);
|
|
470
|
+
if (!docCheck.exists) {
|
|
471
|
+
const warnMsg = `[WARN: document missing at ${docCheck.path || 'unknown path'}]`;
|
|
472
|
+
console.warn(`Document not found for feature ${fileName}: ${warnMsg}`);
|
|
473
|
+
console.warn(`[SKIP] Feature ${fileName} NOT marked as analyzed - document must exist. .done.json file PRESERVED for retry.`);
|
|
474
|
+
// Do NOT mark as successful - preserve .done.json for retry
|
|
475
|
+
docMissingCount++;
|
|
476
|
+
continue;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// Build source file path (relative to SyncStatePath)
|
|
480
|
+
const sourceFilePath = path.join(syncStatePath, sourceFile);
|
|
481
|
+
|
|
482
|
+
// Call update-feature-status.js
|
|
483
|
+
updateFeatureStatus(sourceFilePath, fileName, featureSourcePath, 'true', true, analysisNotes);
|
|
484
|
+
|
|
485
|
+
processedCount++;
|
|
486
|
+
successfulDoneFiles.add(doneFile);
|
|
487
|
+
} catch (error) {
|
|
488
|
+
// Defensive check: if rawContent is undefined, file read itself failed
|
|
489
|
+
if (rawContent === undefined) {
|
|
490
|
+
writeErrorContinue(`Failed to read .done.json file ${doneFile}: ${error.message}`);
|
|
491
|
+
continue;
|
|
492
|
+
}
|
|
493
|
+
// Try fallback parsing for non-JSON format
|
|
494
|
+
const fallbackContent = parseFallbackDone(rawContent, doneFile);
|
|
495
|
+
if (fallbackContent) {
|
|
496
|
+
try {
|
|
497
|
+
// Validate and normalize fallback content
|
|
498
|
+
validateAndNormalizeDoneContent(fallbackContent, doneFile);
|
|
499
|
+
|
|
500
|
+
const fileName = fallbackContent.fileName;
|
|
501
|
+
const featureSourcePath = fallbackContent.sourcePath;
|
|
502
|
+
const sourceFile = fallbackContent.sourceFile;
|
|
503
|
+
let analysisNotes = fallbackContent.analysisNotes;
|
|
504
|
+
|
|
505
|
+
if (fileName && sourceFile) {
|
|
506
|
+
const sourceFilePath = path.join(syncStatePath, sourceFile);
|
|
507
|
+
|
|
508
|
+
// Check if document exists before marking as analyzed (fallback path)
|
|
509
|
+
const docCheck = checkDocumentExists(syncStatePath, sourceFile, fileName, featureSourcePath);
|
|
510
|
+
if (!docCheck.exists) {
|
|
511
|
+
const warnMsg = `[WARN: document missing at ${docCheck.path || 'unknown path'}]`;
|
|
512
|
+
console.warn(`Document not found for feature ${fileName}: ${warnMsg}`);
|
|
513
|
+
console.warn(`[SKIP] Feature ${fileName} NOT marked as analyzed (fallback path) - document must exist. .done.json file PRESERVED.`);
|
|
514
|
+
docMissingCount++;
|
|
515
|
+
continue;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
updateFeatureStatus(sourceFilePath, fileName, featureSourcePath, 'true', true, analysisNotes);
|
|
519
|
+
processedCount++;
|
|
520
|
+
successfulDoneFiles.add(doneFile);
|
|
521
|
+
continue;
|
|
522
|
+
}
|
|
523
|
+
} catch (fallbackError) {
|
|
524
|
+
writeErrorContinue(`Failed to process .done.json file ${doneFile} (fallback): ${fallbackError.message}`);
|
|
525
|
+
continue;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// Final fallback: recover from filename and context
|
|
530
|
+
const recoveredContent = recoverDoneFileFromContext(doneFile, completedDir, syncStatePath);
|
|
531
|
+
if (recoveredContent && recoveredContent.fileName && recoveredContent.sourceFile !== 'unknown') {
|
|
532
|
+
try {
|
|
533
|
+
const fileName = recoveredContent.fileName;
|
|
534
|
+
const featureSourcePath = recoveredContent.sourcePath;
|
|
535
|
+
const sourceFile = recoveredContent.sourceFile;
|
|
536
|
+
let analysisNotes = recoveredContent.analysisNotes;
|
|
537
|
+
|
|
538
|
+
const sourceFilePath = path.join(syncStatePath, sourceFile);
|
|
539
|
+
|
|
540
|
+
// Check if document exists before marking as analyzed (recovery path)
|
|
541
|
+
const docCheck = checkDocumentExists(syncStatePath, sourceFile, fileName, featureSourcePath);
|
|
542
|
+
if (!docCheck.exists) {
|
|
543
|
+
const warnMsg = `[WARN: document missing at ${docCheck.path || 'unknown path'}]`;
|
|
544
|
+
console.warn(`Document not found for feature ${fileName}: ${warnMsg}`);
|
|
545
|
+
console.warn(`[SKIP] Feature ${fileName} NOT marked as analyzed (recovery path) - document must exist. .done.json file PRESERVED.`);
|
|
546
|
+
docMissingCount++;
|
|
547
|
+
continue;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
updateFeatureStatus(sourceFilePath, fileName, featureSourcePath, 'true', true, analysisNotes);
|
|
551
|
+
processedCount++;
|
|
552
|
+
successfulDoneFiles.add(doneFile);
|
|
553
|
+
continue;
|
|
554
|
+
} catch (recoveryError) {
|
|
555
|
+
writeErrorContinue(`Failed to process .done.json file ${doneFile} (recovery): ${recoveryError.message}`);
|
|
556
|
+
continue;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
writeErrorContinue(`Failed to process .done.json file ${doneFile}: ${error.message}`);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// ── Step 2: Collect .graph.json files and write by module ────────────────────
|
|
566
|
+
|
|
567
|
+
function processGraphFiles() {
|
|
568
|
+
if (!fs.existsSync(completedDir)) {
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
const graphFiles = fs.readdirSync(completedDir).filter(f => f.endsWith('.graph.json'));
|
|
573
|
+
|
|
574
|
+
if (graphFiles.length === 0) {
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// Group by module
|
|
579
|
+
const moduleGroups = {};
|
|
580
|
+
|
|
581
|
+
for (const graphFile of graphFiles) {
|
|
582
|
+
try {
|
|
583
|
+
const graphFilePath = path.join(completedDir, graphFile);
|
|
584
|
+
const rawContent = fs.readFileSync(graphFilePath, 'utf8');
|
|
585
|
+
if (!rawContent || rawContent.trim() === '') {
|
|
586
|
+
console.warn(`Empty .graph.json file detected: ${graphFile} - Worker may have failed to write content`);
|
|
587
|
+
skippedFilesCount++;
|
|
588
|
+
continue;
|
|
589
|
+
}
|
|
590
|
+
const content = JSON.parse(rawContent);
|
|
591
|
+
let module = content.module;
|
|
592
|
+
|
|
593
|
+
if (!module) {
|
|
594
|
+
// Try to read module from corresponding .done.json file
|
|
595
|
+
const doneFileName = graphFile.replace('.graph.json', '.done.json');
|
|
596
|
+
const doneFilePath = path.join(completedDir, doneFileName);
|
|
597
|
+
|
|
598
|
+
if (fs.existsSync(doneFilePath)) {
|
|
599
|
+
try {
|
|
600
|
+
const doneRawContent = fs.readFileSync(doneFilePath, 'utf8');
|
|
601
|
+
const doneContent = JSON.parse(doneRawContent);
|
|
602
|
+
if (doneContent.module) {
|
|
603
|
+
module = doneContent.module;
|
|
604
|
+
content.module = module;
|
|
605
|
+
console.warn(`[WARN] .graph.json missing root-level "module" field, falling back to .done.json file: ${graphFile}`);
|
|
606
|
+
}
|
|
607
|
+
} catch (doneError) {
|
|
608
|
+
console.warn(`Failed to read module from .done.json file ${doneFileName}: ${doneError.message}`);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// If still no module, skip this file
|
|
613
|
+
if (!module) {
|
|
614
|
+
console.warn(`Cannot determine module for: ${graphFile}, skipping`);
|
|
615
|
+
console.warn(` Tried to read from .done.json file: ${doneFileName}`);
|
|
616
|
+
console.warn(` File content preview: ${rawContent.substring(0, Math.min(200, rawContent.length))}`);
|
|
617
|
+
continue;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
const effectiveModule = content.module;
|
|
622
|
+
|
|
623
|
+
if (!moduleGroups[effectiveModule]) {
|
|
624
|
+
moduleGroups[effectiveModule] = {
|
|
625
|
+
nodes: [],
|
|
626
|
+
edges: [],
|
|
627
|
+
files: []
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
if (content.nodes && Array.isArray(content.nodes)) {
|
|
632
|
+
moduleGroups[effectiveModule].nodes.push(...content.nodes);
|
|
633
|
+
} else if (content.nodes !== undefined) {
|
|
634
|
+
console.warn(`[WARN] .graph.json "nodes" field is not an array in: ${graphFile}`);
|
|
635
|
+
}
|
|
636
|
+
if (content.edges && Array.isArray(content.edges)) {
|
|
637
|
+
moduleGroups[effectiveModule].edges.push(...content.edges);
|
|
638
|
+
} else if (content.edges !== undefined) {
|
|
639
|
+
console.warn(`[WARN] .graph.json "edges" field is not an array in: ${graphFile}`);
|
|
640
|
+
}
|
|
641
|
+
moduleGroups[effectiveModule].files.push(graphFilePath);
|
|
642
|
+
} catch (error) {
|
|
643
|
+
writeErrorContinue(`Failed to read .graph.json file ${graphFile}: ${error.message}`);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// Process each module
|
|
648
|
+
for (const module of Object.keys(moduleGroups)) {
|
|
649
|
+
const group = moduleGroups[module];
|
|
650
|
+
let tempFile = null;
|
|
651
|
+
let nodesOk = true;
|
|
652
|
+
let edgesOk = true;
|
|
653
|
+
|
|
654
|
+
try {
|
|
655
|
+
// Create temp file with merged data
|
|
656
|
+
const tempFileName = `temp-graph-${module}-${new Date().toISOString().replace(/[:.]/g, '')}.json`;
|
|
657
|
+
tempFile = path.join(require('os').tmpdir(), tempFileName);
|
|
658
|
+
|
|
659
|
+
const mergedData = {
|
|
660
|
+
nodes: group.nodes,
|
|
661
|
+
edges: group.edges
|
|
662
|
+
};
|
|
663
|
+
|
|
664
|
+
fs.writeFileSync(tempFile, JSON.stringify(mergedData, null, 2), 'utf8');
|
|
665
|
+
|
|
666
|
+
// Call graph-write script for nodes
|
|
667
|
+
if (group.nodes.length > 0) {
|
|
668
|
+
try {
|
|
669
|
+
callGraphWriteScript('add-nodes', module, tempFile, graphRoot, platformId);
|
|
670
|
+
} catch (error) {
|
|
671
|
+
nodesOk = false;
|
|
672
|
+
const errorMsg = `Failed to add nodes for module '${module}': ${error.message}`;
|
|
673
|
+
console.warn(errorMsg);
|
|
674
|
+
console.warn(` Parameters: Action=add-nodes, Module=${module}, File=${tempFile}, GraphRoot=${graphRoot}, PlatformId=${platformId}`);
|
|
675
|
+
addFailedOperation(module, 'add-nodes', error.message);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// Call graph-write script for edges
|
|
680
|
+
if (group.edges.length > 0) {
|
|
681
|
+
try {
|
|
682
|
+
callGraphWriteScript('add-edges', module, tempFile, graphRoot, platformId);
|
|
683
|
+
} catch (error) {
|
|
684
|
+
edgesOk = false;
|
|
685
|
+
const errorMsg = `Failed to add edges for module '${module}': ${error.message}`;
|
|
686
|
+
console.warn(errorMsg);
|
|
687
|
+
console.warn(` Parameters: Action=add-edges, Module=${module}, File=${tempFile}, GraphRoot=${graphRoot}, PlatformId=${platformId}`);
|
|
688
|
+
addFailedOperation(module, 'add-edges', error.message);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// Track updated module
|
|
693
|
+
if (!modulesUpdated.includes(module)) {
|
|
694
|
+
modulesUpdated.push(module);
|
|
695
|
+
}
|
|
696
|
+
} catch (error) {
|
|
697
|
+
writeErrorContinue(`Failed to write graph data for module '${module}': ${error.message}`);
|
|
698
|
+
} finally {
|
|
699
|
+
// Mark files as successful only if both nodes and edges were written successfully
|
|
700
|
+
if (nodesOk && edgesOk) {
|
|
701
|
+
for (const filePath of group.files) {
|
|
702
|
+
successfulGraphFiles.add(path.basename(filePath));
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
// Clean up temp file
|
|
706
|
+
if (tempFile && fs.existsSync(tempFile)) {
|
|
707
|
+
try {
|
|
708
|
+
fs.unlinkSync(tempFile);
|
|
709
|
+
} catch (e) {
|
|
710
|
+
// Ignore cleanup errors
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
// ── Step 3: Update metadata ──────────────────────────────────────────────────
|
|
718
|
+
|
|
719
|
+
function updateMetadata() {
|
|
720
|
+
try {
|
|
721
|
+
const ext = path.extname(graphWriteScript).toLowerCase();
|
|
722
|
+
let command, commandArgs;
|
|
723
|
+
|
|
724
|
+
if (ext === '.ps1') {
|
|
725
|
+
command = 'powershell';
|
|
726
|
+
commandArgs = ['-File', graphWriteScript, '-Action', 'update-meta', '-GraphRoot', graphRoot];
|
|
727
|
+
} else {
|
|
728
|
+
command = 'node';
|
|
729
|
+
commandArgs = [graphWriteScript, '--action', 'update-meta', '--graphRoot', graphRoot];
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
execFileSync(command, commandArgs, { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
733
|
+
} catch (error) {
|
|
734
|
+
const errorMsg = `Failed to update metadata: ${error.message}`;
|
|
735
|
+
console.warn(errorMsg);
|
|
736
|
+
console.warn(` Parameters: Action=update-meta, GraphRoot=${graphRoot}`);
|
|
737
|
+
addFailedOperation('N/A', 'update-meta', error.message);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
// ── Step 4: Clean up marker files ────────────────────────────────────────────
|
|
742
|
+
|
|
743
|
+
function removeMarkerFiles() {
|
|
744
|
+
if (!fs.existsSync(completedDir)) {
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
// Log cleanup plan before execution
|
|
749
|
+
console.error('=== Cleanup Plan ===');
|
|
750
|
+
console.error(`[INFO] Successfully processed .done.json files to remove: ${successfulDoneFiles.size}`);
|
|
751
|
+
console.error(`[INFO] Successfully processed .graph.json files to remove: ${successfulGraphFiles.size}`);
|
|
752
|
+
|
|
753
|
+
const allDoneFiles = fs.readdirSync(completedDir).filter(f => f.endsWith('.done.json'));
|
|
754
|
+
const failedDoneFiles = allDoneFiles.filter(f => !successfulDoneFiles.has(f));
|
|
755
|
+
if (failedDoneFiles.length > 0) {
|
|
756
|
+
console.error(`[INFO] Failed/unprocessed .done.json files to PRESERVE: ${failedDoneFiles.length}`);
|
|
757
|
+
for (const file of failedDoneFiles) {
|
|
758
|
+
console.error(` [PRESERVE] ${file}`);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
const allGraphFiles = fs.readdirSync(completedDir).filter(f => f.endsWith('.graph.json'));
|
|
763
|
+
const failedGraphFiles = allGraphFiles.filter(f => !successfulGraphFiles.has(f));
|
|
764
|
+
if (failedGraphFiles.length > 0) {
|
|
765
|
+
console.error(`[INFO] Failed/unprocessed .graph.json files to PRESERVE: ${failedGraphFiles.length}`);
|
|
766
|
+
for (const file of failedGraphFiles) {
|
|
767
|
+
console.error(` [PRESERVE] ${file}`);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
console.error('====================');
|
|
771
|
+
|
|
772
|
+
// Remove .done.json files (ONLY successfully processed ones)
|
|
773
|
+
// IMPORTANT: Failed .done.json files are PRESERVED for retry after fixing the issue
|
|
774
|
+
const doneFiles = fs.readdirSync(completedDir).filter(f => f.endsWith('.done.json'));
|
|
775
|
+
for (const file of doneFiles) {
|
|
776
|
+
if (successfulDoneFiles.has(file)) {
|
|
777
|
+
try {
|
|
778
|
+
const filePath = path.join(completedDir, file);
|
|
779
|
+
console.error(`[CLEANUP] Removing successfully processed .done.json file: ${file}`);
|
|
780
|
+
fs.unlinkSync(filePath);
|
|
781
|
+
} catch (error) {
|
|
782
|
+
writeErrorContinue(`Failed to remove .done.json file ${file}: ${error.message}`);
|
|
783
|
+
}
|
|
784
|
+
} else {
|
|
785
|
+
// PRESERVE failed .done.json files for later retry - DO NOT DELETE
|
|
786
|
+
console.warn(`[PRESERVE] Keeping failed .done.json file for retry: ${file}`);
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
// Remove .graph.json files (ONLY successfully processed ones)
|
|
791
|
+
// IMPORTANT: Failed .graph.json files are PRESERVED for retry after fixing the issue
|
|
792
|
+
const graphFiles = fs.readdirSync(completedDir).filter(f => f.endsWith('.graph.json'));
|
|
793
|
+
for (const file of graphFiles) {
|
|
794
|
+
if (successfulGraphFiles.has(file)) {
|
|
795
|
+
try {
|
|
796
|
+
const filePath = path.join(completedDir, file);
|
|
797
|
+
console.error(`[CLEANUP] Removing successfully processed .graph.json file: ${file}`);
|
|
798
|
+
fs.unlinkSync(filePath);
|
|
799
|
+
} catch (error) {
|
|
800
|
+
writeErrorContinue(`Failed to remove .graph.json file ${file}: ${error.message}`);
|
|
801
|
+
}
|
|
802
|
+
} else {
|
|
803
|
+
// PRESERVE failed .graph.json files for later retry - DO NOT DELETE
|
|
804
|
+
console.warn(`[PRESERVE] Keeping failed .graph.json file for retry: ${file}`);
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
// ── Main Execution ───────────────────────────────────────────────────────────
|
|
810
|
+
|
|
811
|
+
try {
|
|
812
|
+
processDoneFiles();
|
|
813
|
+
processGraphFiles();
|
|
814
|
+
updateMetadata();
|
|
815
|
+
removeMarkerFiles();
|
|
816
|
+
} catch (error) {
|
|
817
|
+
writeErrorContinue(`Unexpected error during batch processing: ${error.message}`);
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
// ── Output Summary ───────────────────────────────────────────────────────────
|
|
821
|
+
|
|
822
|
+
const output = {
|
|
823
|
+
processed: processedCount,
|
|
824
|
+
skipped: skippedFilesCount,
|
|
825
|
+
doc_missing: docMissingCount,
|
|
826
|
+
modules_updated: modulesUpdated,
|
|
827
|
+
errors: errors,
|
|
828
|
+
failed_operations: failedOperations
|
|
829
|
+
};
|
|
830
|
+
|
|
831
|
+
console.log(JSON.stringify(output, null, 2));
|
|
832
|
+
|
|
833
|
+
// Output failed operations summary if any
|
|
834
|
+
if (failedOperations.length > 0) {
|
|
835
|
+
console.warn('=== Failed Operations Summary ===');
|
|
836
|
+
for (const op of failedOperations) {
|
|
837
|
+
console.warn(` Module: ${op.module}, Operation: ${op.operation}, Error: ${op.error}`);
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
// Exit with error code if features were skipped due to missing documents
|
|
842
|
+
if (docMissingCount > 0) {
|
|
843
|
+
console.error(`\n[ERROR] ${docMissingCount} feature(s) skipped - documents not found. .done.json files preserved in completed/ for retry.`);
|
|
844
|
+
process.exit(1);
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
main();
|