speccrew 0.6.68 → 0.7.0

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