speccrew 0.6.69 → 0.7.1
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-task-worker.md +1 -1
- package/.speccrew/agents/speccrew-team-leader.md +336 -189
- package/.speccrew/skills/speccrew-agentflow-manager/SKILL.md +161 -0
- package/.speccrew/skills/speccrew-agentflow-manager/workflow.agentflow.xml +347 -0
- package/.speccrew/skills/speccrew-deploy-build/SKILL.md +3 -56
- package/.speccrew/skills/speccrew-deploy-build/workflow.agentflow.xml +125 -0
- package/.speccrew/skills/speccrew-deploy-migrate/SKILL.md +3 -64
- package/.speccrew/skills/speccrew-deploy-migrate/workflow.agentflow.xml +135 -0
- package/.speccrew/skills/speccrew-deploy-smoke-test/SKILL.md +4 -156
- package/.speccrew/skills/speccrew-deploy-smoke-test/workflow.agentflow.xml +178 -0
- package/.speccrew/skills/speccrew-deploy-startup/SKILL.md +3 -135
- package/.speccrew/skills/speccrew-deploy-startup/workflow.agentflow.xml +223 -0
- package/.speccrew/skills/speccrew-dev-backend/SKILL.md +10 -2
- package/.speccrew/skills/speccrew-dev-backend/workflow.agentflow.xml +254 -0
- package/.speccrew/skills/speccrew-dev-desktop-electron/SKILL.md +10 -2
- package/.speccrew/skills/speccrew-dev-desktop-electron/workflow.agentflow.xml +259 -0
- package/.speccrew/skills/speccrew-dev-desktop-tauri/SKILL.md +10 -2
- package/.speccrew/skills/speccrew-dev-desktop-tauri/workflow.agentflow.xml +245 -0
- package/.speccrew/skills/speccrew-dev-frontend/SKILL.md +10 -2
- package/.speccrew/skills/speccrew-dev-frontend/workflow.agentflow.xml +262 -0
- package/.speccrew/skills/speccrew-dev-mobile/SKILL.md +10 -2
- package/.speccrew/skills/speccrew-dev-mobile/workflow.agentflow.xml +244 -0
- package/.speccrew/skills/speccrew-dev-review-backend/SKILL.md +10 -2
- package/.speccrew/skills/speccrew-dev-review-backend/workflow.agentflow.xml +251 -0
- package/.speccrew/skills/speccrew-dev-review-desktop/SKILL.md +10 -2
- package/.speccrew/skills/speccrew-dev-review-desktop/workflow.agentflow.xml +214 -0
- package/.speccrew/skills/speccrew-dev-review-frontend/SKILL.md +10 -2
- package/.speccrew/skills/speccrew-dev-review-frontend/workflow.agentflow.xml +213 -0
- package/.speccrew/skills/speccrew-dev-review-mobile/SKILL.md +10 -2
- package/.speccrew/skills/speccrew-dev-review-mobile/workflow.agentflow.xml +214 -0
- package/.speccrew/skills/speccrew-fd-api-contract/SKILL.md +7 -1
- package/.speccrew/skills/speccrew-fd-api-contract/workflow.agentflow.xml +222 -0
- package/.speccrew/skills/speccrew-fd-feature-analyze/SKILL.md +7 -1
- package/.speccrew/skills/speccrew-fd-feature-analyze/workflow.agentflow.xml +223 -0
- package/.speccrew/skills/speccrew-fd-feature-design/SKILL.md +7 -1
- package/.speccrew/skills/speccrew-fd-feature-design/workflow.agentflow.xml +322 -0
- package/.speccrew/skills/speccrew-get-timestamp/SKILL.md +3 -39
- package/.speccrew/skills/speccrew-get-timestamp/workflow.agentflow.xml +43 -0
- package/.speccrew/skills/speccrew-knowledge-bizs-api-analyze/SKILL.md +57 -508
- package/.speccrew/skills/{speccrew-knowledge-bizs-api-analyze-xml/SKILL.md → speccrew-knowledge-bizs-api-analyze/workflow.agentflow.xml} +1 -154
- package/.speccrew/skills/speccrew-knowledge-bizs-api-graph/SKILL.md +73 -283
- package/.speccrew/skills/{speccrew-knowledge-bizs-api-graph-xml/SKILL.md → speccrew-knowledge-bizs-api-graph/workflow.agentflow.xml} +0 -298
- package/.speccrew/skills/speccrew-knowledge-bizs-dispatch/SKILL.md +931 -801
- package/.speccrew/skills/{speccrew-knowledge-bizs-dispatch-xml/SKILL.md → speccrew-knowledge-bizs-dispatch/workflow.agentflow.xml} +42 -272
- package/.speccrew/skills/speccrew-knowledge-bizs-identify-entries/SKILL.md +263 -71
- package/.speccrew/skills/{speccrew-knowledge-bizs-identify-entries-xml/SKILL.md → speccrew-knowledge-bizs-identify-entries/workflow.agentflow.xml} +8 -184
- package/.speccrew/skills/speccrew-knowledge-bizs-init-features/SKILL.md +200 -181
- package/.speccrew/skills/{speccrew-knowledge-bizs-init-features-xml/SKILL.md → speccrew-knowledge-bizs-init-features/workflow.agentflow.xml} +7 -134
- package/.speccrew/skills/speccrew-knowledge-bizs-module-classify/SKILL.md +5 -89
- package/.speccrew/skills/speccrew-knowledge-bizs-module-classify/workflow.agentflow.xml +129 -0
- package/.speccrew/skills/speccrew-knowledge-bizs-ui-analyze/SKILL.md +454 -326
- package/.speccrew/skills/{speccrew-knowledge-bizs-ui-analyze-xml/SKILL.md → speccrew-knowledge-bizs-ui-analyze/workflow.agentflow.xml} +8 -128
- package/.speccrew/skills/speccrew-knowledge-bizs-ui-graph/SKILL.md +302 -247
- package/.speccrew/skills/{speccrew-knowledge-bizs-ui-graph-xml/SKILL.md → speccrew-knowledge-bizs-ui-graph/workflow.agentflow.xml} +7 -199
- package/.speccrew/skills/speccrew-knowledge-bizs-ui-style-extract/SKILL.md +267 -156
- package/.speccrew/skills/{speccrew-knowledge-bizs-ui-style-extract-xml/SKILL.md → speccrew-knowledge-bizs-ui-style-extract/workflow.agentflow.xml} +7 -151
- package/.speccrew/skills/speccrew-knowledge-graph-query/SKILL.md +3 -122
- package/.speccrew/skills/speccrew-knowledge-graph-query/workflow.agentflow.xml +106 -0
- package/.speccrew/skills/speccrew-knowledge-graph-write/SKILL.md +3 -80
- package/.speccrew/skills/speccrew-knowledge-graph-write/workflow.agentflow.xml +152 -0
- package/.speccrew/skills/speccrew-knowledge-module-summarize/SKILL.md +371 -265
- package/.speccrew/skills/{speccrew-knowledge-module-summarize-xml/SKILL.md → speccrew-knowledge-module-summarize/workflow.agentflow.xml} +7 -197
- package/.speccrew/skills/speccrew-knowledge-system-summarize/SKILL.md +45 -333
- package/.speccrew/skills/{speccrew-knowledge-system-summarize-xml/SKILL.md → speccrew-knowledge-system-summarize/workflow.agentflow.xml} +0 -177
- package/.speccrew/skills/speccrew-knowledge-techs-dispatch/SKILL.md +174 -727
- package/.speccrew/skills/{speccrew-knowledge-techs-dispatch-xml/SKILL.md → speccrew-knowledge-techs-dispatch/workflow.agentflow.xml} +10 -351
- package/.speccrew/skills/speccrew-knowledge-techs-generate/SKILL.md +20 -150
- package/.speccrew/skills/{speccrew-knowledge-techs-generate-xml/SKILL.md → speccrew-knowledge-techs-generate/workflow.agentflow.xml} +0 -169
- package/.speccrew/skills/speccrew-knowledge-techs-generate-conventions/SKILL.md +75 -587
- package/.speccrew/skills/{speccrew-knowledge-techs-generate-conventions-xml/SKILL.md → speccrew-knowledge-techs-generate-conventions/workflow.agentflow.xml} +0 -153
- package/.speccrew/skills/speccrew-knowledge-techs-generate-quality/SKILL.md +463 -297
- package/.speccrew/skills/{speccrew-knowledge-techs-generate-quality-xml/SKILL.md → speccrew-knowledge-techs-generate-quality/workflow.agentflow.xml} +0 -164
- package/.speccrew/skills/speccrew-knowledge-techs-generate-ui-style/SKILL.md +57 -292
- package/.speccrew/skills/{speccrew-knowledge-techs-generate-ui-style-xml/SKILL.md → speccrew-knowledge-techs-generate-ui-style/workflow.agentflow.xml} +2 -193
- package/.speccrew/skills/speccrew-knowledge-techs-index/SKILL.md +49 -335
- package/.speccrew/skills/{speccrew-knowledge-techs-index-xml/SKILL.md → speccrew-knowledge-techs-index/workflow.agentflow.xml} +0 -167
- package/.speccrew/skills/speccrew-knowledge-techs-init/SKILL.md +28 -109
- package/.speccrew/skills/{speccrew-knowledge-techs-init-xml/SKILL.md → speccrew-knowledge-techs-init/workflow.agentflow.xml} +0 -189
- package/.speccrew/skills/speccrew-knowledge-techs-ui-analyze/SKILL.md +3 -487
- package/.speccrew/skills/speccrew-knowledge-techs-ui-analyze/workflow.agentflow.xml +278 -0
- package/.speccrew/skills/speccrew-pm-knowledge-detector/SKILL.md +3 -71
- package/.speccrew/skills/speccrew-pm-knowledge-detector/workflow.agentflow.xml +108 -0
- package/.speccrew/skills/speccrew-pm-module-initializer/SKILL.md +3 -107
- package/.speccrew/skills/speccrew-pm-module-initializer/workflow.agentflow.xml +139 -0
- package/.speccrew/skills/speccrew-pm-module-matcher/SKILL.md +3 -115
- package/.speccrew/skills/speccrew-pm-module-matcher/workflow.agentflow.xml +146 -0
- package/.speccrew/skills/speccrew-pm-requirement-analysis/SKILL.md +3 -343
- package/.speccrew/skills/speccrew-pm-requirement-analysis/workflow.agentflow.xml +174 -0
- package/.speccrew/skills/speccrew-pm-requirement-assess/SKILL.md +3 -91
- package/.speccrew/skills/speccrew-pm-requirement-assess/workflow.agentflow.xml +173 -0
- package/.speccrew/skills/speccrew-pm-requirement-clarify/SKILL.md +3 -224
- package/.speccrew/skills/speccrew-pm-requirement-clarify/workflow.agentflow.xml +159 -0
- package/.speccrew/skills/speccrew-pm-requirement-model/SKILL.md +3 -275
- package/.speccrew/skills/speccrew-pm-requirement-model/workflow.agentflow.xml +210 -0
- package/.speccrew/skills/speccrew-pm-requirement-simple/SKILL.md +3 -76
- package/.speccrew/skills/speccrew-pm-requirement-simple/workflow.agentflow.xml +120 -0
- package/.speccrew/skills/speccrew-pm-sub-prd-generate/SKILL.md +7 -1
- package/.speccrew/skills/speccrew-pm-sub-prd-generate/workflow.agentflow.xml +218 -0
- package/.speccrew/skills/speccrew-sd-backend/SKILL.md +7 -1
- package/.speccrew/skills/speccrew-sd-backend/workflow.agentflow.xml +264 -0
- package/.speccrew/skills/speccrew-sd-desktop/SKILL.md +7 -1
- package/.speccrew/skills/speccrew-sd-desktop/workflow.agentflow.xml +288 -0
- package/.speccrew/skills/speccrew-sd-framework-evaluate/SKILL.md +7 -1
- package/.speccrew/skills/speccrew-sd-framework-evaluate/workflow.agentflow.xml +235 -0
- package/.speccrew/skills/speccrew-sd-frontend/SKILL.md +7 -1
- package/.speccrew/skills/speccrew-sd-frontend/workflow.agentflow.xml +299 -0
- package/.speccrew/skills/speccrew-sd-mobile/SKILL.md +7 -1
- package/.speccrew/skills/speccrew-sd-mobile/workflow.agentflow.xml +301 -0
- package/.speccrew/skills/speccrew-test-case-design/SKILL.md +165 -284
- package/.speccrew/skills/speccrew-test-case-design/workflow.agentflow.xml +210 -0
- package/.speccrew/skills/speccrew-test-code-gen/SKILL.md +204 -324
- package/.speccrew/skills/speccrew-test-code-gen/workflow.agentflow.xml +265 -0
- package/.speccrew/skills/speccrew-test-reporter/SKILL.md +205 -184
- package/.speccrew/skills/speccrew-test-reporter/workflow.agentflow.xml +284 -0
- package/.speccrew/skills/speccrew-test-runner/SKILL.md +242 -241
- package/.speccrew/skills/speccrew-test-runner/workflow.agentflow.xml +314 -0
- package/bin/cli.js +8 -1
- package/lib/commands/init.js +11 -3
- package/lib/commands/update.js +11 -3
- package/lib/commands/validate.js +565 -0
- package/lib/utils.js +43 -0
- package/package.json +1 -1
- package/workspace-template/docs/rules/{xml-workflow-spec.md → agentflow-spec.md} +5 -5
- package/workspace-template/scripts/validate-agentflow.js +637 -0
- package/.speccrew/agents/speccrew-team-leader-xml.md +0 -480
- package/.speccrew/skills/speccrew-knowledge-bizs-dispatch/STATUS-FORMATS.md +0 -99
- package/.speccrew/skills/speccrew-knowledge-bizs-dispatch/scripts/batch-orchestrator.js +0 -176
- package/.speccrew/skills/speccrew-knowledge-bizs-dispatch/scripts/get-next-batch.js +0 -150
- package/.speccrew/skills/speccrew-knowledge-bizs-dispatch/scripts/get-pending-features.js +0 -106
- package/.speccrew/skills/speccrew-knowledge-bizs-dispatch/scripts/mark-stale.js +0 -249
- package/.speccrew/skills/speccrew-knowledge-bizs-dispatch/scripts/merge-features.js +0 -300
- package/.speccrew/skills/speccrew-knowledge-bizs-dispatch/scripts/process-batch-results.js +0 -915
- package/.speccrew/skills/speccrew-knowledge-bizs-dispatch/scripts/update-feature-status.js +0 -226
- package/.speccrew/skills/speccrew-knowledge-bizs-init-features/examples/features.json +0 -34
- package/.speccrew/skills/speccrew-knowledge-bizs-init-features/scripts/generate-inventory.js +0 -1087
- package/.speccrew/skills/speccrew-knowledge-bizs-init-features/scripts/test-inventory.js +0 -26
- package/.speccrew/skills/speccrew-knowledge-techs-dispatch/STATUS-FORMATS.md +0 -550
package/.speccrew/skills/speccrew-knowledge-bizs-init-features/scripts/generate-inventory.js
DELETED
|
@@ -1,1087 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Generate features.json for UI feature analysis
|
|
4
|
-
*
|
|
5
|
-
* Scans source directory for page files and generates a flat feature list with analysis status tracking.
|
|
6
|
-
* All configuration is passed via parameters - the script does not infer anything.
|
|
7
|
-
*
|
|
8
|
-
* Usage: node generate-inventory.js --sourcePath <path> --outputFileName <name> --platformName <name> --platformType <type> --techStack <json> --fileExtensions <json> [--platformSubtype <subtype>] [--techIdentifier <identifier>] [--analysisMethod <method>] [--excludeDirs <json>] [--includeDataObjects <true|false>] [--outputDir <dir>]
|
|
9
|
-
* Array parameters (--techStack, --fileExtensions, --excludeDirs) accept both JSON format and comma-separated format.
|
|
10
|
-
*
|
|
11
|
-
* Whitelist Mode (using --entryDirsFile):
|
|
12
|
-
* When --entryDirsFile is provided, the script operates in whitelist mode:
|
|
13
|
-
* - Reads entry-dirs JSON file with platformId, sourcePath, and modules array
|
|
14
|
-
* - Loads platform config from platform-mapping.json
|
|
15
|
-
* - Scans only the specified entryDirs for each module
|
|
16
|
-
* - Generates features-{platformId}.json
|
|
17
|
-
*
|
|
18
|
-
* Data Object Exclusion (backend only):
|
|
19
|
-
* By default, files ending with configured suffixes (e.g., VO/DTO/DO/Entity/Convert for Spring) are excluded for backend platforms.
|
|
20
|
-
* The suffixes are read from tech-stack-mappings.json (exclude_file_suffixes field).
|
|
21
|
-
* Use --includeDataObjects true to include them.
|
|
22
|
-
*
|
|
23
|
-
* Output Directory (--outputDir):
|
|
24
|
-
* By default, the script uses findProjectRoot() to locate the project root and outputs to:
|
|
25
|
-
* <projectRoot>/speccrew-workspace/knowledges/base/sync-state/knowledge-bizs/
|
|
26
|
-
* Use --outputDir to explicitly specify the output directory, bypassing findProjectRoot().
|
|
27
|
-
*
|
|
28
|
-
* Example (full scan mode):
|
|
29
|
-
* node generate-inventory.js \
|
|
30
|
-
* --sourcePath "src/views" \
|
|
31
|
-
* --outputFileName "features-web.json" \
|
|
32
|
-
* --platformName "Web Frontend" \
|
|
33
|
-
* --platformType "web" \
|
|
34
|
-
* --platformSubtype "vue" \
|
|
35
|
-
* --techIdentifier "vue" \
|
|
36
|
-
* --techStack "vue,typescript" \
|
|
37
|
-
* --fileExtensions ".vue,.ts" \
|
|
38
|
-
* --analysisMethod "ui-based" \
|
|
39
|
-
* --excludeDirs "components,composables,hooks,utils"
|
|
40
|
-
*
|
|
41
|
-
* Example (whitelist mode):
|
|
42
|
-
* node generate-inventory.js \
|
|
43
|
-
* --entryDirsFile "entry-dirs.json"
|
|
44
|
-
*
|
|
45
|
-
* entry-dirs.json format:
|
|
46
|
-
* {
|
|
47
|
-
* "platformId": "backend-ai",
|
|
48
|
-
* "platformName": "AI Module Backend",
|
|
49
|
-
* "platformType": "backend",
|
|
50
|
-
* "platformSubtype": "ai",
|
|
51
|
-
* "sourcePath": "yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai",
|
|
52
|
-
* "techStack": ["spring-boot", "mybatis-plus"],
|
|
53
|
-
* "modules": [
|
|
54
|
-
* { "name": "chat", "entryDirs": ["controller/admin/chat"] },
|
|
55
|
-
* { "name": "image", "entryDirs": ["controller/admin/image"] }
|
|
56
|
-
* ]
|
|
57
|
-
* }
|
|
58
|
-
*
|
|
59
|
-
* Optional fields (auto-inferred if missing):
|
|
60
|
-
* - platformName: defaults to "{platformType}-{platformSubtype}"
|
|
61
|
-
* - platformType: inferred from platformId (e.g., "backend-ai" → "backend")
|
|
62
|
-
* - platformSubtype: inferred from platformId (e.g., "backend-ai" → "ai")
|
|
63
|
-
* - techStack: defaults based on platformType (backend→["spring-boot"], web→["vue"], mobile→["uniapp"])
|
|
64
|
-
*/
|
|
65
|
-
|
|
66
|
-
const fs = require('fs');
|
|
67
|
-
const path = require('path');
|
|
68
|
-
|
|
69
|
-
// Platform type to tech-stack-mappings category mapping
|
|
70
|
-
const PLATFORM_TYPE_TO_CATEGORY = {
|
|
71
|
-
'frontend': 'web',
|
|
72
|
-
'web': 'web',
|
|
73
|
-
'backend': 'backend',
|
|
74
|
-
'mobile': 'mobile',
|
|
75
|
-
'desktop': 'desktop',
|
|
76
|
-
'api': 'api'
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Parse array parameter that supports both JSON format and comma-separated format.
|
|
81
|
-
* JSON format: '["vue","typescript"]'
|
|
82
|
-
* Comma-separated format: "vue,typescript" (recommended for PowerShell compatibility)
|
|
83
|
-
*/
|
|
84
|
-
function parseArrayParam(value) {
|
|
85
|
-
// Handle boolean true (from flag-only args like --excludeDirs without value)
|
|
86
|
-
if (value === true) return [];
|
|
87
|
-
if (!value) return [];
|
|
88
|
-
const trimmed = value.trim();
|
|
89
|
-
if (trimmed.startsWith('[')) {
|
|
90
|
-
try {
|
|
91
|
-
return JSON.parse(trimmed);
|
|
92
|
-
} catch (e) {
|
|
93
|
-
// PowerShell may strip quotes: [vue,typescript] → parse as comma-separated
|
|
94
|
-
return trimmed.slice(1, -1).split(',').map(s => s.trim()).filter(Boolean);
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
return trimmed.split(',').map(s => s.trim()).filter(Boolean);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// Parse command line arguments
|
|
101
|
-
function parseArgs() {
|
|
102
|
-
const args = process.argv.slice(2);
|
|
103
|
-
const params = {};
|
|
104
|
-
|
|
105
|
-
for (let i = 0; i < args.length; i++) {
|
|
106
|
-
const arg = args[i];
|
|
107
|
-
if (arg.startsWith('--')) {
|
|
108
|
-
const key = arg.slice(2);
|
|
109
|
-
const value = args[i + 1];
|
|
110
|
-
// Accept empty string "" as valid value, only skip if undefined or next arg is a flag
|
|
111
|
-
if (value !== undefined && !value.startsWith('--')) {
|
|
112
|
-
params[key] = value;
|
|
113
|
-
i++;
|
|
114
|
-
} else {
|
|
115
|
-
params[key] = true;
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
return params;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* Parse boolean parameter from string value
|
|
125
|
-
* @param {string|boolean} value - Parameter value
|
|
126
|
-
* @param {boolean} defaultValue - Default value if not provided
|
|
127
|
-
* @returns {boolean} Parsed boolean value
|
|
128
|
-
*/
|
|
129
|
-
function parseBooleanParam(value, defaultValue = false) {
|
|
130
|
-
if (value === undefined || value === null) return defaultValue;
|
|
131
|
-
if (typeof value === 'boolean') return value;
|
|
132
|
-
if (typeof value === 'string') {
|
|
133
|
-
return value.toLowerCase() === 'true';
|
|
134
|
-
}
|
|
135
|
-
return defaultValue;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// Normalize path separators to forward slashes
|
|
139
|
-
function normalizePath(filePath) {
|
|
140
|
-
if (!filePath) return '';
|
|
141
|
-
return filePath.replace(/\\/g, '/');
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// Find project root by searching upward for speccrew-workspace directory
|
|
145
|
-
function findProjectRoot(startPath) {
|
|
146
|
-
let currentDir = path.resolve(startPath);
|
|
147
|
-
const root = path.parse(currentDir).root;
|
|
148
|
-
|
|
149
|
-
while (currentDir !== root) {
|
|
150
|
-
const workspaceDir = path.join(currentDir, 'speccrew-workspace');
|
|
151
|
-
if (fs.existsSync(workspaceDir) && fs.statSync(workspaceDir).isDirectory()) {
|
|
152
|
-
return currentDir;
|
|
153
|
-
}
|
|
154
|
-
currentDir = path.dirname(currentDir);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// Fallback: return source path's drive root
|
|
158
|
-
return root;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// Check if path contains any excluded directory
|
|
162
|
-
function isExcludedPath(filePath, excludeDirs) {
|
|
163
|
-
const parts = normalizePath(filePath).split('/').filter(p => p);
|
|
164
|
-
for (const part of parts) {
|
|
165
|
-
if (excludeDirs.includes(part)) {
|
|
166
|
-
return true;
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
return false;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// Check if file is a data object class (VO/DTO/DO/Entity/Convert) - should be excluded for backend
|
|
173
|
-
function isDataObjectFile(fileName, extension, excludeSuffixes) {
|
|
174
|
-
// If no suffixes configured, don't exclude any files
|
|
175
|
-
if (!excludeSuffixes || excludeSuffixes.length === 0) {
|
|
176
|
-
return false;
|
|
177
|
-
}
|
|
178
|
-
// Check if fileName ends with any configured suffix
|
|
179
|
-
for (const suffix of excludeSuffixes) {
|
|
180
|
-
if (fileName.endsWith(suffix)) {
|
|
181
|
-
return true;
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
return false;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
// Check if file name matches exact exclusion list (e.g., package-info)
|
|
188
|
-
function isExcludedFileName(fileName, excludeFileNames) {
|
|
189
|
-
if (!excludeFileNames || excludeFileNames.length === 0) {
|
|
190
|
-
return false;
|
|
191
|
-
}
|
|
192
|
-
return excludeFileNames.includes(fileName);
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// Get module name (first non-excluded directory level)
|
|
196
|
-
function getModuleName(dirPath, excludeDirs, fallbackModuleName) {
|
|
197
|
-
const parts = normalizePath(dirPath).split('/').filter(p => p && p !== '.');
|
|
198
|
-
for (const part of parts) {
|
|
199
|
-
if (!excludeDirs.includes(part)) {
|
|
200
|
-
return part;
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
// All parts were excluded (e.g., "src/App.vue" with src excluded)
|
|
204
|
-
// Return "_root" to indicate framework/root-level files
|
|
205
|
-
return '_root';
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/**
|
|
209
|
-
* Load platform configuration from platform-mapping.json
|
|
210
|
-
* @param {string} platformId - Platform ID like "backend-ai", "web-vue"
|
|
211
|
-
* @param {string} projectRoot - Project root directory
|
|
212
|
-
* @returns {object|null} Platform config with platformName, platformType, techStack, extensions, or null if not found
|
|
213
|
-
*/
|
|
214
|
-
function loadPlatformConfig(platformId, projectRoot) {
|
|
215
|
-
const configPath = path.join(projectRoot, 'speccrew-workspace', 'docs', 'configs', 'platform-mapping.json');
|
|
216
|
-
if (!fs.existsSync(configPath)) {
|
|
217
|
-
console.error(`Error: platform-mapping.json not found at ${configPath}`);
|
|
218
|
-
return null;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
const configContent = fs.readFileSync(configPath, 'utf8');
|
|
222
|
-
const config = JSON.parse(configContent);
|
|
223
|
-
|
|
224
|
-
// Find platform mapping by platform_id
|
|
225
|
-
const mapping = config.mappings.find(m => m.platform_id === platformId);
|
|
226
|
-
if (!mapping) {
|
|
227
|
-
console.error(`Error: Platform ID "${platformId}" not found in platform-mapping.json`);
|
|
228
|
-
return null;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
// Build platform config from mapping
|
|
232
|
-
const platformConfig = {
|
|
233
|
-
platformId: mapping.platform_id,
|
|
234
|
-
platformType: mapping.platform_type,
|
|
235
|
-
platformSubtype: mapping.platform_subtype || mapping.framework,
|
|
236
|
-
framework: mapping.framework,
|
|
237
|
-
platformName: `${mapping.platform_type}-${mapping.framework}` // Default name, can be customized
|
|
238
|
-
};
|
|
239
|
-
|
|
240
|
-
return platformConfig;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
/**
|
|
244
|
-
* Normalize tech identifier by removing common language prefixes.
|
|
245
|
-
* @param {string} id - Tech identifier like "python-fastapi", "node-express"
|
|
246
|
-
* @returns {string} Normalized identifier like "fastapi", "express"
|
|
247
|
-
*/
|
|
248
|
-
function normalizeTechIdentifier(id) {
|
|
249
|
-
if (!id) return id;
|
|
250
|
-
// Remove common language prefixes: python-, node-, java-, etc.
|
|
251
|
-
const prefixes = ['python-', 'node-', 'java-', 'kotlin-', 'swift-', 'dart-', 'go-', 'rust-', 'php-', 'ruby-'];
|
|
252
|
-
for (const prefix of prefixes) {
|
|
253
|
-
if (id.toLowerCase().startsWith(prefix)) {
|
|
254
|
-
return id.substring(prefix.length);
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
return id;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
/**
|
|
261
|
-
* Load tech stack configuration from tech-stack-mappings.json
|
|
262
|
-
* @param {string} platformType - Platform type like "backend", "web"
|
|
263
|
-
* @param {string} framework - Framework like "spring", "vue"
|
|
264
|
-
* @param {string} projectRoot - Project root directory
|
|
265
|
-
* @returns {object} Tech config with extensions and exclude_file_suffixes
|
|
266
|
-
*/
|
|
267
|
-
function loadTechStackConfig(platformType, framework, projectRoot) {
|
|
268
|
-
const configPath = path.join(projectRoot, 'speccrew-workspace', 'docs', 'configs', 'tech-stack-mappings.json');
|
|
269
|
-
if (!fs.existsSync(configPath)) {
|
|
270
|
-
console.error(`Warning: tech-stack-mappings.json not found at ${configPath}`);
|
|
271
|
-
return { extensions: [], exclude_file_suffixes: [] };
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
const configContent = fs.readFileSync(configPath, 'utf8');
|
|
275
|
-
const config = JSON.parse(configContent);
|
|
276
|
-
|
|
277
|
-
// Map platformType to tech-stack-mappings category
|
|
278
|
-
const techCategory = PLATFORM_TYPE_TO_CATEGORY[platformType] || platformType;
|
|
279
|
-
|
|
280
|
-
// Try exact match first
|
|
281
|
-
let techConfig = null;
|
|
282
|
-
if (config.tech_stacks &&
|
|
283
|
-
config.tech_stacks[techCategory] &&
|
|
284
|
-
config.tech_stacks[techCategory][framework]) {
|
|
285
|
-
techConfig = config.tech_stacks[techCategory][framework];
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
// If not found, try normalized identifier (remove language prefix)
|
|
289
|
-
if (!techConfig) {
|
|
290
|
-
const normalizedFramework = normalizeTechIdentifier(framework);
|
|
291
|
-
if (normalizedFramework !== framework &&
|
|
292
|
-
config.tech_stacks &&
|
|
293
|
-
config.tech_stacks[techCategory] &&
|
|
294
|
-
config.tech_stacks[techCategory][normalizedFramework]) {
|
|
295
|
-
techConfig = config.tech_stacks[techCategory][normalizedFramework];
|
|
296
|
-
console.log(`Using normalized tech identifier: ${framework} → ${normalizedFramework}`);
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
if (techConfig) {
|
|
301
|
-
return {
|
|
302
|
-
extensions: techConfig.extensions || [],
|
|
303
|
-
exclude_file_suffixes: techConfig.exclude_file_suffixes || [],
|
|
304
|
-
exclude_file_names: techConfig.exclude_file_names || []
|
|
305
|
-
};
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
return { extensions: [], exclude_file_suffixes: [], exclude_file_names: [] };
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
/**
|
|
312
|
-
* Validate entry-dirs JSON schema
|
|
313
|
-
* @param {object} data - Parsed entry-dirs JSON data
|
|
314
|
-
* @returns {string[]|null} Array of error messages or null if valid
|
|
315
|
-
*/
|
|
316
|
-
function validateEntryDirsSchema(data) {
|
|
317
|
-
const errors = [];
|
|
318
|
-
if (!data.platformId || typeof data.platformId !== 'string') {
|
|
319
|
-
errors.push('platformId must be a non-empty string');
|
|
320
|
-
} else if (!/^[a-zA-Z0-9_-]+$/.test(data.platformId)) {
|
|
321
|
-
errors.push(`platformId format invalid: "${data.platformId}" (only alphanumeric, hyphen, underscore allowed)`);
|
|
322
|
-
}
|
|
323
|
-
if (!data.sourcePath || typeof data.sourcePath !== 'string') {
|
|
324
|
-
errors.push('sourcePath must be a non-empty string');
|
|
325
|
-
}
|
|
326
|
-
if (!Array.isArray(data.modules) || data.modules.length === 0) {
|
|
327
|
-
errors.push('modules must be a non-empty array');
|
|
328
|
-
} else {
|
|
329
|
-
for (let i = 0; i < data.modules.length; i++) {
|
|
330
|
-
const mod = data.modules[i];
|
|
331
|
-
if (!mod.name || typeof mod.name !== 'string') {
|
|
332
|
-
errors.push(`modules[${i}].name must be a non-empty string`);
|
|
333
|
-
}
|
|
334
|
-
if (!Array.isArray(mod.entryDirs) || mod.entryDirs.length === 0) {
|
|
335
|
-
errors.push(`modules[${i}].entryDirs must be a non-empty array`);
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
return errors.length > 0 ? errors : null;
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
/**
|
|
343
|
-
* Infer platform info from platformId
|
|
344
|
-
* @param {string} platformId - Platform ID like "backend-ai", "web-vue", "mobile-uniapp"
|
|
345
|
-
* @returns {object} Inferred platform info { platformType, platformSubtype }
|
|
346
|
-
*/
|
|
347
|
-
function inferPlatformInfo(platformId) {
|
|
348
|
-
// Parse platformId: "{type}-{subtype}" format
|
|
349
|
-
const parts = platformId.split('-');
|
|
350
|
-
if (parts.length >= 2) {
|
|
351
|
-
return {
|
|
352
|
-
platformType: parts[0],
|
|
353
|
-
platformSubtype: parts.slice(1).join('-')
|
|
354
|
-
};
|
|
355
|
-
}
|
|
356
|
-
// Fallback: treat entire platformId as type
|
|
357
|
-
return {
|
|
358
|
-
platformType: platformId,
|
|
359
|
-
platformSubtype: ''
|
|
360
|
-
};
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
/**
|
|
364
|
-
* Infer framework from techStack array
|
|
365
|
-
* @param {string[]} techStack - Array of tech stack names
|
|
366
|
-
* @returns {string} Framework identifier like "spring", "vue", "uniapp"
|
|
367
|
-
*/
|
|
368
|
-
function inferFrameworkFromTechStack(techStack) {
|
|
369
|
-
if (!techStack || techStack.length === 0) {
|
|
370
|
-
return '';
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
// Mapping: tech stack name → framework identifier
|
|
374
|
-
const techToFramework = {
|
|
375
|
-
// Backend
|
|
376
|
-
'spring-boot': 'spring',
|
|
377
|
-
'spring': 'spring',
|
|
378
|
-
'springboot': 'spring',
|
|
379
|
-
'mybatis-plus': 'spring',
|
|
380
|
-
'mybatis': 'spring',
|
|
381
|
-
'jpa': 'spring',
|
|
382
|
-
// Frontend
|
|
383
|
-
'vue': 'vue',
|
|
384
|
-
'vue3': 'vue',
|
|
385
|
-
'vue2': 'vue',
|
|
386
|
-
'react': 'react',
|
|
387
|
-
'reactjs': 'react',
|
|
388
|
-
'nextjs': 'next',
|
|
389
|
-
'next.js': 'next',
|
|
390
|
-
'angular': 'angular',
|
|
391
|
-
// Mobile
|
|
392
|
-
'uniapp': 'uniapp',
|
|
393
|
-
'uni-app': 'uniapp',
|
|
394
|
-
'flutter': 'flutter',
|
|
395
|
-
'react-native': 'react-native',
|
|
396
|
-
'reactnative': 'react-native'
|
|
397
|
-
};
|
|
398
|
-
|
|
399
|
-
// Platform-specific frameworks have higher priority than generic ones
|
|
400
|
-
const platformSpecific = new Set(['uniapp', 'flutter', 'react-native', 'next']);
|
|
401
|
-
|
|
402
|
-
// First pass: look for platform-specific framework match
|
|
403
|
-
for (const tech of techStack) {
|
|
404
|
-
const normalizedTech = tech.toLowerCase();
|
|
405
|
-
const framework = techToFramework[normalizedTech];
|
|
406
|
-
if (framework && platformSpecific.has(framework)) {
|
|
407
|
-
return framework;
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
// Second pass: fallback to first matching generic framework
|
|
412
|
-
for (const tech of techStack) {
|
|
413
|
-
const normalizedTech = tech.toLowerCase();
|
|
414
|
-
if (techToFramework[normalizedTech]) {
|
|
415
|
-
return techToFramework[normalizedTech];
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
// Fallback: use first tech stack as framework
|
|
420
|
-
return techStack[0].toLowerCase();
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
/**
|
|
424
|
-
* Get default techStack for platformType
|
|
425
|
-
* @param {string} platformType - Platform type like "backend", "web", "mobile"
|
|
426
|
-
* @returns {string[]} Default tech stack array
|
|
427
|
-
*/
|
|
428
|
-
function getDefaultTechStack(platformType) {
|
|
429
|
-
const defaults = {
|
|
430
|
-
backend: ['spring-boot'],
|
|
431
|
-
web: ['vue'],
|
|
432
|
-
mobile: ['uniapp'],
|
|
433
|
-
desktop: ['electron']
|
|
434
|
-
};
|
|
435
|
-
return defaults[platformType] || [];
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
/**
|
|
439
|
-
* Find files in a specific entry directory (non-recursive, just the directory itself)
|
|
440
|
-
* @param {string} dir - Directory to scan
|
|
441
|
-
* @param {string[]} extensions - File extensions to match
|
|
442
|
-
* @param {string} baseDir - Base directory for relative paths
|
|
443
|
-
* @returns {object[]} Array of file objects
|
|
444
|
-
*/
|
|
445
|
-
function findFilesInDir(dir, extensions, baseDir) {
|
|
446
|
-
const files = [];
|
|
447
|
-
|
|
448
|
-
if (!fs.existsSync(dir)) {
|
|
449
|
-
return files;
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
const items = fs.readdirSync(dir);
|
|
453
|
-
|
|
454
|
-
for (const item of items) {
|
|
455
|
-
const fullPath = path.join(dir, item);
|
|
456
|
-
const stat = fs.statSync(fullPath);
|
|
457
|
-
|
|
458
|
-
if (stat.isFile()) {
|
|
459
|
-
const ext = path.extname(item);
|
|
460
|
-
if (extensions.includes(ext)) {
|
|
461
|
-
const relativePath = normalizePath(path.relative(baseDir, fullPath));
|
|
462
|
-
files.push({
|
|
463
|
-
fullPath,
|
|
464
|
-
relativePath,
|
|
465
|
-
fileName: path.basename(item, ext),
|
|
466
|
-
extension: ext,
|
|
467
|
-
directory: path.dirname(relativePath),
|
|
468
|
-
lastModified: stat.mtime.toISOString()
|
|
469
|
-
});
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
return files;
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
/**
|
|
478
|
-
* Generate features.json from entry-dirs whitelist mode
|
|
479
|
-
* @param {object} entryDirsData - Entry directories data from JSON file
|
|
480
|
-
* @param {object} platformConfig - Platform configuration
|
|
481
|
-
* @param {string} projectRoot - Project root directory
|
|
482
|
-
* @param {string} outputDir - Output directory for features JSON
|
|
483
|
-
* @returns {boolean} Success status
|
|
484
|
-
*/
|
|
485
|
-
function generateFromEntryDirs(entryDirsData, platformConfig, projectRoot, outputDir, overwrite) {
|
|
486
|
-
const { platformId, sourcePath, modules } = entryDirsData;
|
|
487
|
-
const { platformType, platformSubtype, framework } = platformConfig;
|
|
488
|
-
|
|
489
|
-
// Load tech stack config for extensions and exclude_file_suffixes
|
|
490
|
-
const techConfig = loadTechStackConfig(platformType, framework, projectRoot);
|
|
491
|
-
const { extensions, exclude_file_suffixes, exclude_file_names } = techConfig;
|
|
492
|
-
|
|
493
|
-
if (extensions.length === 0) {
|
|
494
|
-
console.error(`Error: No extensions found for ${platformType}/${framework} in tech-stack-mappings.json`);
|
|
495
|
-
return false;
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
console.log(`Whitelist mode: Platform ${platformId}`);
|
|
499
|
-
console.log(`Source path: ${sourcePath}`);
|
|
500
|
-
console.log(`Extensions: ${extensions.join(', ')}`);
|
|
501
|
-
if (exclude_file_suffixes.length > 0) {
|
|
502
|
-
console.log(`Exclude suffixes: ${exclude_file_suffixes.join(', ')}`);
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
// Resolve absolute source path
|
|
506
|
-
const absoluteSourcePath = path.resolve(projectRoot, sourcePath);
|
|
507
|
-
if (!fs.existsSync(absoluteSourcePath)) {
|
|
508
|
-
console.error(`Error: Source path does not exist: ${absoluteSourcePath}`);
|
|
509
|
-
return false;
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
// Collect all features
|
|
513
|
-
const features = [];
|
|
514
|
-
const moduleNames = [];
|
|
515
|
-
|
|
516
|
-
for (const module of modules) {
|
|
517
|
-
const { name: moduleName, entryDirs } = module;
|
|
518
|
-
moduleNames.push(moduleName);
|
|
519
|
-
|
|
520
|
-
for (const entryDir of entryDirs) {
|
|
521
|
-
// entryDir is relative to sourcePath
|
|
522
|
-
const entryFullPath = path.join(absoluteSourcePath, entryDir);
|
|
523
|
-
|
|
524
|
-
if (!fs.existsSync(entryFullPath)) {
|
|
525
|
-
console.log(` Skipping non-existent entry: ${entryDir}`);
|
|
526
|
-
continue;
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
// Scan files in the entry directory (recursive for web/mobile platforms with nested dirs)
|
|
530
|
-
const excludeDirs = techConfig.exclude_dirs || [];
|
|
531
|
-
const files = findFiles(entryFullPath, extensions, excludeDirs, absoluteSourcePath);
|
|
532
|
-
|
|
533
|
-
for (const file of files) {
|
|
534
|
-
// Apply exclude_file_suffixes filter
|
|
535
|
-
if (isDataObjectFile(file.fileName, file.extension, exclude_file_suffixes)) {
|
|
536
|
-
continue;
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
// Apply exclude_file_names filter (e.g., package-info)
|
|
540
|
-
if (isExcludedFileName(file.fileName, exclude_file_names)) {
|
|
541
|
-
continue;
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
// Build feature ID: moduleName-entryDirSegs-fileName
|
|
545
|
-
// entryDir like "controller/admin/chat" → "controller-admin-chat"
|
|
546
|
-
const entryDirNormalized = normalizePath(entryDir).replace(/[\/\\]/g, '-');
|
|
547
|
-
const featureId = `${moduleName}-${entryDirNormalized}-${file.fileName}`;
|
|
548
|
-
|
|
549
|
-
// Build relative file path from sourcePath
|
|
550
|
-
const relativeFilePath = normalizePath(path.relative(projectRoot, file.fullPath));
|
|
551
|
-
|
|
552
|
-
// Build document path
|
|
553
|
-
const docPath = `speccrew-workspace/knowledges/bizs/${platformType}-${platformSubtype}/${moduleName}/${file.fileName}.md`;
|
|
554
|
-
|
|
555
|
-
const feature = {
|
|
556
|
-
id: featureId,
|
|
557
|
-
fileName: file.fileName,
|
|
558
|
-
sourcePath: relativeFilePath,
|
|
559
|
-
documentPath: docPath,
|
|
560
|
-
module: moduleName,
|
|
561
|
-
lastModified: file.lastModified,
|
|
562
|
-
analyzed: false,
|
|
563
|
-
startedAt: null,
|
|
564
|
-
completedAt: null,
|
|
565
|
-
analysisNotes: null
|
|
566
|
-
};
|
|
567
|
-
features.push(feature);
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
console.log(`Found ${features.length} features across ${moduleNames.length} modules`);
|
|
573
|
-
|
|
574
|
-
// Build inventory object
|
|
575
|
-
const inventory = {
|
|
576
|
-
platformId: platformId,
|
|
577
|
-
platformName: platformConfig.platformName,
|
|
578
|
-
platformType: platformType,
|
|
579
|
-
sourcePath: sourcePath,
|
|
580
|
-
techStack: entryDirsData.techStack || [framework],
|
|
581
|
-
modules: [...new Set(moduleNames)].sort(),
|
|
582
|
-
totalFiles: features.length,
|
|
583
|
-
analyzedCount: 0,
|
|
584
|
-
pendingCount: features.length,
|
|
585
|
-
generatedAt: new Date().toISOString().replace(/[-:]/g, '').slice(0, 15).replace('T', '-'),
|
|
586
|
-
analysisMethod: 'api-based',
|
|
587
|
-
features: features
|
|
588
|
-
};
|
|
589
|
-
|
|
590
|
-
// Add platformSubtype if present
|
|
591
|
-
if (platformSubtype) {
|
|
592
|
-
inventory.platformSubtype = platformSubtype;
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
// Add techIdentifier
|
|
596
|
-
inventory.techIdentifier = framework;
|
|
597
|
-
|
|
598
|
-
// Write output file
|
|
599
|
-
const outputFileName = `features-${platformId}.json`;
|
|
600
|
-
const outputPath = path.join(outputDir, outputFileName);
|
|
601
|
-
|
|
602
|
-
// Incremental: if features file already exists and overwrite is not set, write to *.new.json
|
|
603
|
-
const actualOutputPath = (!overwrite && fs.existsSync(outputPath))
|
|
604
|
-
? outputPath.replace(/\.json$/, '.new.json')
|
|
605
|
-
: outputPath;
|
|
606
|
-
|
|
607
|
-
// Ensure output directory exists
|
|
608
|
-
if (!fs.existsSync(outputDir)) {
|
|
609
|
-
fs.mkdirSync(outputDir, { recursive: true });
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
fs.writeFileSync(actualOutputPath, JSON.stringify(inventory, null, 2), 'utf8');
|
|
613
|
-
|
|
614
|
-
if (actualOutputPath !== outputPath) {
|
|
615
|
-
console.log(`Incremental: Generated ${path.basename(actualOutputPath)} (existing features detected)`);
|
|
616
|
-
} else {
|
|
617
|
-
console.log(`Full: Generated ${path.basename(actualOutputPath)} with ${features.length} features`);
|
|
618
|
-
}
|
|
619
|
-
console.log(`Output: ${actualOutputPath}`);
|
|
620
|
-
|
|
621
|
-
return true;
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
// Recursively find all files matching extensions
|
|
625
|
-
function findFiles(dir, extensions, excludeDirs, baseDir) {
|
|
626
|
-
const files = [];
|
|
627
|
-
const items = fs.readdirSync(dir);
|
|
628
|
-
|
|
629
|
-
for (const item of items) {
|
|
630
|
-
const fullPath = path.join(dir, item);
|
|
631
|
-
const relativePath = normalizePath(path.relative(baseDir, fullPath));
|
|
632
|
-
const stat = fs.statSync(fullPath);
|
|
633
|
-
|
|
634
|
-
if (stat.isDirectory()) {
|
|
635
|
-
// Skip excluded directories
|
|
636
|
-
if (excludeDirs.includes(item)) {
|
|
637
|
-
continue;
|
|
638
|
-
}
|
|
639
|
-
files.push(...findFiles(fullPath, extensions, excludeDirs, baseDir));
|
|
640
|
-
} else if (stat.isFile()) {
|
|
641
|
-
const ext = path.extname(item);
|
|
642
|
-
if (extensions.includes(ext)) {
|
|
643
|
-
files.push({
|
|
644
|
-
fullPath,
|
|
645
|
-
relativePath,
|
|
646
|
-
fileName: path.basename(item, ext),
|
|
647
|
-
extension: ext,
|
|
648
|
-
directory: path.dirname(relativePath),
|
|
649
|
-
lastModified: stat.mtime.toISOString()
|
|
650
|
-
});
|
|
651
|
-
}
|
|
652
|
-
}
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
return files;
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
// Main function
|
|
659
|
-
function main() {
|
|
660
|
-
const params = parseArgs();
|
|
661
|
-
|
|
662
|
-
// Parse overwrite parameter (default: false for backward compatibility)
|
|
663
|
-
const overwrite = parseBooleanParam(params.overwrite, false);
|
|
664
|
-
|
|
665
|
-
// Check for whitelist mode (--entryDirsFile provided)
|
|
666
|
-
if (params.entryDirsFile) {
|
|
667
|
-
// Whitelist mode: scan only specified entry directories
|
|
668
|
-
const entryDirsFilePath = path.resolve(params.entryDirsFile);
|
|
669
|
-
|
|
670
|
-
if (!fs.existsSync(entryDirsFilePath)) {
|
|
671
|
-
console.error(`Error: entryDirsFile does not exist: ${params.entryDirsFile}`);
|
|
672
|
-
process.exit(1);
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
// Read entry-dirs JSON file
|
|
676
|
-
let entryDirsData;
|
|
677
|
-
try {
|
|
678
|
-
const content = fs.readFileSync(entryDirsFilePath, 'utf8');
|
|
679
|
-
entryDirsData = JSON.parse(content);
|
|
680
|
-
} catch (e) {
|
|
681
|
-
console.error(`Error: Failed to parse entryDirsFile: ${e.message}`);
|
|
682
|
-
process.exit(1);
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
// Validate entryDirsData structure
|
|
686
|
-
if (!entryDirsData.platformId) {
|
|
687
|
-
console.error('Error: entryDirsFile missing required field "platformId"');
|
|
688
|
-
process.exit(1);
|
|
689
|
-
}
|
|
690
|
-
if (!entryDirsData.sourcePath) {
|
|
691
|
-
console.error('Error: entryDirsFile missing required field "sourcePath"');
|
|
692
|
-
process.exit(1);
|
|
693
|
-
}
|
|
694
|
-
// Check for common format mistakes
|
|
695
|
-
if (entryDirsData.businessModules && Array.isArray(entryDirsData.businessModules)) {
|
|
696
|
-
console.error('Error: entryDirsFile uses unsupported "businessModules" format.');
|
|
697
|
-
console.error('Expected: { "modules": [ { "name": "...", "entryDirs": ["..."] } ] }');
|
|
698
|
-
console.error('Received: { "businessModules": [...] }');
|
|
699
|
-
console.error('');
|
|
700
|
-
console.error('Fix: The entry-dirs JSON must use a flat "modules" array.');
|
|
701
|
-
console.error('Each module should have "name" (string) and "entryDirs" (array of strings).');
|
|
702
|
-
console.error('Sub-modules must be flattened into top-level entries.');
|
|
703
|
-
console.error('Re-run the identify-entries skill to regenerate with correct format.');
|
|
704
|
-
process.exit(1);
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
if (!entryDirsData.modules || !Array.isArray(entryDirsData.modules)) {
|
|
708
|
-
console.error('Error: entryDirsFile missing required field "modules" array.');
|
|
709
|
-
console.error('Expected format: { "platformId": "...", "modules": [ { "name": "...", "entryDirs": ["..."] } ] }');
|
|
710
|
-
const foundKeys = Object.keys(entryDirsData).join(', ');
|
|
711
|
-
console.error(`Found top-level keys: ${foundKeys}`);
|
|
712
|
-
process.exit(1);
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
// Validate entry-dirs JSON schema
|
|
716
|
-
const schemaErrors = validateEntryDirsSchema(entryDirsData);
|
|
717
|
-
if (schemaErrors) {
|
|
718
|
-
console.error('Error: entry-dirs JSON schema validation failed:');
|
|
719
|
-
schemaErrors.forEach(err => console.error(` - ${err}`));
|
|
720
|
-
process.exit(1);
|
|
721
|
-
}
|
|
722
|
-
|
|
723
|
-
// Find project root (use current directory or entryDirsFile directory)
|
|
724
|
-
const projectRoot = findProjectRoot(path.dirname(entryDirsFilePath));
|
|
725
|
-
|
|
726
|
-
// Build platform config from entryDirsData (no longer requires platform-mapping.json)
|
|
727
|
-
// Step 1: Get platformType and platformSubtype (from entryDirsData or infer from platformId)
|
|
728
|
-
let platformType = entryDirsData.platformType;
|
|
729
|
-
let platformSubtype = entryDirsData.platformSubtype;
|
|
730
|
-
|
|
731
|
-
if (!platformType || !platformSubtype) {
|
|
732
|
-
const inferred = inferPlatformInfo(entryDirsData.platformId);
|
|
733
|
-
if (!platformType) {
|
|
734
|
-
platformType = inferred.platformType;
|
|
735
|
-
console.log(`Inferred platformType from platformId: ${platformType}`);
|
|
736
|
-
}
|
|
737
|
-
if (!platformSubtype) {
|
|
738
|
-
platformSubtype = inferred.platformSubtype;
|
|
739
|
-
console.log(`Inferred platformSubtype from platformId: ${platformSubtype}`);
|
|
740
|
-
}
|
|
741
|
-
}
|
|
742
|
-
|
|
743
|
-
// Step 2: Get techStack (from entryDirsData or default based on platformType)
|
|
744
|
-
let techStack = entryDirsData.techStack;
|
|
745
|
-
if (!techStack || techStack.length === 0) {
|
|
746
|
-
techStack = getDefaultTechStack(platformType);
|
|
747
|
-
console.log(`Using default techStack for ${platformType}: [${techStack.join(', ')}]`);
|
|
748
|
-
}
|
|
749
|
-
|
|
750
|
-
// Step 3: Infer framework from techStack
|
|
751
|
-
const framework = inferFrameworkFromTechStack(techStack);
|
|
752
|
-
|
|
753
|
-
// Step 4: Get platformName (from entryDirsData or build from type/subtype)
|
|
754
|
-
const platformName = entryDirsData.platformName || `${platformType}-${platformSubtype}`;
|
|
755
|
-
|
|
756
|
-
// Build final platformConfig object
|
|
757
|
-
const platformConfig = {
|
|
758
|
-
platformId: entryDirsData.platformId,
|
|
759
|
-
platformType: platformType,
|
|
760
|
-
platformSubtype: platformSubtype,
|
|
761
|
-
framework: framework,
|
|
762
|
-
platformName: platformName
|
|
763
|
-
};
|
|
764
|
-
|
|
765
|
-
console.log(`Platform config: type=${platformType}, subtype=${platformSubtype}, framework=${framework}`);
|
|
766
|
-
|
|
767
|
-
// Set output directory (prefer --outputDir parameter, fallback to findProjectRoot)
|
|
768
|
-
let outputDir;
|
|
769
|
-
if (params.outputDir) {
|
|
770
|
-
outputDir = path.resolve(params.outputDir);
|
|
771
|
-
console.log(`Using outputDir from parameter: ${outputDir}`);
|
|
772
|
-
} else {
|
|
773
|
-
console.warn('WARNING: --outputDir not specified, falling back to default path.');
|
|
774
|
-
console.warn(' Recommended: explicitly pass --outputDir to avoid incorrect output location.');
|
|
775
|
-
outputDir = path.join(projectRoot, 'speccrew-workspace', 'knowledges', 'base', 'sync-state', 'knowledge-bizs');
|
|
776
|
-
console.warn(` Using fallback outputDir: ${outputDir}`);
|
|
777
|
-
}
|
|
778
|
-
|
|
779
|
-
// Generate features from entry dirs
|
|
780
|
-
const success = generateFromEntryDirs(entryDirsData, platformConfig, projectRoot, outputDir, overwrite);
|
|
781
|
-
process.exit(success ? 0 : 1);
|
|
782
|
-
}
|
|
783
|
-
|
|
784
|
-
// Required parameters (full scan mode)
|
|
785
|
-
const sourcePath = params.sourcePath;
|
|
786
|
-
const outputFileName = params.outputFileName;
|
|
787
|
-
const platformName = params.platformName;
|
|
788
|
-
const platformType = params.platformType;
|
|
789
|
-
const techStackStr = params.techStack;
|
|
790
|
-
const fileExtensionsStr = params.fileExtensions;
|
|
791
|
-
|
|
792
|
-
// Optional parameters
|
|
793
|
-
const platformSubtype = params.platformSubtype || '';
|
|
794
|
-
const techIdentifier = params.techIdentifier || platformSubtype;
|
|
795
|
-
const analysisMethod = params.analysisMethod || 'ui-based';
|
|
796
|
-
let excludeDirsStr = params.excludeDirs;
|
|
797
|
-
// --includeDataObjects: set to "true" to include VO/DTO/DO/Entity/Convert files (default: false)
|
|
798
|
-
const includeDataObjects = params.includeDataObjects === 'true';
|
|
799
|
-
|
|
800
|
-
// Resolve source path first to find project root
|
|
801
|
-
const resolvedSourcePath = path.resolve(sourcePath);
|
|
802
|
-
if (!fs.existsSync(resolvedSourcePath)) {
|
|
803
|
-
console.error(`Error: Source path does not exist: ${sourcePath}`);
|
|
804
|
-
process.exit(1);
|
|
805
|
-
}
|
|
806
|
-
|
|
807
|
-
// Find project root for config file lookup
|
|
808
|
-
const projectRoot = findProjectRoot(resolvedSourcePath);
|
|
809
|
-
|
|
810
|
-
// If excludeDirs not provided or empty, try to read from tech-stack-mappings.json
|
|
811
|
-
let excludeFileSuffixes = [];
|
|
812
|
-
let excludeFileNames = [];
|
|
813
|
-
if (!excludeDirsStr || excludeDirsStr === '[]') {
|
|
814
|
-
try {
|
|
815
|
-
const configPath = path.join(projectRoot, 'speccrew-workspace', 'docs', 'configs', 'tech-stack-mappings.json');
|
|
816
|
-
if (fs.existsSync(configPath)) {
|
|
817
|
-
const configContent = fs.readFileSync(configPath, 'utf8');
|
|
818
|
-
const config = JSON.parse(configContent);
|
|
819
|
-
|
|
820
|
-
// Map platformType to tech-stack-mappings category
|
|
821
|
-
const techCategory = PLATFORM_TYPE_TO_CATEGORY[platformType] || platformType;
|
|
822
|
-
|
|
823
|
-
// Load tech-stack-specific exclude_dirs
|
|
824
|
-
let techExcludeDirs = [];
|
|
825
|
-
let effectiveTechIdentifier = techIdentifier;
|
|
826
|
-
|
|
827
|
-
// Try exact match first
|
|
828
|
-
if (config.tech_stacks &&
|
|
829
|
-
config.tech_stacks[techCategory] &&
|
|
830
|
-
config.tech_stacks[techCategory][techIdentifier] &&
|
|
831
|
-
config.tech_stacks[techCategory][techIdentifier].exclude_dirs) {
|
|
832
|
-
techExcludeDirs = config.tech_stacks[techCategory][techIdentifier].exclude_dirs;
|
|
833
|
-
} else {
|
|
834
|
-
// Try normalized identifier (remove language prefix like "python-fastapi" → "fastapi")
|
|
835
|
-
const normalizedIdentifier = normalizeTechIdentifier(techIdentifier);
|
|
836
|
-
if (normalizedIdentifier !== techIdentifier &&
|
|
837
|
-
config.tech_stacks &&
|
|
838
|
-
config.tech_stacks[techCategory] &&
|
|
839
|
-
config.tech_stacks[techCategory][normalizedIdentifier] &&
|
|
840
|
-
config.tech_stacks[techCategory][normalizedIdentifier].exclude_dirs) {
|
|
841
|
-
techExcludeDirs = config.tech_stacks[techCategory][normalizedIdentifier].exclude_dirs;
|
|
842
|
-
effectiveTechIdentifier = normalizedIdentifier;
|
|
843
|
-
console.log(`Using normalized tech identifier for exclude_dirs: ${techIdentifier} → ${normalizedIdentifier}`);
|
|
844
|
-
}
|
|
845
|
-
}
|
|
846
|
-
|
|
847
|
-
// Load global exclude_dirs (applies to all platforms)
|
|
848
|
-
const globalExcludeDirs = config.global_exclude_dirs || [];
|
|
849
|
-
|
|
850
|
-
// Merge: global + tech-specific
|
|
851
|
-
const mergedDirs = [...new Set([...globalExcludeDirs, ...techExcludeDirs])];
|
|
852
|
-
excludeDirsStr = JSON.stringify(mergedDirs);
|
|
853
|
-
console.log(`Loaded exclude_dirs from tech-stack-mappings.json (${globalExcludeDirs.length} global + ${techExcludeDirs.length} tech-specific = ${mergedDirs.length} total)`);
|
|
854
|
-
|
|
855
|
-
// Load tech-stack-specific exclude_file_suffixes
|
|
856
|
-
if (config.tech_stacks &&
|
|
857
|
-
config.tech_stacks[techCategory] &&
|
|
858
|
-
config.tech_stacks[techCategory][effectiveTechIdentifier] &&
|
|
859
|
-
config.tech_stacks[techCategory][effectiveTechIdentifier].exclude_file_suffixes) {
|
|
860
|
-
excludeFileSuffixes = config.tech_stacks[techCategory][effectiveTechIdentifier].exclude_file_suffixes;
|
|
861
|
-
if (excludeFileSuffixes.length > 0) {
|
|
862
|
-
console.log(`Loaded exclude_file_suffixes from tech-stack-mappings.json: ${excludeFileSuffixes.join(', ')}`);
|
|
863
|
-
}
|
|
864
|
-
}
|
|
865
|
-
|
|
866
|
-
// Load tech-stack-specific exclude_file_names
|
|
867
|
-
if (config.tech_stacks &&
|
|
868
|
-
config.tech_stacks[techCategory] &&
|
|
869
|
-
config.tech_stacks[techCategory][effectiveTechIdentifier] &&
|
|
870
|
-
config.tech_stacks[techCategory][effectiveTechIdentifier].exclude_file_names) {
|
|
871
|
-
excludeFileNames = config.tech_stacks[techCategory][effectiveTechIdentifier].exclude_file_names;
|
|
872
|
-
if (excludeFileNames.length > 0) {
|
|
873
|
-
console.log(`Loaded exclude_file_names from tech-stack-mappings.json: ${excludeFileNames.join(', ')}`);
|
|
874
|
-
}
|
|
875
|
-
}
|
|
876
|
-
}
|
|
877
|
-
} catch (e) {
|
|
878
|
-
// Silent fallback - continue with default or empty
|
|
879
|
-
console.log(`Could not load exclude_dirs from config: ${e.message}`);
|
|
880
|
-
}
|
|
881
|
-
}
|
|
882
|
-
|
|
883
|
-
// Default fallback if still not set
|
|
884
|
-
if (!excludeDirsStr) {
|
|
885
|
-
excludeDirsStr = '["components","composables","hooks","utils"]';
|
|
886
|
-
}
|
|
887
|
-
|
|
888
|
-
// Validate required parameters
|
|
889
|
-
if (!sourcePath || !outputFileName || !platformName || !platformType || !techStackStr || !fileExtensionsStr) {
|
|
890
|
-
console.error('Usage: node generate-inventory.js --sourcePath <path> --outputFileName <name> --platformName <name> --platformType <type> --techStack <json> --fileExtensions <json> [--platformSubtype <subtype>] [--techIdentifier <identifier>] [--analysisMethod <method>] [--excludeDirs <json>] [--includeDataObjects <true|false>] [--outputDir <dir>]');
|
|
891
|
-
console.error('Example: node generate-inventory.js --sourcePath "src/views" --outputFileName "features-web.json" --platformName "Web Frontend" --platformType "web" --platformSubtype "vue" --techStack "vue,typescript" --fileExtensions ".vue,.ts" --analysisMethod "ui-based" --excludeDirs "components,composables,hooks,utils"');
|
|
892
|
-
process.exit(1);
|
|
893
|
-
}
|
|
894
|
-
|
|
895
|
-
// Find sync-state directory (prefer --outputDir parameter, fallback to findProjectRoot)
|
|
896
|
-
let syncStateDir;
|
|
897
|
-
if (params.outputDir) {
|
|
898
|
-
syncStateDir = path.resolve(params.outputDir);
|
|
899
|
-
console.log(`Using outputDir from parameter: ${syncStateDir}`);
|
|
900
|
-
} else {
|
|
901
|
-
console.warn('WARNING: --outputDir not specified, falling back to default path.');
|
|
902
|
-
console.warn(' Recommended: explicitly pass --outputDir to avoid incorrect output location.');
|
|
903
|
-
syncStateDir = path.join(projectRoot, 'speccrew-workspace', 'knowledges', 'base', 'sync-state', 'knowledge-bizs');
|
|
904
|
-
console.warn(` Using fallback outputDir: ${syncStateDir}`);
|
|
905
|
-
}
|
|
906
|
-
const outputPath = path.join(syncStateDir, outputFileName);
|
|
907
|
-
|
|
908
|
-
// Calculate relative source path from project root
|
|
909
|
-
let relativeSourcePath = normalizePath(sourcePath);
|
|
910
|
-
if (/^[a-zA-Z]:/.test(sourcePath) || path.isAbsolute(sourcePath)) {
|
|
911
|
-
// Absolute path - make it relative to project root
|
|
912
|
-
relativeSourcePath = normalizePath(path.relative(projectRoot, resolvedSourcePath));
|
|
913
|
-
}
|
|
914
|
-
|
|
915
|
-
// Handle special case: if source path is current directory (.), use empty string for proper replacement
|
|
916
|
-
if (relativeSourcePath === '.') {
|
|
917
|
-
relativeSourcePath = '';
|
|
918
|
-
}
|
|
919
|
-
|
|
920
|
-
// Calculate fallback module name from source path (last directory name)
|
|
921
|
-
const fallbackModuleName = path.basename(resolvedSourcePath);
|
|
922
|
-
|
|
923
|
-
console.log(`Scanning: ${sourcePath}`);
|
|
924
|
-
console.log(`Output: ${outputPath}`);
|
|
925
|
-
console.log(`Platform: ${platformName} (${platformType})`);
|
|
926
|
-
console.log(`TechStack: ${techStackStr}`);
|
|
927
|
-
console.log(`Extensions: ${fileExtensionsStr}`);
|
|
928
|
-
|
|
929
|
-
// Parse array parameters (supports both JSON and comma-separated formats)
|
|
930
|
-
const techStackArray = parseArrayParam(techStackStr);
|
|
931
|
-
const extensionsArray = parseArrayParam(fileExtensionsStr);
|
|
932
|
-
const excludeDirsArray = parseArrayParam(excludeDirsStr);
|
|
933
|
-
|
|
934
|
-
console.log(`Scanning for files: ${extensionsArray.map(ext => `*${ext}`).join(', ')}`);
|
|
935
|
-
|
|
936
|
-
// Find all files recursively matching the extensions
|
|
937
|
-
const allFiles = findFiles(resolvedSourcePath, extensionsArray, excludeDirsArray, resolvedSourcePath);
|
|
938
|
-
|
|
939
|
-
// Filter out files in excluded directories
|
|
940
|
-
let files = allFiles.filter(file => !isExcludedPath(file.relativePath, excludeDirsArray));
|
|
941
|
-
|
|
942
|
-
// For backend platforms, filter out data object files (VO/DTO/DO/Entity/Convert) unless includeDataObjects is set
|
|
943
|
-
let excludedDataObjectsCount = 0;
|
|
944
|
-
const isBackend = platformType === 'backend';
|
|
945
|
-
if (isBackend && !includeDataObjects && excludeFileSuffixes.length > 0) {
|
|
946
|
-
const filesBeforeFilter = files.length;
|
|
947
|
-
files = files.filter(file => !isDataObjectFile(file.fileName, file.extension, excludeFileSuffixes));
|
|
948
|
-
excludedDataObjectsCount = filesBeforeFilter - files.length;
|
|
949
|
-
}
|
|
950
|
-
|
|
951
|
-
// Filter out files with excluded names (e.g., package-info)
|
|
952
|
-
let excludedFileNamesCount = 0;
|
|
953
|
-
if (excludeFileNames.length > 0) {
|
|
954
|
-
const filesBeforeFilter = files.length;
|
|
955
|
-
files = files.filter(file => !isExcludedFileName(file.fileName, excludeFileNames));
|
|
956
|
-
excludedFileNamesCount = filesBeforeFilter - files.length;
|
|
957
|
-
}
|
|
958
|
-
|
|
959
|
-
console.log(`Found ${allFiles.length} total files, ${files.length} after excluding components directories`);
|
|
960
|
-
if (excludedDataObjectsCount > 0) {
|
|
961
|
-
console.log(`Excluded: ${excludedDataObjectsCount} data objects (VO/DTO/DO/Entity/Convert)`);
|
|
962
|
-
}
|
|
963
|
-
if (excludedFileNamesCount > 0) {
|
|
964
|
-
console.log(`Excluded: ${excludedFileNamesCount} files by name (${excludeFileNames.join(', ')})`);
|
|
965
|
-
}
|
|
966
|
-
|
|
967
|
-
// Build flat feature list - each file is a feature
|
|
968
|
-
const features = [];
|
|
969
|
-
|
|
970
|
-
for (const file of files) {
|
|
971
|
-
// Calculate relative file path from project root
|
|
972
|
-
const relativeFilePath = normalizePath(path.relative(projectRoot, file.fullPath));
|
|
973
|
-
|
|
974
|
-
// Calculate document path: replace source path prefix with knowledge base path and change extension to .md
|
|
975
|
-
let docPath;
|
|
976
|
-
if (!relativeSourcePath) {
|
|
977
|
-
// Source is project root, just prepend knowledge base path
|
|
978
|
-
docPath = `speccrew-workspace/knowledges/bizs/${platformType}-${platformSubtype}/${relativeFilePath}`;
|
|
979
|
-
} else {
|
|
980
|
-
// Source is a subdirectory, replace the prefix
|
|
981
|
-
const regex = new RegExp(`^${relativeSourcePath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`);
|
|
982
|
-
docPath = relativeFilePath.replace(regex, `speccrew-workspace/knowledges/bizs/${platformType}-${platformSubtype}`);
|
|
983
|
-
}
|
|
984
|
-
docPath = docPath.replace(/\.[^.]+$/, '.md');
|
|
985
|
-
|
|
986
|
-
// Extract module name from relative directory, with fallback to source path's last directory
|
|
987
|
-
const moduleName = getModuleName(file.directory, excludeDirsArray, fallbackModuleName);
|
|
988
|
-
|
|
989
|
-
// Ensure docPath contains module directory when file is directly under the platform root
|
|
990
|
-
const platformPrefix = `speccrew-workspace/knowledges/bizs/${platformType}-${platformSubtype}/`;
|
|
991
|
-
if (docPath.startsWith(platformPrefix)) {
|
|
992
|
-
const docRelative = docPath.slice(platformPrefix.length);
|
|
993
|
-
if (!docRelative.includes('/')) {
|
|
994
|
-
// File directly at root level, insert module directory
|
|
995
|
-
docPath = `${platformPrefix}${moduleName}/${docRelative}`;
|
|
996
|
-
}
|
|
997
|
-
}
|
|
998
|
-
|
|
999
|
-
// Use directory path to build unique ID, avoid filename collisions
|
|
1000
|
-
// Example: mail/account/index.vue → mail-account-index
|
|
1001
|
-
// Example: mail/template/index.vue → mail-template-index
|
|
1002
|
-
// Example: dict/index.vue → dict-index (keep compatible when no nesting)
|
|
1003
|
-
let dirSegments = file.directory
|
|
1004
|
-
? file.directory.replace(/[\/\\]/g, '-').replace(/^-+|-+$/g, '').replace(/-+/g, '-')
|
|
1005
|
-
: '';
|
|
1006
|
-
|
|
1007
|
-
// Top-level files (directory = ".") should not include "."
|
|
1008
|
-
if (dirSegments === '.' || dirSegments === './') {
|
|
1009
|
-
dirSegments = '';
|
|
1010
|
-
}
|
|
1011
|
-
|
|
1012
|
-
// If dirSegments already contains moduleName prefix, remove to avoid duplication
|
|
1013
|
-
// Example: directory='mail/account', moduleName='mail' → dirSegments='account'
|
|
1014
|
-
if (moduleName && dirSegments.startsWith(moduleName + '-')) {
|
|
1015
|
-
dirSegments = dirSegments.slice(moduleName.length + 1);
|
|
1016
|
-
} else if (moduleName && dirSegments === moduleName) {
|
|
1017
|
-
dirSegments = '';
|
|
1018
|
-
}
|
|
1019
|
-
|
|
1020
|
-
const featureId = dirSegments
|
|
1021
|
-
? `${moduleName}-${dirSegments}-${file.fileName}`
|
|
1022
|
-
: `${moduleName}-${file.fileName}`;
|
|
1023
|
-
const feature = {
|
|
1024
|
-
id: featureId,
|
|
1025
|
-
fileName: file.fileName,
|
|
1026
|
-
sourcePath: relativeFilePath,
|
|
1027
|
-
documentPath: docPath,
|
|
1028
|
-
module: moduleName,
|
|
1029
|
-
lastModified: file.lastModified,
|
|
1030
|
-
analyzed: false,
|
|
1031
|
-
startedAt: null,
|
|
1032
|
-
completedAt: null,
|
|
1033
|
-
analysisNotes: null
|
|
1034
|
-
};
|
|
1035
|
-
features.push(feature);
|
|
1036
|
-
}
|
|
1037
|
-
|
|
1038
|
-
// Collect unique module names
|
|
1039
|
-
const moduleList = [...new Set(features.map(f => f.module))].sort();
|
|
1040
|
-
|
|
1041
|
-
// Build inventory object
|
|
1042
|
-
const inventory = {
|
|
1043
|
-
platformName: platformName,
|
|
1044
|
-
platformType: platformType,
|
|
1045
|
-
sourcePath: relativeSourcePath,
|
|
1046
|
-
techStack: techStackArray,
|
|
1047
|
-
modules: moduleList,
|
|
1048
|
-
totalFiles: files.length,
|
|
1049
|
-
analyzedCount: 0,
|
|
1050
|
-
pendingCount: files.length,
|
|
1051
|
-
generatedAt: new Date().toISOString().replace(/[-:]/g, '').slice(0, 15).replace('T', '-'),
|
|
1052
|
-
analysisMethod: analysisMethod,
|
|
1053
|
-
features: features
|
|
1054
|
-
};
|
|
1055
|
-
|
|
1056
|
-
// Add platformSubtype if provided
|
|
1057
|
-
if (platformSubtype) {
|
|
1058
|
-
inventory.platformSubtype = platformSubtype;
|
|
1059
|
-
}
|
|
1060
|
-
|
|
1061
|
-
// Add techIdentifier if provided (used by reindex-modules.js to lookup exclude_dirs)
|
|
1062
|
-
if (techIdentifier) {
|
|
1063
|
-
inventory.techIdentifier = techIdentifier;
|
|
1064
|
-
}
|
|
1065
|
-
|
|
1066
|
-
// Ensure sync-state directory exists
|
|
1067
|
-
if (!fs.existsSync(syncStateDir)) {
|
|
1068
|
-
fs.mkdirSync(syncStateDir, { recursive: true });
|
|
1069
|
-
}
|
|
1070
|
-
|
|
1071
|
-
// Incremental: if features file already exists and overwrite is not set, write to *.new.json
|
|
1072
|
-
const actualOutputPath = (!overwrite && fs.existsSync(outputPath))
|
|
1073
|
-
? outputPath.replace(/\.json$/, '.new.json')
|
|
1074
|
-
: outputPath;
|
|
1075
|
-
|
|
1076
|
-
// Write JSON output
|
|
1077
|
-
fs.writeFileSync(actualOutputPath, JSON.stringify(inventory, null, 2), 'utf8');
|
|
1078
|
-
|
|
1079
|
-
if (actualOutputPath !== outputPath) {
|
|
1080
|
-
console.log(`Incremental: Generated ${path.basename(actualOutputPath)} (existing features detected)`);
|
|
1081
|
-
} else {
|
|
1082
|
-
console.log(`Full: Generated features.json with ${files.length} features`);
|
|
1083
|
-
}
|
|
1084
|
-
console.log(`Output: ${actualOutputPath}`);
|
|
1085
|
-
}
|
|
1086
|
-
|
|
1087
|
-
main();
|