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.
Files changed (137) hide show
  1. package/.speccrew/agents/speccrew-task-worker.md +1 -1
  2. package/.speccrew/agents/speccrew-team-leader.md +336 -189
  3. package/.speccrew/skills/speccrew-agentflow-manager/SKILL.md +161 -0
  4. package/.speccrew/skills/speccrew-agentflow-manager/workflow.agentflow.xml +347 -0
  5. package/.speccrew/skills/speccrew-deploy-build/SKILL.md +3 -56
  6. package/.speccrew/skills/speccrew-deploy-build/workflow.agentflow.xml +125 -0
  7. package/.speccrew/skills/speccrew-deploy-migrate/SKILL.md +3 -64
  8. package/.speccrew/skills/speccrew-deploy-migrate/workflow.agentflow.xml +135 -0
  9. package/.speccrew/skills/speccrew-deploy-smoke-test/SKILL.md +4 -156
  10. package/.speccrew/skills/speccrew-deploy-smoke-test/workflow.agentflow.xml +178 -0
  11. package/.speccrew/skills/speccrew-deploy-startup/SKILL.md +3 -135
  12. package/.speccrew/skills/speccrew-deploy-startup/workflow.agentflow.xml +223 -0
  13. package/.speccrew/skills/speccrew-dev-backend/SKILL.md +10 -2
  14. package/.speccrew/skills/speccrew-dev-backend/workflow.agentflow.xml +254 -0
  15. package/.speccrew/skills/speccrew-dev-desktop-electron/SKILL.md +10 -2
  16. package/.speccrew/skills/speccrew-dev-desktop-electron/workflow.agentflow.xml +259 -0
  17. package/.speccrew/skills/speccrew-dev-desktop-tauri/SKILL.md +10 -2
  18. package/.speccrew/skills/speccrew-dev-desktop-tauri/workflow.agentflow.xml +245 -0
  19. package/.speccrew/skills/speccrew-dev-frontend/SKILL.md +10 -2
  20. package/.speccrew/skills/speccrew-dev-frontend/workflow.agentflow.xml +262 -0
  21. package/.speccrew/skills/speccrew-dev-mobile/SKILL.md +10 -2
  22. package/.speccrew/skills/speccrew-dev-mobile/workflow.agentflow.xml +244 -0
  23. package/.speccrew/skills/speccrew-dev-review-backend/SKILL.md +10 -2
  24. package/.speccrew/skills/speccrew-dev-review-backend/workflow.agentflow.xml +251 -0
  25. package/.speccrew/skills/speccrew-dev-review-desktop/SKILL.md +10 -2
  26. package/.speccrew/skills/speccrew-dev-review-desktop/workflow.agentflow.xml +214 -0
  27. package/.speccrew/skills/speccrew-dev-review-frontend/SKILL.md +10 -2
  28. package/.speccrew/skills/speccrew-dev-review-frontend/workflow.agentflow.xml +213 -0
  29. package/.speccrew/skills/speccrew-dev-review-mobile/SKILL.md +10 -2
  30. package/.speccrew/skills/speccrew-dev-review-mobile/workflow.agentflow.xml +214 -0
  31. package/.speccrew/skills/speccrew-fd-api-contract/SKILL.md +7 -1
  32. package/.speccrew/skills/speccrew-fd-api-contract/workflow.agentflow.xml +222 -0
  33. package/.speccrew/skills/speccrew-fd-feature-analyze/SKILL.md +7 -1
  34. package/.speccrew/skills/speccrew-fd-feature-analyze/workflow.agentflow.xml +223 -0
  35. package/.speccrew/skills/speccrew-fd-feature-design/SKILL.md +7 -1
  36. package/.speccrew/skills/speccrew-fd-feature-design/workflow.agentflow.xml +322 -0
  37. package/.speccrew/skills/speccrew-get-timestamp/SKILL.md +3 -39
  38. package/.speccrew/skills/speccrew-get-timestamp/workflow.agentflow.xml +43 -0
  39. package/.speccrew/skills/speccrew-knowledge-bizs-api-analyze/SKILL.md +57 -508
  40. package/.speccrew/skills/{speccrew-knowledge-bizs-api-analyze-xml/SKILL.md → speccrew-knowledge-bizs-api-analyze/workflow.agentflow.xml} +1 -154
  41. package/.speccrew/skills/speccrew-knowledge-bizs-api-graph/SKILL.md +73 -283
  42. package/.speccrew/skills/{speccrew-knowledge-bizs-api-graph-xml/SKILL.md → speccrew-knowledge-bizs-api-graph/workflow.agentflow.xml} +0 -298
  43. package/.speccrew/skills/speccrew-knowledge-bizs-dispatch/SKILL.md +931 -801
  44. package/.speccrew/skills/{speccrew-knowledge-bizs-dispatch-xml/SKILL.md → speccrew-knowledge-bizs-dispatch/workflow.agentflow.xml} +42 -272
  45. package/.speccrew/skills/speccrew-knowledge-bizs-identify-entries/SKILL.md +263 -71
  46. package/.speccrew/skills/{speccrew-knowledge-bizs-identify-entries-xml/SKILL.md → speccrew-knowledge-bizs-identify-entries/workflow.agentflow.xml} +8 -184
  47. package/.speccrew/skills/speccrew-knowledge-bizs-init-features/SKILL.md +200 -181
  48. package/.speccrew/skills/{speccrew-knowledge-bizs-init-features-xml/SKILL.md → speccrew-knowledge-bizs-init-features/workflow.agentflow.xml} +7 -134
  49. package/.speccrew/skills/speccrew-knowledge-bizs-module-classify/SKILL.md +5 -89
  50. package/.speccrew/skills/speccrew-knowledge-bizs-module-classify/workflow.agentflow.xml +129 -0
  51. package/.speccrew/skills/speccrew-knowledge-bizs-ui-analyze/SKILL.md +454 -326
  52. package/.speccrew/skills/{speccrew-knowledge-bizs-ui-analyze-xml/SKILL.md → speccrew-knowledge-bizs-ui-analyze/workflow.agentflow.xml} +8 -128
  53. package/.speccrew/skills/speccrew-knowledge-bizs-ui-graph/SKILL.md +302 -247
  54. package/.speccrew/skills/{speccrew-knowledge-bizs-ui-graph-xml/SKILL.md → speccrew-knowledge-bizs-ui-graph/workflow.agentflow.xml} +7 -199
  55. package/.speccrew/skills/speccrew-knowledge-bizs-ui-style-extract/SKILL.md +267 -156
  56. package/.speccrew/skills/{speccrew-knowledge-bizs-ui-style-extract-xml/SKILL.md → speccrew-knowledge-bizs-ui-style-extract/workflow.agentflow.xml} +7 -151
  57. package/.speccrew/skills/speccrew-knowledge-graph-query/SKILL.md +3 -122
  58. package/.speccrew/skills/speccrew-knowledge-graph-query/workflow.agentflow.xml +106 -0
  59. package/.speccrew/skills/speccrew-knowledge-graph-write/SKILL.md +3 -80
  60. package/.speccrew/skills/speccrew-knowledge-graph-write/workflow.agentflow.xml +152 -0
  61. package/.speccrew/skills/speccrew-knowledge-module-summarize/SKILL.md +371 -265
  62. package/.speccrew/skills/{speccrew-knowledge-module-summarize-xml/SKILL.md → speccrew-knowledge-module-summarize/workflow.agentflow.xml} +7 -197
  63. package/.speccrew/skills/speccrew-knowledge-system-summarize/SKILL.md +45 -333
  64. package/.speccrew/skills/{speccrew-knowledge-system-summarize-xml/SKILL.md → speccrew-knowledge-system-summarize/workflow.agentflow.xml} +0 -177
  65. package/.speccrew/skills/speccrew-knowledge-techs-dispatch/SKILL.md +174 -727
  66. package/.speccrew/skills/{speccrew-knowledge-techs-dispatch-xml/SKILL.md → speccrew-knowledge-techs-dispatch/workflow.agentflow.xml} +10 -351
  67. package/.speccrew/skills/speccrew-knowledge-techs-generate/SKILL.md +20 -150
  68. package/.speccrew/skills/{speccrew-knowledge-techs-generate-xml/SKILL.md → speccrew-knowledge-techs-generate/workflow.agentflow.xml} +0 -169
  69. package/.speccrew/skills/speccrew-knowledge-techs-generate-conventions/SKILL.md +75 -587
  70. package/.speccrew/skills/{speccrew-knowledge-techs-generate-conventions-xml/SKILL.md → speccrew-knowledge-techs-generate-conventions/workflow.agentflow.xml} +0 -153
  71. package/.speccrew/skills/speccrew-knowledge-techs-generate-quality/SKILL.md +463 -297
  72. package/.speccrew/skills/{speccrew-knowledge-techs-generate-quality-xml/SKILL.md → speccrew-knowledge-techs-generate-quality/workflow.agentflow.xml} +0 -164
  73. package/.speccrew/skills/speccrew-knowledge-techs-generate-ui-style/SKILL.md +57 -292
  74. package/.speccrew/skills/{speccrew-knowledge-techs-generate-ui-style-xml/SKILL.md → speccrew-knowledge-techs-generate-ui-style/workflow.agentflow.xml} +2 -193
  75. package/.speccrew/skills/speccrew-knowledge-techs-index/SKILL.md +49 -335
  76. package/.speccrew/skills/{speccrew-knowledge-techs-index-xml/SKILL.md → speccrew-knowledge-techs-index/workflow.agentflow.xml} +0 -167
  77. package/.speccrew/skills/speccrew-knowledge-techs-init/SKILL.md +28 -109
  78. package/.speccrew/skills/{speccrew-knowledge-techs-init-xml/SKILL.md → speccrew-knowledge-techs-init/workflow.agentflow.xml} +0 -189
  79. package/.speccrew/skills/speccrew-knowledge-techs-ui-analyze/SKILL.md +3 -487
  80. package/.speccrew/skills/speccrew-knowledge-techs-ui-analyze/workflow.agentflow.xml +278 -0
  81. package/.speccrew/skills/speccrew-pm-knowledge-detector/SKILL.md +3 -71
  82. package/.speccrew/skills/speccrew-pm-knowledge-detector/workflow.agentflow.xml +108 -0
  83. package/.speccrew/skills/speccrew-pm-module-initializer/SKILL.md +3 -107
  84. package/.speccrew/skills/speccrew-pm-module-initializer/workflow.agentflow.xml +139 -0
  85. package/.speccrew/skills/speccrew-pm-module-matcher/SKILL.md +3 -115
  86. package/.speccrew/skills/speccrew-pm-module-matcher/workflow.agentflow.xml +146 -0
  87. package/.speccrew/skills/speccrew-pm-requirement-analysis/SKILL.md +3 -343
  88. package/.speccrew/skills/speccrew-pm-requirement-analysis/workflow.agentflow.xml +174 -0
  89. package/.speccrew/skills/speccrew-pm-requirement-assess/SKILL.md +3 -91
  90. package/.speccrew/skills/speccrew-pm-requirement-assess/workflow.agentflow.xml +173 -0
  91. package/.speccrew/skills/speccrew-pm-requirement-clarify/SKILL.md +3 -224
  92. package/.speccrew/skills/speccrew-pm-requirement-clarify/workflow.agentflow.xml +159 -0
  93. package/.speccrew/skills/speccrew-pm-requirement-model/SKILL.md +3 -275
  94. package/.speccrew/skills/speccrew-pm-requirement-model/workflow.agentflow.xml +210 -0
  95. package/.speccrew/skills/speccrew-pm-requirement-simple/SKILL.md +3 -76
  96. package/.speccrew/skills/speccrew-pm-requirement-simple/workflow.agentflow.xml +120 -0
  97. package/.speccrew/skills/speccrew-pm-sub-prd-generate/SKILL.md +7 -1
  98. package/.speccrew/skills/speccrew-pm-sub-prd-generate/workflow.agentflow.xml +218 -0
  99. package/.speccrew/skills/speccrew-sd-backend/SKILL.md +7 -1
  100. package/.speccrew/skills/speccrew-sd-backend/workflow.agentflow.xml +264 -0
  101. package/.speccrew/skills/speccrew-sd-desktop/SKILL.md +7 -1
  102. package/.speccrew/skills/speccrew-sd-desktop/workflow.agentflow.xml +288 -0
  103. package/.speccrew/skills/speccrew-sd-framework-evaluate/SKILL.md +7 -1
  104. package/.speccrew/skills/speccrew-sd-framework-evaluate/workflow.agentflow.xml +235 -0
  105. package/.speccrew/skills/speccrew-sd-frontend/SKILL.md +7 -1
  106. package/.speccrew/skills/speccrew-sd-frontend/workflow.agentflow.xml +299 -0
  107. package/.speccrew/skills/speccrew-sd-mobile/SKILL.md +7 -1
  108. package/.speccrew/skills/speccrew-sd-mobile/workflow.agentflow.xml +301 -0
  109. package/.speccrew/skills/speccrew-test-case-design/SKILL.md +165 -284
  110. package/.speccrew/skills/speccrew-test-case-design/workflow.agentflow.xml +210 -0
  111. package/.speccrew/skills/speccrew-test-code-gen/SKILL.md +204 -324
  112. package/.speccrew/skills/speccrew-test-code-gen/workflow.agentflow.xml +265 -0
  113. package/.speccrew/skills/speccrew-test-reporter/SKILL.md +205 -184
  114. package/.speccrew/skills/speccrew-test-reporter/workflow.agentflow.xml +284 -0
  115. package/.speccrew/skills/speccrew-test-runner/SKILL.md +242 -241
  116. package/.speccrew/skills/speccrew-test-runner/workflow.agentflow.xml +314 -0
  117. package/bin/cli.js +8 -1
  118. package/lib/commands/init.js +11 -3
  119. package/lib/commands/update.js +11 -3
  120. package/lib/commands/validate.js +565 -0
  121. package/lib/utils.js +43 -0
  122. package/package.json +1 -1
  123. package/workspace-template/docs/rules/{xml-workflow-spec.md → agentflow-spec.md} +5 -5
  124. package/workspace-template/scripts/validate-agentflow.js +637 -0
  125. package/.speccrew/agents/speccrew-team-leader-xml.md +0 -480
  126. package/.speccrew/skills/speccrew-knowledge-bizs-dispatch/STATUS-FORMATS.md +0 -99
  127. package/.speccrew/skills/speccrew-knowledge-bizs-dispatch/scripts/batch-orchestrator.js +0 -176
  128. package/.speccrew/skills/speccrew-knowledge-bizs-dispatch/scripts/get-next-batch.js +0 -150
  129. package/.speccrew/skills/speccrew-knowledge-bizs-dispatch/scripts/get-pending-features.js +0 -106
  130. package/.speccrew/skills/speccrew-knowledge-bizs-dispatch/scripts/mark-stale.js +0 -249
  131. package/.speccrew/skills/speccrew-knowledge-bizs-dispatch/scripts/merge-features.js +0 -300
  132. package/.speccrew/skills/speccrew-knowledge-bizs-dispatch/scripts/process-batch-results.js +0 -915
  133. package/.speccrew/skills/speccrew-knowledge-bizs-dispatch/scripts/update-feature-status.js +0 -226
  134. package/.speccrew/skills/speccrew-knowledge-bizs-init-features/examples/features.json +0 -34
  135. package/.speccrew/skills/speccrew-knowledge-bizs-init-features/scripts/generate-inventory.js +0 -1087
  136. package/.speccrew/skills/speccrew-knowledge-bizs-init-features/scripts/test-inventory.js +0 -26
  137. package/.speccrew/skills/speccrew-knowledge-techs-dispatch/STATUS-FORMATS.md +0 -550
@@ -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();