speccrew 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (153) hide show
  1. package/.speccrew/agents/speccrew-feature-designer.md +142 -0
  2. package/.speccrew/agents/speccrew-product-manager.md +61 -0
  3. package/.speccrew/agents/speccrew-system-designer.md +200 -0
  4. package/.speccrew/agents/speccrew-system-developer.md +238 -0
  5. package/.speccrew/agents/speccrew-task-worker.md +80 -0
  6. package/.speccrew/agents/speccrew-team-leader.md +92 -0
  7. package/.speccrew/agents/speccrew-test-manager.md +313 -0
  8. package/.speccrew/skills/speccrew-create-agents/SKILL.md +98 -0
  9. package/.speccrew/skills/speccrew-create-agents/templates/agents/designer-agent.md +54 -0
  10. package/.speccrew/skills/speccrew-create-agents/templates/agents/dev-agent.md +79 -0
  11. package/.speccrew/skills/speccrew-create-agents/templates/agents/test-agent.md +80 -0
  12. package/.speccrew/skills/speccrew-dev-backend/SKILL.md +205 -0
  13. package/.speccrew/skills/speccrew-dev-backend/templates/TASK-RECORD-TEMPLATE.md +118 -0
  14. package/.speccrew/skills/speccrew-dev-desktop/SKILL.md +258 -0
  15. package/.speccrew/skills/speccrew-dev-desktop/templates/TASK-RECORD-TEMPLATE.md +161 -0
  16. package/.speccrew/skills/speccrew-dev-frontend/SKILL.md +202 -0
  17. package/.speccrew/skills/speccrew-dev-frontend/templates/TASK-RECORD-TEMPLATE.md +115 -0
  18. package/.speccrew/skills/speccrew-dev-mobile/SKILL.md +200 -0
  19. package/.speccrew/skills/speccrew-dev-mobile/templates/TASK-RECORD-TEMPLATE.md +125 -0
  20. package/.speccrew/skills/speccrew-fd-api-contract/SKILL.md +73 -0
  21. package/.speccrew/skills/speccrew-fd-api-contract/templates/API-CONTRACT-TEMPLATE.md +96 -0
  22. package/.speccrew/skills/speccrew-fd-feature-design/SKILL.md +395 -0
  23. package/.speccrew/skills/speccrew-fd-feature-design/templates/FEATURE-SPEC-TEMPLATE.md +387 -0
  24. package/.speccrew/skills/speccrew-get-timestamp/SKILL.md +80 -0
  25. package/.speccrew/skills/speccrew-get-timestamp/scripts/get-timestamp.js +35 -0
  26. package/.speccrew/skills/speccrew-knowledge-bizs-api-analyze/SKILL.md +1116 -0
  27. package/.speccrew/skills/speccrew-knowledge-bizs-api-analyze/templates/FEATURE-DETAIL-TEMPLATE-FASTAPI.md +462 -0
  28. package/.speccrew/skills/speccrew-knowledge-bizs-api-analyze/templates/FEATURE-DETAIL-TEMPLATE-JAVA.md +480 -0
  29. package/.speccrew/skills/speccrew-knowledge-bizs-api-analyze/templates/FEATURE-DETAIL-TEMPLATE-NET.md +464 -0
  30. package/.speccrew/skills/speccrew-knowledge-bizs-api-analyze/templates/FEATURE-DETAIL-TEMPLATE.md +480 -0
  31. package/.speccrew/skills/speccrew-knowledge-bizs-api-analyze/templates/MODULE-OVERVIEW-TEMPLATE.md +367 -0
  32. package/.speccrew/skills/speccrew-knowledge-bizs-dispatch/SKILL.md +667 -0
  33. package/.speccrew/skills/speccrew-knowledge-bizs-dispatch/STATUS-FORMATS.md +74 -0
  34. package/.speccrew/skills/speccrew-knowledge-bizs-dispatch/scripts/batch-orchestrator.js +176 -0
  35. package/.speccrew/skills/speccrew-knowledge-bizs-dispatch/scripts/get-next-batch.js +150 -0
  36. package/.speccrew/skills/speccrew-knowledge-bizs-dispatch/scripts/get-pending-features.js +106 -0
  37. package/.speccrew/skills/speccrew-knowledge-bizs-dispatch/scripts/mark-stale.js +249 -0
  38. package/.speccrew/skills/speccrew-knowledge-bizs-dispatch/scripts/process-batch-results.js +848 -0
  39. package/.speccrew/skills/speccrew-knowledge-bizs-dispatch/scripts/update-feature-status.js +226 -0
  40. package/.speccrew/skills/speccrew-knowledge-bizs-init-features/SKILL.md +264 -0
  41. package/.speccrew/skills/speccrew-knowledge-bizs-init-features/examples/features.json +34 -0
  42. package/.speccrew/skills/speccrew-knowledge-bizs-init-features/scripts/generate-inventory.js +867 -0
  43. package/.speccrew/skills/speccrew-knowledge-bizs-init-features/scripts/test-inventory.js +26 -0
  44. package/.speccrew/skills/speccrew-knowledge-bizs-module-classify/SKILL.md +165 -0
  45. package/.speccrew/skills/speccrew-knowledge-bizs-module-classify/scripts/apply-module-mapping.js +208 -0
  46. package/.speccrew/skills/speccrew-knowledge-bizs-module-classify/scripts/extract-module-summary.js +180 -0
  47. package/.speccrew/skills/speccrew-knowledge-bizs-module-classify/scripts/reindex-modules.js +358 -0
  48. package/.speccrew/skills/speccrew-knowledge-bizs-ui-analyze/SKILL.md +1055 -0
  49. package/.speccrew/skills/speccrew-knowledge-bizs-ui-analyze/templates/FEATURE-DETAIL-TEMPLATE-UI-DESKTOP.md +303 -0
  50. package/.speccrew/skills/speccrew-knowledge-bizs-ui-analyze/templates/FEATURE-DETAIL-TEMPLATE-UI-ELECTRON.md +327 -0
  51. package/.speccrew/skills/speccrew-knowledge-bizs-ui-analyze/templates/FEATURE-DETAIL-TEMPLATE-UI-MINIAPP.md +292 -0
  52. package/.speccrew/skills/speccrew-knowledge-bizs-ui-analyze/templates/FEATURE-DETAIL-TEMPLATE-UI-MOBILE.md +281 -0
  53. package/.speccrew/skills/speccrew-knowledge-bizs-ui-analyze/templates/FEATURE-DETAIL-TEMPLATE-UI.md +324 -0
  54. package/.speccrew/skills/speccrew-knowledge-bizs-ui-style-extract/SKILL.md +270 -0
  55. package/.speccrew/skills/speccrew-knowledge-bizs-ui-style-extract/templates/COMPONENT-PATTERN-TEMPLATE.md +33 -0
  56. package/.speccrew/skills/speccrew-knowledge-bizs-ui-style-extract/templates/LAYOUT-PATTERN-TEMPLATE.md +33 -0
  57. package/.speccrew/skills/speccrew-knowledge-bizs-ui-style-extract/templates/PAGE-TYPE-TEMPLATE.md +33 -0
  58. package/.speccrew/skills/speccrew-knowledge-graph-query/SKILL.md +229 -0
  59. package/.speccrew/skills/speccrew-knowledge-graph-query/scripts/graph-query.js +549 -0
  60. package/.speccrew/skills/speccrew-knowledge-graph-write/SKILL.md +181 -0
  61. package/.speccrew/skills/speccrew-knowledge-graph-write/scripts/graph-write.js +651 -0
  62. package/.speccrew/skills/speccrew-knowledge-module-summarize/SKILL.md +305 -0
  63. package/.speccrew/skills/speccrew-knowledge-module-summarize/templates/MODULE-OVERVIEW-TEMPLATE.md +400 -0
  64. package/.speccrew/skills/speccrew-knowledge-system-summarize/SKILL.md +351 -0
  65. package/.speccrew/skills/speccrew-knowledge-system-summarize/templates/SYSTEM-OVERVIEW-TEMPLATE.md +294 -0
  66. package/.speccrew/skills/speccrew-knowledge-techs-dispatch/SKILL.md +683 -0
  67. package/.speccrew/skills/speccrew-knowledge-techs-dispatch/STATUS-FORMATS.md +550 -0
  68. package/.speccrew/skills/speccrew-knowledge-techs-dispatch/templates/techs-manifest-EXAMPLE.json +35 -0
  69. package/.speccrew/skills/speccrew-knowledge-techs-generate/SKILL.md +1087 -0
  70. package/.speccrew/skills/speccrew-knowledge-techs-generate/templates/ARCHITECTURE-TEMPLATE.md +240 -0
  71. package/.speccrew/skills/speccrew-knowledge-techs-generate/templates/COLOR-SYSTEM-TEMPLATE.md +68 -0
  72. package/.speccrew/skills/speccrew-knowledge-techs-generate/templates/COMPONENT-LIBRARY-TEMPLATE.md +86 -0
  73. package/.speccrew/skills/speccrew-knowledge-techs-generate/templates/CONVENTIONS-BUILD-TEMPLATE.md +466 -0
  74. package/.speccrew/skills/speccrew-knowledge-techs-generate/templates/CONVENTIONS-DATA-TEMPLATE.md +432 -0
  75. package/.speccrew/skills/speccrew-knowledge-techs-generate/templates/CONVENTIONS-DESIGN-TEMPLATE.md +1209 -0
  76. package/.speccrew/skills/speccrew-knowledge-techs-generate/templates/CONVENTIONS-DEV-TEMPLATE.md +1433 -0
  77. package/.speccrew/skills/speccrew-knowledge-techs-generate/templates/CONVENTIONS-SYSTEM-TEST-TEMPLATE.md +1052 -0
  78. package/.speccrew/skills/speccrew-knowledge-techs-generate/templates/CONVENTIONS-UNIT-TEST-TEMPLATE.md +946 -0
  79. package/.speccrew/skills/speccrew-knowledge-techs-generate/templates/INDEX-TEMPLATE.md +29 -0
  80. package/.speccrew/skills/speccrew-knowledge-techs-generate/templates/PAGE-LAYOUTS-TEMPLATE.md +69 -0
  81. package/.speccrew/skills/speccrew-knowledge-techs-generate/templates/PAGE-TYPE-SUMMARY-TEMPLATE.md +74 -0
  82. package/.speccrew/skills/speccrew-knowledge-techs-generate/templates/TECH-STACK-TEMPLATE.md +232 -0
  83. package/.speccrew/skills/speccrew-knowledge-techs-generate-conventions/SKILL.md +628 -0
  84. package/.speccrew/skills/speccrew-knowledge-techs-generate-ui-style/SKILL.md +392 -0
  85. package/.speccrew/skills/speccrew-knowledge-techs-index/SKILL.md +489 -0
  86. package/.speccrew/skills/speccrew-knowledge-techs-index/templates/INDEX-TEMPLATE.md +243 -0
  87. package/.speccrew/skills/speccrew-knowledge-techs-init/SKILL.md +269 -0
  88. package/.speccrew/skills/speccrew-knowledge-techs-ui-analyze/SKILL.md +562 -0
  89. package/.speccrew/skills/speccrew-knowledge-techs-ui-analyze/templates/BUSINESS-COMPONENTS-TEMPLATE.md +171 -0
  90. package/.speccrew/skills/speccrew-knowledge-techs-ui-analyze/templates/COMMON-COMPONENTS-TEMPLATE.md +177 -0
  91. package/.speccrew/skills/speccrew-knowledge-techs-ui-analyze/templates/COMPONENT-INDIVIDUAL-TEMPLATE.md +80 -0
  92. package/.speccrew/skills/speccrew-knowledge-techs-ui-analyze/templates/COMPONENT-LIBRARY-TEMPLATE.md +118 -0
  93. package/.speccrew/skills/speccrew-knowledge-techs-ui-analyze/templates/LAYOUT-INDIVIDUAL-TEMPLATE.md +97 -0
  94. package/.speccrew/skills/speccrew-knowledge-techs-ui-analyze/templates/LAYOUT-PATTERNS-TEMPLATE.md +208 -0
  95. package/.speccrew/skills/speccrew-knowledge-techs-ui-analyze/templates/NAVIGATION-PATTERNS-TEMPLATE.md +157 -0
  96. package/.speccrew/skills/speccrew-knowledge-techs-ui-analyze/templates/PAGE-TYPE-INDIVIDUAL-TEMPLATE.md +123 -0
  97. package/.speccrew/skills/speccrew-knowledge-techs-ui-analyze/templates/PAGE-TYPE-SUMMARY-TEMPLATE.md +58 -0
  98. package/.speccrew/skills/speccrew-knowledge-techs-ui-analyze/templates/SPACING-TEMPLATE.md +119 -0
  99. package/.speccrew/skills/speccrew-knowledge-techs-ui-analyze/templates/STYLE-SYSTEM-TEMPLATE.md +117 -0
  100. package/.speccrew/skills/speccrew-knowledge-techs-ui-analyze/templates/TYPOGRAPHY-TEMPLATE.md +107 -0
  101. package/.speccrew/skills/speccrew-knowledge-techs-ui-analyze/templates/UI-STYLE-GUIDE-TEMPLATE.md +171 -0
  102. package/.speccrew/skills/speccrew-pm-requirement-analysis/SKILL.md +434 -0
  103. package/.speccrew/skills/speccrew-pm-requirement-analysis/templates/BIZS-MODELING-TEMPLATE.md +332 -0
  104. package/.speccrew/skills/speccrew-pm-requirement-analysis/templates/PRD-TEMPLATE.md +200 -0
  105. package/.speccrew/skills/speccrew-pm-requirement-assess/SKILL.md +195 -0
  106. package/.speccrew/skills/speccrew-project-diagnosis/SKILL.md +208 -0
  107. package/.speccrew/skills/speccrew-project-diagnosis/templates/DIAGNOSIS-REPORT-TEMPLATE.md +202 -0
  108. package/.speccrew/skills/speccrew-sd-backend/SKILL.md +188 -0
  109. package/.speccrew/skills/speccrew-sd-backend/templates/INDEX-TEMPLATE.md +85 -0
  110. package/.speccrew/skills/speccrew-sd-backend/templates/SD-BACKEND-TEMPLATE.md +269 -0
  111. package/.speccrew/skills/speccrew-sd-desktop/SKILL.md +192 -0
  112. package/.speccrew/skills/speccrew-sd-desktop/templates/INDEX-TEMPLATE.md +271 -0
  113. package/.speccrew/skills/speccrew-sd-desktop/templates/SD-DESKTOP-TEMPLATE.md +673 -0
  114. package/.speccrew/skills/speccrew-sd-frontend/SKILL.md +176 -0
  115. package/.speccrew/skills/speccrew-sd-frontend/templates/INDEX-TEMPLATE.md +184 -0
  116. package/.speccrew/skills/speccrew-sd-frontend/templates/SD-FRONTEND-TEMPLATE.md +382 -0
  117. package/.speccrew/skills/speccrew-sd-mobile/SKILL.md +189 -0
  118. package/.speccrew/skills/speccrew-sd-mobile/templates/INDEX-TEMPLATE.md +219 -0
  119. package/.speccrew/skills/speccrew-sd-mobile/templates/SD-MOBILE-TEMPLATE.md +534 -0
  120. package/.speccrew/skills/speccrew-test-case-design/SKILL.md +284 -0
  121. package/.speccrew/skills/speccrew-test-case-design/templates/TEST-CASE-DESIGN-TEMPLATE.md +263 -0
  122. package/.speccrew/skills/speccrew-test-code-gen/SKILL.md +313 -0
  123. package/.speccrew/skills/speccrew-test-code-gen/templates/TEST-CODE-PLAN-TEMPLATE.md +180 -0
  124. package/.speccrew/skills/speccrew-test-execute/SKILL.md +283 -0
  125. package/.speccrew/skills/speccrew-test-execute/templates/BUG-REPORT-TEMPLATE.md +50 -0
  126. package/.speccrew/skills/speccrew-test-execute/templates/TEST-REPORT-TEMPLATE.md +57 -0
  127. package/.speccrew/skills/speccrew-workflow-diagnose/SKILL.md +155 -0
  128. package/LICENSE +21 -0
  129. package/README.ar.md +318 -0
  130. package/README.en.md +318 -0
  131. package/README.es.md +318 -0
  132. package/README.md +340 -0
  133. package/bin/cli.js +62 -0
  134. package/lib/commands/doctor.js +138 -0
  135. package/lib/commands/init.js +231 -0
  136. package/lib/commands/list.js +114 -0
  137. package/lib/commands/uninstall.js +117 -0
  138. package/lib/commands/update.js +351 -0
  139. package/lib/ide-adapters.js +73 -0
  140. package/lib/utils.js +104 -0
  141. package/package.json +28 -0
  142. package/workspace-template/docs/configs/document-templates.json +667 -0
  143. package/workspace-template/docs/configs/platform-mapping.json +194 -0
  144. package/workspace-template/docs/configs/tech-stack-mappings.json +313 -0
  145. package/workspace-template/docs/configs/validation-rules.json +87 -0
  146. package/workspace-template/docs/rules/mermaid-rule.md +114 -0
  147. package/workspace-template/docs/solutions/Agent/346/212/200/350/203/275/345/256/232/344/271/211+/351/234/200/346/261/202/346/226/207/346/241/243+UML/344/275/277/347/224/250/346/250/241/346/235/277/357/274/210ISA-95/345/205/255/346/256/265/345/274/217/350/236/215/345/220/210/347/211/210/357/274/211.md +586 -0
  148. package/workspace-template/docs/solutions/agent-knowledge-map.md +238 -0
  149. package/workspace-template/docs/solutions/bizs-knowledge-pipeline.md +678 -0
  150. package/workspace-template/docs/solutions/harness.md +410 -0
  151. package/workspace-template/docs/solutions/knowledge-incremental-sync-spec.md +943 -0
  152. package/workspace-template/docs/solutions/techs-knowledge-pipeline.md +803 -0
  153. package/workspace-template/docs/solutions/workspace-structure.md +318 -0
@@ -0,0 +1,867 @@
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>]
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
+ * Example (full scan mode):
24
+ * node generate-inventory.js \
25
+ * --sourcePath "src/views" \
26
+ * --outputFileName "features-web.json" \
27
+ * --platformName "Web Frontend" \
28
+ * --platformType "web" \
29
+ * --platformSubtype "vue" \
30
+ * --techIdentifier "vue" \
31
+ * --techStack "vue,typescript" \
32
+ * --fileExtensions ".vue,.ts" \
33
+ * --analysisMethod "ui-based" \
34
+ * --excludeDirs "components,composables,hooks,utils"
35
+ *
36
+ * Example (whitelist mode):
37
+ * node generate-inventory.js \
38
+ * --entryDirsFile "entry-dirs.json"
39
+ *
40
+ * entry-dirs.json format:
41
+ * {
42
+ * "platformId": "backend-ai",
43
+ * "platformName": "AI Module Backend",
44
+ * "platformType": "backend",
45
+ * "platformSubtype": "ai",
46
+ * "sourcePath": "yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai",
47
+ * "techStack": ["spring-boot", "mybatis-plus"],
48
+ * "modules": [
49
+ * { "name": "chat", "entryDirs": ["controller/admin/chat"] },
50
+ * { "name": "image", "entryDirs": ["controller/admin/image"] }
51
+ * ]
52
+ * }
53
+ *
54
+ * Optional fields (auto-inferred if missing):
55
+ * - platformName: defaults to "{platformType}-{platformSubtype}"
56
+ * - platformType: inferred from platformId (e.g., "backend-ai" → "backend")
57
+ * - platformSubtype: inferred from platformId (e.g., "backend-ai" → "ai")
58
+ * - techStack: defaults based on platformType (backend→["spring-boot"], web→["vue"], mobile→["uniapp"])
59
+ */
60
+
61
+ const fs = require('fs');
62
+ const path = require('path');
63
+
64
+ /**
65
+ * Parse array parameter that supports both JSON format and comma-separated format.
66
+ * JSON format: '["vue","typescript"]'
67
+ * Comma-separated format: "vue,typescript" (recommended for PowerShell compatibility)
68
+ */
69
+ function parseArrayParam(value) {
70
+ // Handle boolean true (from flag-only args like --excludeDirs without value)
71
+ if (value === true) return [];
72
+ if (!value) return [];
73
+ const trimmed = value.trim();
74
+ if (trimmed.startsWith('[')) {
75
+ try {
76
+ return JSON.parse(trimmed);
77
+ } catch (e) {
78
+ // PowerShell may strip quotes: [vue,typescript] → parse as comma-separated
79
+ return trimmed.slice(1, -1).split(',').map(s => s.trim()).filter(Boolean);
80
+ }
81
+ }
82
+ return trimmed.split(',').map(s => s.trim()).filter(Boolean);
83
+ }
84
+
85
+ // Parse command line arguments
86
+ function parseArgs() {
87
+ const args = process.argv.slice(2);
88
+ const params = {};
89
+
90
+ for (let i = 0; i < args.length; i++) {
91
+ const arg = args[i];
92
+ if (arg.startsWith('--')) {
93
+ const key = arg.slice(2);
94
+ const value = args[i + 1];
95
+ // Accept empty string "" as valid value, only skip if undefined or next arg is a flag
96
+ if (value !== undefined && !value.startsWith('--')) {
97
+ params[key] = value;
98
+ i++;
99
+ } else {
100
+ params[key] = true;
101
+ }
102
+ }
103
+ }
104
+
105
+ return params;
106
+ }
107
+
108
+ // Normalize path separators to forward slashes
109
+ function normalizePath(filePath) {
110
+ if (!filePath) return '';
111
+ return filePath.replace(/\\/g, '/');
112
+ }
113
+
114
+ // Find project root by searching upward for speccrew-workspace directory
115
+ function findProjectRoot(startPath) {
116
+ let currentDir = path.resolve(startPath);
117
+ const root = path.parse(currentDir).root;
118
+
119
+ while (currentDir !== root) {
120
+ const workspaceDir = path.join(currentDir, 'speccrew-workspace');
121
+ if (fs.existsSync(workspaceDir) && fs.statSync(workspaceDir).isDirectory()) {
122
+ return currentDir;
123
+ }
124
+ currentDir = path.dirname(currentDir);
125
+ }
126
+
127
+ // Fallback: return source path's drive root
128
+ return root;
129
+ }
130
+
131
+ // Check if path contains any excluded directory
132
+ function isExcludedPath(filePath, excludeDirs) {
133
+ const parts = normalizePath(filePath).split('/').filter(p => p);
134
+ for (const part of parts) {
135
+ if (excludeDirs.includes(part)) {
136
+ return true;
137
+ }
138
+ }
139
+ return false;
140
+ }
141
+
142
+ // Check if file is a data object class (VO/DTO/DO/Entity/Convert) - should be excluded for backend
143
+ function isDataObjectFile(fileName, extension, excludeSuffixes) {
144
+ // If no suffixes configured, don't exclude any files
145
+ if (!excludeSuffixes || excludeSuffixes.length === 0) {
146
+ return false;
147
+ }
148
+ // Check if fileName ends with any configured suffix
149
+ for (const suffix of excludeSuffixes) {
150
+ if (fileName.endsWith(suffix)) {
151
+ return true;
152
+ }
153
+ }
154
+ return false;
155
+ }
156
+
157
+ // Get module name (first non-excluded directory level)
158
+ function getModuleName(dirPath, excludeDirs, fallbackModuleName) {
159
+ const parts = normalizePath(dirPath).split('/').filter(p => p && p !== '.');
160
+ for (const part of parts) {
161
+ if (!excludeDirs.includes(part)) {
162
+ return part;
163
+ }
164
+ }
165
+ // All parts were excluded (e.g., "src/App.vue" with src excluded)
166
+ // Return "_root" to indicate framework/root-level files
167
+ return '_root';
168
+ }
169
+
170
+ /**
171
+ * Load platform configuration from platform-mapping.json
172
+ * @param {string} platformId - Platform ID like "backend-ai", "web-vue"
173
+ * @param {string} projectRoot - Project root directory
174
+ * @returns {object|null} Platform config with platformName, platformType, techStack, extensions, or null if not found
175
+ */
176
+ function loadPlatformConfig(platformId, projectRoot) {
177
+ const configPath = path.join(projectRoot, 'speccrew-workspace', 'docs', 'configs', 'platform-mapping.json');
178
+ if (!fs.existsSync(configPath)) {
179
+ console.error(`Error: platform-mapping.json not found at ${configPath}`);
180
+ return null;
181
+ }
182
+
183
+ const configContent = fs.readFileSync(configPath, 'utf8');
184
+ const config = JSON.parse(configContent);
185
+
186
+ // Find platform mapping by platform_id
187
+ const mapping = config.mappings.find(m => m.platform_id === platformId);
188
+ if (!mapping) {
189
+ console.error(`Error: Platform ID "${platformId}" not found in platform-mapping.json`);
190
+ return null;
191
+ }
192
+
193
+ // Build platform config from mapping
194
+ const platformConfig = {
195
+ platformId: mapping.platform_id,
196
+ platformType: mapping.platform_type,
197
+ platformSubtype: mapping.platform_subtype || mapping.framework,
198
+ framework: mapping.framework,
199
+ platformName: `${mapping.platform_type}-${mapping.framework}` // Default name, can be customized
200
+ };
201
+
202
+ return platformConfig;
203
+ }
204
+
205
+ /**
206
+ * Load tech stack configuration from tech-stack-mappings.json
207
+ * @param {string} platformType - Platform type like "backend", "web"
208
+ * @param {string} framework - Framework like "spring", "vue"
209
+ * @param {string} projectRoot - Project root directory
210
+ * @returns {object} Tech config with extensions and exclude_file_suffixes
211
+ */
212
+ function loadTechStackConfig(platformType, framework, projectRoot) {
213
+ const configPath = path.join(projectRoot, 'speccrew-workspace', 'docs', 'configs', 'tech-stack-mappings.json');
214
+ if (!fs.existsSync(configPath)) {
215
+ console.error(`Warning: tech-stack-mappings.json not found at ${configPath}`);
216
+ return { extensions: [], exclude_file_suffixes: [] };
217
+ }
218
+
219
+ const configContent = fs.readFileSync(configPath, 'utf8');
220
+ const config = JSON.parse(configContent);
221
+
222
+ if (config.tech_stacks &&
223
+ config.tech_stacks[platformType] &&
224
+ config.tech_stacks[platformType][framework]) {
225
+ const techConfig = config.tech_stacks[platformType][framework];
226
+ return {
227
+ extensions: techConfig.extensions || [],
228
+ exclude_file_suffixes: techConfig.exclude_file_suffixes || []
229
+ };
230
+ }
231
+
232
+ return { extensions: [], exclude_file_suffixes: [] };
233
+ }
234
+
235
+ /**
236
+ * Infer platform info from platformId
237
+ * @param {string} platformId - Platform ID like "backend-ai", "web-vue", "mobile-uniapp"
238
+ * @returns {object} Inferred platform info { platformType, platformSubtype }
239
+ */
240
+ function inferPlatformInfo(platformId) {
241
+ // Parse platformId: "{type}-{subtype}" format
242
+ const parts = platformId.split('-');
243
+ if (parts.length >= 2) {
244
+ return {
245
+ platformType: parts[0],
246
+ platformSubtype: parts.slice(1).join('-')
247
+ };
248
+ }
249
+ // Fallback: treat entire platformId as type
250
+ return {
251
+ platformType: platformId,
252
+ platformSubtype: ''
253
+ };
254
+ }
255
+
256
+ /**
257
+ * Infer framework from techStack array
258
+ * @param {string[]} techStack - Array of tech stack names
259
+ * @returns {string} Framework identifier like "spring", "vue", "uniapp"
260
+ */
261
+ function inferFrameworkFromTechStack(techStack) {
262
+ if (!techStack || techStack.length === 0) {
263
+ return '';
264
+ }
265
+
266
+ // Mapping: tech stack name → framework identifier
267
+ const techToFramework = {
268
+ // Backend
269
+ 'spring-boot': 'spring',
270
+ 'spring': 'spring',
271
+ 'springboot': 'spring',
272
+ 'mybatis-plus': 'spring',
273
+ 'mybatis': 'spring',
274
+ 'jpa': 'spring',
275
+ // Frontend
276
+ 'vue': 'vue',
277
+ 'vue3': 'vue',
278
+ 'vue2': 'vue',
279
+ 'react': 'react',
280
+ 'reactjs': 'react',
281
+ 'nextjs': 'next',
282
+ 'next.js': 'next',
283
+ 'angular': 'angular',
284
+ // Mobile
285
+ 'uniapp': 'uniapp',
286
+ 'uni-app': 'uniapp',
287
+ 'flutter': 'flutter',
288
+ 'react-native': 'react-native',
289
+ 'reactnative': 'react-native'
290
+ };
291
+
292
+ // Platform-specific frameworks have higher priority than generic ones
293
+ const platformSpecific = new Set(['uniapp', 'flutter', 'react-native', 'next']);
294
+
295
+ // First pass: look for platform-specific framework match
296
+ for (const tech of techStack) {
297
+ const normalizedTech = tech.toLowerCase();
298
+ const framework = techToFramework[normalizedTech];
299
+ if (framework && platformSpecific.has(framework)) {
300
+ return framework;
301
+ }
302
+ }
303
+
304
+ // Second pass: fallback to first matching generic framework
305
+ for (const tech of techStack) {
306
+ const normalizedTech = tech.toLowerCase();
307
+ if (techToFramework[normalizedTech]) {
308
+ return techToFramework[normalizedTech];
309
+ }
310
+ }
311
+
312
+ // Fallback: use first tech stack as framework
313
+ return techStack[0].toLowerCase();
314
+ }
315
+
316
+ /**
317
+ * Get default techStack for platformType
318
+ * @param {string} platformType - Platform type like "backend", "web", "mobile"
319
+ * @returns {string[]} Default tech stack array
320
+ */
321
+ function getDefaultTechStack(platformType) {
322
+ const defaults = {
323
+ backend: ['spring-boot'],
324
+ web: ['vue'],
325
+ mobile: ['uniapp'],
326
+ desktop: ['electron']
327
+ };
328
+ return defaults[platformType] || [];
329
+ }
330
+
331
+ /**
332
+ * Find files in a specific entry directory (non-recursive, just the directory itself)
333
+ * @param {string} dir - Directory to scan
334
+ * @param {string[]} extensions - File extensions to match
335
+ * @param {string} baseDir - Base directory for relative paths
336
+ * @returns {object[]} Array of file objects
337
+ */
338
+ function findFilesInDir(dir, extensions, baseDir) {
339
+ const files = [];
340
+
341
+ if (!fs.existsSync(dir)) {
342
+ return files;
343
+ }
344
+
345
+ const items = fs.readdirSync(dir);
346
+
347
+ for (const item of items) {
348
+ const fullPath = path.join(dir, item);
349
+ const stat = fs.statSync(fullPath);
350
+
351
+ if (stat.isFile()) {
352
+ const ext = path.extname(item);
353
+ if (extensions.includes(ext)) {
354
+ const relativePath = normalizePath(path.relative(baseDir, fullPath));
355
+ files.push({
356
+ fullPath,
357
+ relativePath,
358
+ fileName: path.basename(item, ext),
359
+ extension: ext,
360
+ directory: path.dirname(relativePath)
361
+ });
362
+ }
363
+ }
364
+ }
365
+
366
+ return files;
367
+ }
368
+
369
+ /**
370
+ * Generate features.json from entry-dirs whitelist mode
371
+ * @param {object} entryDirsData - Entry directories data from JSON file
372
+ * @param {object} platformConfig - Platform configuration
373
+ * @param {string} projectRoot - Project root directory
374
+ * @param {string} outputDir - Output directory for features JSON
375
+ * @returns {boolean} Success status
376
+ */
377
+ function generateFromEntryDirs(entryDirsData, platformConfig, projectRoot, outputDir) {
378
+ const { platformId, sourcePath, modules } = entryDirsData;
379
+ const { platformType, platformSubtype, framework } = platformConfig;
380
+
381
+ // Load tech stack config for extensions and exclude_file_suffixes
382
+ const techConfig = loadTechStackConfig(platformType, framework, projectRoot);
383
+ const { extensions, exclude_file_suffixes } = techConfig;
384
+
385
+ if (extensions.length === 0) {
386
+ console.error(`Error: No extensions found for ${platformType}/${framework} in tech-stack-mappings.json`);
387
+ return false;
388
+ }
389
+
390
+ console.log(`Whitelist mode: Platform ${platformId}`);
391
+ console.log(`Source path: ${sourcePath}`);
392
+ console.log(`Extensions: ${extensions.join(', ')}`);
393
+ if (exclude_file_suffixes.length > 0) {
394
+ console.log(`Exclude suffixes: ${exclude_file_suffixes.join(', ')}`);
395
+ }
396
+
397
+ // Resolve absolute source path
398
+ const absoluteSourcePath = path.resolve(projectRoot, sourcePath);
399
+ if (!fs.existsSync(absoluteSourcePath)) {
400
+ console.error(`Error: Source path does not exist: ${absoluteSourcePath}`);
401
+ return false;
402
+ }
403
+
404
+ // Collect all features
405
+ const features = [];
406
+ const moduleNames = [];
407
+
408
+ for (const module of modules) {
409
+ const { name: moduleName, entryDirs } = module;
410
+ moduleNames.push(moduleName);
411
+
412
+ for (const entryDir of entryDirs) {
413
+ // entryDir is relative to sourcePath
414
+ const entryFullPath = path.join(absoluteSourcePath, entryDir);
415
+
416
+ if (!fs.existsSync(entryFullPath)) {
417
+ console.log(` Skipping non-existent entry: ${entryDir}`);
418
+ continue;
419
+ }
420
+
421
+ // Scan files in the entry directory (recursive for web/mobile platforms with nested dirs)
422
+ const excludeDirs = techConfig.exclude_dirs || [];
423
+ const files = findFiles(entryFullPath, extensions, excludeDirs, absoluteSourcePath);
424
+
425
+ for (const file of files) {
426
+ // Apply exclude_file_suffixes filter
427
+ if (isDataObjectFile(file.fileName, file.extension, exclude_file_suffixes)) {
428
+ continue;
429
+ }
430
+
431
+ // Build feature ID: moduleName-entryDirSegs-fileName
432
+ // entryDir like "controller/admin/chat" → "controller-admin-chat"
433
+ const entryDirNormalized = normalizePath(entryDir).replace(/[\/\\]/g, '-');
434
+ const featureId = `${moduleName}-${entryDirNormalized}-${file.fileName}`;
435
+
436
+ // Build relative file path from sourcePath
437
+ const relativeFilePath = normalizePath(path.relative(projectRoot, file.fullPath));
438
+
439
+ // Build document path
440
+ const docPath = `speccrew-workspace/knowledges/bizs/${platformType}-${platformSubtype}/${moduleName}/${file.fileName}.md`;
441
+
442
+ const feature = {
443
+ id: featureId,
444
+ fileName: file.fileName,
445
+ sourcePath: relativeFilePath,
446
+ documentPath: docPath,
447
+ module: moduleName,
448
+ analyzed: false,
449
+ startedAt: null,
450
+ completedAt: null,
451
+ analysisNotes: null
452
+ };
453
+ features.push(feature);
454
+ }
455
+ }
456
+ }
457
+
458
+ console.log(`Found ${features.length} features across ${moduleNames.length} modules`);
459
+
460
+ // Build inventory object
461
+ const inventory = {
462
+ platformId: platformId,
463
+ platformName: platformConfig.platformName,
464
+ platformType: platformType,
465
+ sourcePath: sourcePath,
466
+ techStack: [framework],
467
+ modules: [...new Set(moduleNames)].sort(),
468
+ totalFiles: features.length,
469
+ analyzedCount: 0,
470
+ pendingCount: features.length,
471
+ generatedAt: new Date().toISOString().replace(/[-:]/g, '').slice(0, 15).replace('T', '-'),
472
+ analysisMethod: 'api-based',
473
+ features: features
474
+ };
475
+
476
+ // Add platformSubtype if present
477
+ if (platformSubtype) {
478
+ inventory.platformSubtype = platformSubtype;
479
+ }
480
+
481
+ // Add techIdentifier
482
+ inventory.techIdentifier = framework;
483
+
484
+ // Write output file
485
+ const outputFileName = `features-${platformId}.json`;
486
+ const outputPath = path.join(outputDir, outputFileName);
487
+
488
+ // Ensure output directory exists
489
+ if (!fs.existsSync(outputDir)) {
490
+ fs.mkdirSync(outputDir, { recursive: true });
491
+ }
492
+
493
+ fs.writeFileSync(outputPath, JSON.stringify(inventory, null, 2), 'utf8');
494
+
495
+ console.log(`Generated ${outputFileName} with ${features.length} features`);
496
+ console.log(`Output: ${outputPath}`);
497
+
498
+ return true;
499
+ }
500
+
501
+ // Recursively find all files matching extensions
502
+ function findFiles(dir, extensions, excludeDirs, baseDir) {
503
+ const files = [];
504
+ const items = fs.readdirSync(dir);
505
+
506
+ for (const item of items) {
507
+ const fullPath = path.join(dir, item);
508
+ const relativePath = normalizePath(path.relative(baseDir, fullPath));
509
+ const stat = fs.statSync(fullPath);
510
+
511
+ if (stat.isDirectory()) {
512
+ // Skip excluded directories
513
+ if (excludeDirs.includes(item)) {
514
+ continue;
515
+ }
516
+ files.push(...findFiles(fullPath, extensions, excludeDirs, baseDir));
517
+ } else if (stat.isFile()) {
518
+ const ext = path.extname(item);
519
+ if (extensions.includes(ext)) {
520
+ files.push({
521
+ fullPath,
522
+ relativePath,
523
+ fileName: path.basename(item, ext),
524
+ extension: ext,
525
+ directory: path.dirname(relativePath)
526
+ });
527
+ }
528
+ }
529
+ }
530
+
531
+ return files;
532
+ }
533
+
534
+ // Main function
535
+ function main() {
536
+ const params = parseArgs();
537
+
538
+ // Check for whitelist mode (--entryDirsFile provided)
539
+ if (params.entryDirsFile) {
540
+ // Whitelist mode: scan only specified entry directories
541
+ const entryDirsFilePath = path.resolve(params.entryDirsFile);
542
+
543
+ if (!fs.existsSync(entryDirsFilePath)) {
544
+ console.error(`Error: entryDirsFile does not exist: ${params.entryDirsFile}`);
545
+ process.exit(1);
546
+ }
547
+
548
+ // Read entry-dirs JSON file
549
+ let entryDirsData;
550
+ try {
551
+ const content = fs.readFileSync(entryDirsFilePath, 'utf8');
552
+ entryDirsData = JSON.parse(content);
553
+ } catch (e) {
554
+ console.error(`Error: Failed to parse entryDirsFile: ${e.message}`);
555
+ process.exit(1);
556
+ }
557
+
558
+ // Validate entryDirsData structure
559
+ if (!entryDirsData.platformId) {
560
+ console.error('Error: entryDirsFile missing required field "platformId"');
561
+ process.exit(1);
562
+ }
563
+ if (!entryDirsData.sourcePath) {
564
+ console.error('Error: entryDirsFile missing required field "sourcePath"');
565
+ process.exit(1);
566
+ }
567
+ if (!entryDirsData.modules || !Array.isArray(entryDirsData.modules)) {
568
+ console.error('Error: entryDirsFile missing required field "modules" array');
569
+ process.exit(1);
570
+ }
571
+
572
+ // Find project root (use current directory or entryDirsFile directory)
573
+ const projectRoot = findProjectRoot(path.dirname(entryDirsFilePath));
574
+
575
+ // Build platform config from entryDirsData (no longer requires platform-mapping.json)
576
+ // Step 1: Get platformType and platformSubtype (from entryDirsData or infer from platformId)
577
+ let platformType = entryDirsData.platformType;
578
+ let platformSubtype = entryDirsData.platformSubtype;
579
+
580
+ if (!platformType || !platformSubtype) {
581
+ const inferred = inferPlatformInfo(entryDirsData.platformId);
582
+ if (!platformType) {
583
+ platformType = inferred.platformType;
584
+ console.log(`Inferred platformType from platformId: ${platformType}`);
585
+ }
586
+ if (!platformSubtype) {
587
+ platformSubtype = inferred.platformSubtype;
588
+ console.log(`Inferred platformSubtype from platformId: ${platformSubtype}`);
589
+ }
590
+ }
591
+
592
+ // Step 2: Get techStack (from entryDirsData or default based on platformType)
593
+ let techStack = entryDirsData.techStack;
594
+ if (!techStack || techStack.length === 0) {
595
+ techStack = getDefaultTechStack(platformType);
596
+ console.log(`Using default techStack for ${platformType}: [${techStack.join(', ')}]`);
597
+ }
598
+
599
+ // Step 3: Infer framework from techStack
600
+ const framework = inferFrameworkFromTechStack(techStack);
601
+
602
+ // Step 4: Get platformName (from entryDirsData or build from type/subtype)
603
+ const platformName = entryDirsData.platformName || `${platformType}-${platformSubtype}`;
604
+
605
+ // Build final platformConfig object
606
+ const platformConfig = {
607
+ platformId: entryDirsData.platformId,
608
+ platformType: platformType,
609
+ platformSubtype: platformSubtype,
610
+ framework: framework,
611
+ platformName: platformName
612
+ };
613
+
614
+ console.log(`Platform config: type=${platformType}, subtype=${platformSubtype}, framework=${framework}`);
615
+
616
+ // Set output directory
617
+ const outputDir = path.join(projectRoot, 'speccrew-workspace', 'knowledges', 'base', 'sync-state', 'knowledge-bizs');
618
+
619
+ // Generate features from entry dirs
620
+ const success = generateFromEntryDirs(entryDirsData, platformConfig, projectRoot, outputDir);
621
+ process.exit(success ? 0 : 1);
622
+ }
623
+
624
+ // Required parameters (full scan mode)
625
+ const sourcePath = params.sourcePath;
626
+ const outputFileName = params.outputFileName;
627
+ const platformName = params.platformName;
628
+ const platformType = params.platformType;
629
+ const techStackStr = params.techStack;
630
+ const fileExtensionsStr = params.fileExtensions;
631
+
632
+ // Optional parameters
633
+ const platformSubtype = params.platformSubtype || '';
634
+ const techIdentifier = params.techIdentifier || platformSubtype;
635
+ const analysisMethod = params.analysisMethod || 'ui-based';
636
+ let excludeDirsStr = params.excludeDirs;
637
+ // --includeDataObjects: set to "true" to include VO/DTO/DO/Entity/Convert files (default: false)
638
+ const includeDataObjects = params.includeDataObjects === 'true';
639
+
640
+ // Resolve source path first to find project root
641
+ const resolvedSourcePath = path.resolve(sourcePath);
642
+ if (!fs.existsSync(resolvedSourcePath)) {
643
+ console.error(`Error: Source path does not exist: ${sourcePath}`);
644
+ process.exit(1);
645
+ }
646
+
647
+ // Find project root for config file lookup
648
+ const projectRoot = findProjectRoot(resolvedSourcePath);
649
+
650
+ // If excludeDirs not provided or empty, try to read from tech-stack-mappings.json
651
+ let excludeFileSuffixes = [];
652
+ if (!excludeDirsStr || excludeDirsStr === '[]') {
653
+ try {
654
+ const configPath = path.join(projectRoot, 'speccrew-workspace', 'docs', 'configs', 'tech-stack-mappings.json');
655
+ if (fs.existsSync(configPath)) {
656
+ const configContent = fs.readFileSync(configPath, 'utf8');
657
+ const config = JSON.parse(configContent);
658
+
659
+ // Load tech-stack-specific exclude_dirs
660
+ let techExcludeDirs = [];
661
+ if (config.tech_stacks &&
662
+ config.tech_stacks[platformType] &&
663
+ config.tech_stacks[platformType][techIdentifier] &&
664
+ config.tech_stacks[platformType][techIdentifier].exclude_dirs) {
665
+ techExcludeDirs = config.tech_stacks[platformType][techIdentifier].exclude_dirs;
666
+ }
667
+
668
+ // Load global exclude_dirs (applies to all platforms)
669
+ const globalExcludeDirs = config.global_exclude_dirs || [];
670
+
671
+ // Merge: global + tech-specific
672
+ const mergedDirs = [...new Set([...globalExcludeDirs, ...techExcludeDirs])];
673
+ excludeDirsStr = JSON.stringify(mergedDirs);
674
+ console.log(`Loaded exclude_dirs from tech-stack-mappings.json (${globalExcludeDirs.length} global + ${techExcludeDirs.length} tech-specific = ${mergedDirs.length} total)`);
675
+
676
+ // Load tech-stack-specific exclude_file_suffixes
677
+ if (config.tech_stacks &&
678
+ config.tech_stacks[platformType] &&
679
+ config.tech_stacks[platformType][techIdentifier] &&
680
+ config.tech_stacks[platformType][techIdentifier].exclude_file_suffixes) {
681
+ excludeFileSuffixes = config.tech_stacks[platformType][techIdentifier].exclude_file_suffixes;
682
+ if (excludeFileSuffixes.length > 0) {
683
+ console.log(`Loaded exclude_file_suffixes from tech-stack-mappings.json: ${excludeFileSuffixes.join(', ')}`);
684
+ }
685
+ }
686
+ }
687
+ } catch (e) {
688
+ // Silent fallback - continue with default or empty
689
+ console.log(`Could not load exclude_dirs from config: ${e.message}`);
690
+ }
691
+ }
692
+
693
+ // Default fallback if still not set
694
+ if (!excludeDirsStr) {
695
+ excludeDirsStr = '["components","composables","hooks","utils"]';
696
+ }
697
+
698
+ // Validate required parameters
699
+ if (!sourcePath || !outputFileName || !platformName || !platformType || !techStackStr || !fileExtensionsStr) {
700
+ 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>]');
701
+ 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"');
702
+ process.exit(1);
703
+ }
704
+
705
+ // Find sync-state directory
706
+ const syncStateDir = path.join(projectRoot, 'speccrew-workspace', 'knowledges', 'base', 'sync-state', 'knowledge-bizs');
707
+ const outputPath = path.join(syncStateDir, outputFileName);
708
+
709
+ // Calculate relative source path from project root
710
+ let relativeSourcePath = normalizePath(sourcePath);
711
+ if (/^[a-zA-Z]:/.test(sourcePath) || path.isAbsolute(sourcePath)) {
712
+ // Absolute path - make it relative to project root
713
+ relativeSourcePath = normalizePath(path.relative(projectRoot, resolvedSourcePath));
714
+ }
715
+
716
+ // Handle special case: if source path is current directory (.), use empty string for proper replacement
717
+ if (relativeSourcePath === '.') {
718
+ relativeSourcePath = '';
719
+ }
720
+
721
+ // Calculate fallback module name from source path (last directory name)
722
+ const fallbackModuleName = path.basename(resolvedSourcePath);
723
+
724
+ console.log(`Scanning: ${sourcePath}`);
725
+ console.log(`Output: ${outputPath}`);
726
+ console.log(`Platform: ${platformName} (${platformType})`);
727
+ console.log(`TechStack: ${techStackStr}`);
728
+ console.log(`Extensions: ${fileExtensionsStr}`);
729
+
730
+ // Parse array parameters (supports both JSON and comma-separated formats)
731
+ const techStackArray = parseArrayParam(techStackStr);
732
+ const extensionsArray = parseArrayParam(fileExtensionsStr);
733
+ const excludeDirsArray = parseArrayParam(excludeDirsStr);
734
+
735
+ console.log(`Scanning for files: ${extensionsArray.map(ext => `*${ext}`).join(', ')}`);
736
+
737
+ // Find all files recursively matching the extensions
738
+ const allFiles = findFiles(resolvedSourcePath, extensionsArray, excludeDirsArray, resolvedSourcePath);
739
+
740
+ // Filter out files in excluded directories
741
+ let files = allFiles.filter(file => !isExcludedPath(file.relativePath, excludeDirsArray));
742
+
743
+ // For backend platforms, filter out data object files (VO/DTO/DO/Entity/Convert) unless includeDataObjects is set
744
+ let excludedDataObjectsCount = 0;
745
+ const isBackend = platformType === 'backend';
746
+ if (isBackend && !includeDataObjects && excludeFileSuffixes.length > 0) {
747
+ const filesBeforeFilter = files.length;
748
+ files = files.filter(file => !isDataObjectFile(file.fileName, file.extension, excludeFileSuffixes));
749
+ excludedDataObjectsCount = filesBeforeFilter - files.length;
750
+ }
751
+
752
+ console.log(`Found ${allFiles.length} total files, ${files.length} after excluding components directories`);
753
+ if (excludedDataObjectsCount > 0) {
754
+ console.log(`Excluded: ${excludedDataObjectsCount} data objects (VO/DTO/DO/Entity/Convert)`);
755
+ }
756
+
757
+ // Build flat feature list - each file is a feature
758
+ const features = [];
759
+
760
+ for (const file of files) {
761
+ // Calculate relative file path from project root
762
+ const relativeFilePath = normalizePath(path.relative(projectRoot, file.fullPath));
763
+
764
+ // Calculate document path: replace source path prefix with knowledge base path and change extension to .md
765
+ let docPath;
766
+ if (!relativeSourcePath) {
767
+ // Source is project root, just prepend knowledge base path
768
+ docPath = `speccrew-workspace/knowledges/bizs/${platformType}-${platformSubtype}/${relativeFilePath}`;
769
+ } else {
770
+ // Source is a subdirectory, replace the prefix
771
+ const regex = new RegExp(`^${relativeSourcePath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`);
772
+ docPath = relativeFilePath.replace(regex, `speccrew-workspace/knowledges/bizs/${platformType}-${platformSubtype}`);
773
+ }
774
+ docPath = docPath.replace(/\.[^.]+$/, '.md');
775
+
776
+ // Extract module name from relative directory, with fallback to source path's last directory
777
+ const moduleName = getModuleName(file.directory, excludeDirsArray, fallbackModuleName);
778
+
779
+ // Ensure docPath contains module directory when file is directly under the platform root
780
+ const platformPrefix = `speccrew-workspace/knowledges/bizs/${platformType}-${platformSubtype}/`;
781
+ if (docPath.startsWith(platformPrefix)) {
782
+ const docRelative = docPath.slice(platformPrefix.length);
783
+ if (!docRelative.includes('/')) {
784
+ // File directly at root level, insert module directory
785
+ docPath = `${platformPrefix}${moduleName}/${docRelative}`;
786
+ }
787
+ }
788
+
789
+ // 使用目录路径构建唯一 ID,避免同名文件碰撞
790
+ // 例: mail/account/index.vue → mail-account-index
791
+ // 例: mail/template/index.vue → mail-template-index
792
+ // 例: dict/index.vue → dict-index (无嵌套时保持兼容)
793
+ let dirSegments = file.directory
794
+ ? file.directory.replace(/[\/\\]/g, '-').replace(/^-+|-+$/g, '').replace(/-+/g, '-')
795
+ : '';
796
+
797
+ // 顶层文件(directory = ".")不应引入 "."
798
+ if (dirSegments === '.' || dirSegments === './') {
799
+ dirSegments = '';
800
+ }
801
+
802
+ // 如果 dirSegments 已包含 moduleName 前缀,去除避免重复
803
+ // 例: directory='mail/account', moduleName='mail' → dirSegments='account'
804
+ if (moduleName && dirSegments.startsWith(moduleName + '-')) {
805
+ dirSegments = dirSegments.slice(moduleName.length + 1);
806
+ } else if (moduleName && dirSegments === moduleName) {
807
+ dirSegments = '';
808
+ }
809
+
810
+ const featureId = dirSegments
811
+ ? `${moduleName}-${dirSegments}-${file.fileName}`
812
+ : `${moduleName}-${file.fileName}`;
813
+ const feature = {
814
+ id: featureId,
815
+ fileName: file.fileName,
816
+ sourcePath: relativeFilePath,
817
+ documentPath: docPath,
818
+ module: moduleName,
819
+ analyzed: false,
820
+ startedAt: null,
821
+ completedAt: null,
822
+ analysisNotes: null
823
+ };
824
+ features.push(feature);
825
+ }
826
+
827
+ // Collect unique module names
828
+ const moduleList = [...new Set(features.map(f => f.module))].sort();
829
+
830
+ // Build inventory object
831
+ const inventory = {
832
+ platformName: platformName,
833
+ platformType: platformType,
834
+ sourcePath: relativeSourcePath,
835
+ techStack: techStackArray,
836
+ modules: moduleList,
837
+ totalFiles: files.length,
838
+ analyzedCount: 0,
839
+ pendingCount: files.length,
840
+ generatedAt: new Date().toISOString().replace(/[-:]/g, '').slice(0, 15).replace('T', '-'),
841
+ analysisMethod: analysisMethod,
842
+ features: features
843
+ };
844
+
845
+ // Add platformSubtype if provided
846
+ if (platformSubtype) {
847
+ inventory.platformSubtype = platformSubtype;
848
+ }
849
+
850
+ // Add techIdentifier if provided (used by reindex-modules.js to lookup exclude_dirs)
851
+ if (techIdentifier) {
852
+ inventory.techIdentifier = techIdentifier;
853
+ }
854
+
855
+ // Ensure sync-state directory exists
856
+ if (!fs.existsSync(syncStateDir)) {
857
+ fs.mkdirSync(syncStateDir, { recursive: true });
858
+ }
859
+
860
+ // Write JSON output
861
+ fs.writeFileSync(outputPath, JSON.stringify(inventory, null, 2), 'utf8');
862
+
863
+ console.log(`Generated features.json with ${files.length} features`);
864
+ console.log(`Ready for analysis: ${outputPath}`);
865
+ }
866
+
867
+ main();