code-as-plan 2.0.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 (188) hide show
  1. package/LICENSE +21 -0
  2. package/README.ja-JP.md +834 -0
  3. package/README.ko-KR.md +823 -0
  4. package/README.md +1006 -0
  5. package/README.pt-BR.md +452 -0
  6. package/README.zh-CN.md +800 -0
  7. package/agents/cap-brainstormer.md +154 -0
  8. package/agents/cap-debugger.md +221 -0
  9. package/agents/cap-prototyper.md +170 -0
  10. package/agents/cap-reviewer.md +230 -0
  11. package/agents/cap-tester.md +193 -0
  12. package/bin/install.js +5002 -0
  13. package/cap/bin/gsd-tools.cjs +1141 -0
  14. package/cap/bin/lib/arc-scanner.cjs +341 -0
  15. package/cap/bin/lib/cap-feature-map.cjs +506 -0
  16. package/cap/bin/lib/cap-session.cjs +191 -0
  17. package/cap/bin/lib/cap-stack-docs.cjs +598 -0
  18. package/cap/bin/lib/cap-tag-scanner.cjs +458 -0
  19. package/cap/bin/lib/commands.cjs +959 -0
  20. package/cap/bin/lib/config.cjs +466 -0
  21. package/cap/bin/lib/convention-reader.cjs +180 -0
  22. package/cap/bin/lib/core.cjs +1230 -0
  23. package/cap/bin/lib/feature-aggregator.cjs +422 -0
  24. package/cap/bin/lib/frontmatter.cjs +336 -0
  25. package/cap/bin/lib/init.cjs +1442 -0
  26. package/cap/bin/lib/manifest-generator.cjs +381 -0
  27. package/cap/bin/lib/milestone.cjs +252 -0
  28. package/cap/bin/lib/model-profiles.cjs +68 -0
  29. package/cap/bin/lib/monorepo-context.cjs +224 -0
  30. package/cap/bin/lib/monorepo-migrator.cjs +507 -0
  31. package/cap/bin/lib/phase.cjs +888 -0
  32. package/cap/bin/lib/profile-output.cjs +952 -0
  33. package/cap/bin/lib/profile-pipeline.cjs +539 -0
  34. package/cap/bin/lib/roadmap.cjs +329 -0
  35. package/cap/bin/lib/security.cjs +382 -0
  36. package/cap/bin/lib/session-manager.cjs +290 -0
  37. package/cap/bin/lib/skeleton-generator.cjs +177 -0
  38. package/cap/bin/lib/state.cjs +1031 -0
  39. package/cap/bin/lib/template.cjs +222 -0
  40. package/cap/bin/lib/test-detector.cjs +61 -0
  41. package/cap/bin/lib/uat.cjs +282 -0
  42. package/cap/bin/lib/verify.cjs +888 -0
  43. package/cap/bin/lib/workspace-detector.cjs +369 -0
  44. package/cap/bin/lib/workstream.cjs +491 -0
  45. package/cap/commands/gsd/workstreams.md +63 -0
  46. package/cap/references/arc-standard.md +315 -0
  47. package/cap/references/cap-agent-architecture.md +102 -0
  48. package/cap/references/cap-gitignore-template +9 -0
  49. package/cap/references/cap-zero-deps.md +158 -0
  50. package/cap/references/checkpoints.md +778 -0
  51. package/cap/references/continuation-format.md +249 -0
  52. package/cap/references/decimal-phase-calculation.md +64 -0
  53. package/cap/references/feature-map-template.md +25 -0
  54. package/cap/references/git-integration.md +295 -0
  55. package/cap/references/git-planning-commit.md +38 -0
  56. package/cap/references/model-profile-resolution.md +36 -0
  57. package/cap/references/model-profiles.md +139 -0
  58. package/cap/references/phase-argument-parsing.md +61 -0
  59. package/cap/references/planning-config.md +202 -0
  60. package/cap/references/questioning.md +162 -0
  61. package/cap/references/session-template.json +8 -0
  62. package/cap/references/tdd.md +263 -0
  63. package/cap/references/ui-brand.md +160 -0
  64. package/cap/references/user-profiling.md +681 -0
  65. package/cap/references/verification-patterns.md +612 -0
  66. package/cap/references/workstream-flag.md +58 -0
  67. package/cap/templates/DEBUG.md +164 -0
  68. package/cap/templates/UAT.md +265 -0
  69. package/cap/templates/UI-SPEC.md +100 -0
  70. package/cap/templates/VALIDATION.md +76 -0
  71. package/cap/templates/claude-md.md +122 -0
  72. package/cap/templates/codebase/architecture.md +255 -0
  73. package/cap/templates/codebase/concerns.md +310 -0
  74. package/cap/templates/codebase/conventions.md +307 -0
  75. package/cap/templates/codebase/integrations.md +280 -0
  76. package/cap/templates/codebase/stack.md +186 -0
  77. package/cap/templates/codebase/structure.md +285 -0
  78. package/cap/templates/codebase/testing.md +480 -0
  79. package/cap/templates/config.json +44 -0
  80. package/cap/templates/context.md +352 -0
  81. package/cap/templates/continue-here.md +78 -0
  82. package/cap/templates/copilot-instructions.md +7 -0
  83. package/cap/templates/debug-subagent-prompt.md +91 -0
  84. package/cap/templates/dev-preferences.md +21 -0
  85. package/cap/templates/discovery.md +146 -0
  86. package/cap/templates/discussion-log.md +63 -0
  87. package/cap/templates/milestone-archive.md +123 -0
  88. package/cap/templates/milestone.md +115 -0
  89. package/cap/templates/phase-prompt.md +610 -0
  90. package/cap/templates/planner-subagent-prompt.md +117 -0
  91. package/cap/templates/project.md +186 -0
  92. package/cap/templates/requirements.md +231 -0
  93. package/cap/templates/research-project/ARCHITECTURE.md +204 -0
  94. package/cap/templates/research-project/FEATURES.md +147 -0
  95. package/cap/templates/research-project/PITFALLS.md +200 -0
  96. package/cap/templates/research-project/STACK.md +120 -0
  97. package/cap/templates/research-project/SUMMARY.md +170 -0
  98. package/cap/templates/research.md +552 -0
  99. package/cap/templates/retrospective.md +54 -0
  100. package/cap/templates/roadmap.md +202 -0
  101. package/cap/templates/state.md +176 -0
  102. package/cap/templates/summary-complex.md +59 -0
  103. package/cap/templates/summary-minimal.md +41 -0
  104. package/cap/templates/summary-standard.md +48 -0
  105. package/cap/templates/summary.md +248 -0
  106. package/cap/templates/user-profile.md +146 -0
  107. package/cap/templates/user-setup.md +311 -0
  108. package/cap/templates/verification-report.md +322 -0
  109. package/cap/workflows/add-phase.md +112 -0
  110. package/cap/workflows/add-tests.md +351 -0
  111. package/cap/workflows/add-todo.md +158 -0
  112. package/cap/workflows/audit-milestone.md +340 -0
  113. package/cap/workflows/audit-uat.md +109 -0
  114. package/cap/workflows/autonomous.md +891 -0
  115. package/cap/workflows/check-todos.md +177 -0
  116. package/cap/workflows/cleanup.md +152 -0
  117. package/cap/workflows/complete-milestone.md +767 -0
  118. package/cap/workflows/diagnose-issues.md +231 -0
  119. package/cap/workflows/discovery-phase.md +289 -0
  120. package/cap/workflows/discuss-phase-assumptions.md +653 -0
  121. package/cap/workflows/discuss-phase.md +1049 -0
  122. package/cap/workflows/do.md +104 -0
  123. package/cap/workflows/execute-phase.md +846 -0
  124. package/cap/workflows/execute-plan.md +514 -0
  125. package/cap/workflows/fast.md +105 -0
  126. package/cap/workflows/forensics.md +265 -0
  127. package/cap/workflows/health.md +181 -0
  128. package/cap/workflows/help.md +660 -0
  129. package/cap/workflows/insert-phase.md +130 -0
  130. package/cap/workflows/list-phase-assumptions.md +178 -0
  131. package/cap/workflows/list-workspaces.md +56 -0
  132. package/cap/workflows/manager.md +362 -0
  133. package/cap/workflows/map-codebase.md +377 -0
  134. package/cap/workflows/milestone-summary.md +223 -0
  135. package/cap/workflows/new-milestone.md +486 -0
  136. package/cap/workflows/new-project.md +1250 -0
  137. package/cap/workflows/new-workspace.md +237 -0
  138. package/cap/workflows/next.md +97 -0
  139. package/cap/workflows/node-repair.md +92 -0
  140. package/cap/workflows/note.md +156 -0
  141. package/cap/workflows/pause-work.md +176 -0
  142. package/cap/workflows/plan-milestone-gaps.md +273 -0
  143. package/cap/workflows/plan-phase.md +859 -0
  144. package/cap/workflows/plant-seed.md +169 -0
  145. package/cap/workflows/pr-branch.md +129 -0
  146. package/cap/workflows/profile-user.md +450 -0
  147. package/cap/workflows/progress.md +507 -0
  148. package/cap/workflows/quick.md +757 -0
  149. package/cap/workflows/remove-phase.md +155 -0
  150. package/cap/workflows/remove-workspace.md +90 -0
  151. package/cap/workflows/research-phase.md +82 -0
  152. package/cap/workflows/resume-project.md +326 -0
  153. package/cap/workflows/review.md +228 -0
  154. package/cap/workflows/session-report.md +146 -0
  155. package/cap/workflows/settings.md +283 -0
  156. package/cap/workflows/ship.md +228 -0
  157. package/cap/workflows/stats.md +60 -0
  158. package/cap/workflows/transition.md +671 -0
  159. package/cap/workflows/ui-phase.md +302 -0
  160. package/cap/workflows/ui-review.md +165 -0
  161. package/cap/workflows/update.md +323 -0
  162. package/cap/workflows/validate-phase.md +174 -0
  163. package/cap/workflows/verify-phase.md +254 -0
  164. package/cap/workflows/verify-work.md +637 -0
  165. package/commands/cap/annotate.md +165 -0
  166. package/commands/cap/brainstorm.md +238 -0
  167. package/commands/cap/debug.md +297 -0
  168. package/commands/cap/init.md +262 -0
  169. package/commands/cap/iterate.md +234 -0
  170. package/commands/cap/prototype.md +281 -0
  171. package/commands/cap/refresh-docs.md +37 -0
  172. package/commands/cap/review.md +272 -0
  173. package/commands/cap/scan.md +249 -0
  174. package/commands/cap/start.md +234 -0
  175. package/commands/cap/status.md +189 -0
  176. package/commands/cap/test.md +250 -0
  177. package/hooks/dist/gsd-check-update.js +114 -0
  178. package/hooks/dist/gsd-context-monitor.js +156 -0
  179. package/hooks/dist/gsd-prompt-guard.js +96 -0
  180. package/hooks/dist/gsd-statusline.js +119 -0
  181. package/hooks/dist/gsd-workflow-guard.js +94 -0
  182. package/package.json +51 -0
  183. package/scripts/base64-scan.sh +262 -0
  184. package/scripts/build-hooks.js +82 -0
  185. package/scripts/cap-removal-checklist.md +202 -0
  186. package/scripts/prompt-injection-scan.sh +198 -0
  187. package/scripts/run-tests.cjs +29 -0
  188. package/scripts/secret-scan.sh +227 -0
@@ -0,0 +1,458 @@
1
+ // @gsd-context CAP v2.0 tag scanner -- extracts @cap-feature, @cap-todo, @cap-risk, and @cap-decision tags from source files.
2
+ // @gsd-decision Separate module from arc-scanner.cjs -- CAP tags use @cap- prefix (not @gsd-) and have different metadata semantics (feature: key instead of phase: key).
3
+ // @gsd-decision Regex-based extraction (not AST) -- language-agnostic, zero dependencies, proven sufficient in GSD arc-scanner.cjs.
4
+ // @gsd-constraint Zero external dependencies -- uses only Node.js built-ins (fs, path).
5
+ // @gsd-pattern Same comment anchor rule as ARC: tag is only valid when first non-whitespace content on a line is a comment token.
6
+
7
+ 'use strict';
8
+
9
+ const fs = require('node:fs');
10
+ const path = require('node:path');
11
+
12
+ // @gsd-todo(ref:AC-20) Primary tags are @cap-feature and @cap-todo; risk and decision are optional standalone tags
13
+ // @gsd-decision CAP tag types: 2 primary (feature, todo) + 2 optional (risk, decision). Simplified from GSD's 8 types.
14
+ const CAP_TAG_TYPES = ['feature', 'todo', 'risk', 'decision'];
15
+
16
+ // @gsd-todo(ref:AC-25) Tag scanner uses native RegExp with dotAll flag for multiline extraction
17
+ // @gsd-pattern Tag regex anchors to comment tokens at line start -- identical approach to arc-scanner.cjs
18
+ const CAP_TAG_RE = /^[ \t]*(?:\/\/|\/\*|\*|#|--|"""|''')[ \t]*@cap-(feature|todo|risk|decision)(?:\(([^)]*)\))?[ \t]*(.*)/;
19
+
20
+ // @gsd-todo(ref:AC-26) Tag scanner is language-agnostic, operating on comment syntax patterns across JS, TS, Python, Ruby, Shell
21
+ const SUPPORTED_EXTENSIONS = ['.js', '.cjs', '.mjs', '.ts', '.tsx', '.jsx', '.py', '.rb', '.sh', '.bash', '.sql', '.go', '.rs', '.java', '.c', '.cpp', '.h', '.hpp'];
22
+ const DEFAULT_EXCLUDE = ['node_modules', '.git', '.cap', 'dist', 'build', 'coverage', '.planning'];
23
+
24
+ // @gsd-todo(ref:AC-22) @cap-todo supports structured subtypes: risk:..., decision:...
25
+ // @gsd-decision Subtype detection uses prefix matching on the description text (e.g., "risk: memory leak" -> subtype: "risk")
26
+ const SUBTYPE_RE = /^(risk|decision):\s*(.*)/;
27
+
28
+ /**
29
+ * @typedef {Object} CapTag
30
+ * @property {string} type - Tag type without @cap- prefix ('feature', 'todo', 'risk', 'decision')
31
+ * @property {string} file - Relative path from project root
32
+ * @property {number} line - 1-based line number
33
+ * @property {Object<string,string>} metadata - Parsed key-value pairs from parenthesized block
34
+ * @property {string} description - Text after metadata block
35
+ * @property {string} raw - Complete original line
36
+ * @property {string|null} subtype - For @cap-todo: 'risk' or 'decision' if prefixed, else null
37
+ */
38
+
39
+ // @gsd-api parseMetadata(metadataStr) -- Parses parenthesized key:value pairs.
40
+ // Returns: Object<string,string> -- flat key-value object.
41
+ /**
42
+ * @param {string} metadataStr - Raw metadata string without parens (e.g., "feature:auth, ac:AUTH/AC-1")
43
+ * @returns {Object<string,string>}
44
+ */
45
+ function parseMetadata(metadataStr) {
46
+ if (!metadataStr || !metadataStr.trim()) return {};
47
+ const result = {};
48
+ const pairs = metadataStr.split(',');
49
+ for (const pair of pairs) {
50
+ const trimmed = pair.trim();
51
+ if (!trimmed) continue;
52
+ const colonIdx = trimmed.indexOf(':');
53
+ if (colonIdx === -1) {
54
+ // Key without value -- store as truthy flag
55
+ result[trimmed] = 'true';
56
+ } else {
57
+ const key = trimmed.slice(0, colonIdx).trim();
58
+ const value = trimmed.slice(colonIdx + 1).trim();
59
+ if (key) result[key] = value;
60
+ }
61
+ }
62
+ return result;
63
+ }
64
+
65
+ // @gsd-api extractTags(content, filePath) -- Regex extraction engine supporting //, #, /* */, """ """ comment styles.
66
+ // Returns: CapTag[] -- array of extracted tags.
67
+ /**
68
+ * @param {string} content - File content to scan
69
+ * @param {string} filePath - Relative file path (for tag metadata)
70
+ * @returns {CapTag[]}
71
+ */
72
+ function extractTags(content, filePath) {
73
+ const lines = content.split('\n');
74
+ const tags = [];
75
+ for (let i = 0; i < lines.length; i++) {
76
+ const line = lines[i];
77
+ const match = line.match(CAP_TAG_RE);
78
+ if (!match) continue;
79
+
80
+ const type = match[1];
81
+ const metadataStr = match[2] || '';
82
+ const description = (match[3] || '').trim();
83
+ const metadata = parseMetadata(metadataStr);
84
+
85
+ // @gsd-todo(ref:AC-22) Detect subtypes in @cap-todo description (risk:..., decision:...)
86
+ let subtype = null;
87
+ if (type === 'todo') {
88
+ const subtypeMatch = description.match(SUBTYPE_RE);
89
+ if (subtypeMatch) {
90
+ subtype = subtypeMatch[1];
91
+ }
92
+ }
93
+
94
+ tags.push({
95
+ type,
96
+ file: filePath,
97
+ line: i + 1,
98
+ metadata,
99
+ description,
100
+ raw: line,
101
+ subtype,
102
+ });
103
+ }
104
+ return tags;
105
+ }
106
+
107
+ // @gsd-api scanFile(filePath, projectRoot) -- Scans a single file for @cap-* tags.
108
+ // Returns: CapTag[] -- array of extracted tags with file, line, metadata, description.
109
+ /**
110
+ * @param {string} filePath - Absolute path to file
111
+ * @param {string} projectRoot - Absolute path to project root (for relative path computation)
112
+ * @returns {CapTag[]}
113
+ */
114
+ function scanFile(filePath, projectRoot) {
115
+ // @gsd-todo(ref:AC-25) Use native RegExp for tag extraction -- no AST parsing
116
+ let content;
117
+ try {
118
+ content = fs.readFileSync(filePath, 'utf8');
119
+ } catch (_e) {
120
+ return [];
121
+ }
122
+ const relativePath = path.relative(projectRoot, filePath);
123
+ return extractTags(content, relativePath);
124
+ }
125
+
126
+ // @gsd-api scanDirectory(dirPath, options) -- Recursively scans a directory for @cap-* tags.
127
+ // Returns: CapTag[] -- aggregated tags from all matching files.
128
+ // Options: { extensions?: string[], exclude?: string[] }
129
+ /**
130
+ * @param {string} dirPath - Absolute path to directory to scan
131
+ * @param {Object} [options]
132
+ * @param {string[]} [options.extensions] - File extensions to include (e.g., ['.js', '.ts', '.py'])
133
+ * @param {string[]} [options.exclude] - Directory names to exclude (e.g., ['node_modules', '.git'])
134
+ * @param {string} [options.projectRoot] - Project root for relative paths (defaults to dirPath)
135
+ * @returns {CapTag[]}
136
+ */
137
+ function scanDirectory(dirPath, options = {}) {
138
+ const extensions = options.extensions || SUPPORTED_EXTENSIONS;
139
+ const exclude = options.exclude || DEFAULT_EXCLUDE;
140
+ const projectRoot = options.projectRoot || dirPath;
141
+ const tags = [];
142
+
143
+ // @gsd-constraint Uses readdirSync (not glob) per project zero-dep constraint
144
+ function walk(dir) {
145
+ let entries;
146
+ try {
147
+ entries = fs.readdirSync(dir, { withFileTypes: true });
148
+ } catch (_e) {
149
+ return;
150
+ }
151
+ for (const entry of entries) {
152
+ const fullPath = path.join(dir, entry.name);
153
+ if (entry.isDirectory()) {
154
+ if (exclude.includes(entry.name)) continue;
155
+ walk(fullPath);
156
+ } else if (entry.isFile()) {
157
+ const ext = path.extname(entry.name);
158
+ if (!extensions.includes(ext)) continue;
159
+ const fileTags = scanFile(fullPath, projectRoot);
160
+ tags.push(...fileTags);
161
+ }
162
+ }
163
+ }
164
+
165
+ walk(dirPath);
166
+ return tags;
167
+ }
168
+
169
+ // @gsd-api groupByFeature(tags) -- Groups tags by their feature: metadata value.
170
+ // Returns: Object<string, CapTag[]> -- map from feature name to tags.
171
+ /**
172
+ * @param {CapTag[]} tags - Array of extracted tags
173
+ * @returns {Object<string, CapTag[]>}
174
+ */
175
+ function groupByFeature(tags) {
176
+ const groups = {};
177
+ for (const tag of tags) {
178
+ const featureId = tag.metadata.feature || '(unassigned)';
179
+ if (!groups[featureId]) groups[featureId] = [];
180
+ groups[featureId].push(tag);
181
+ }
182
+ return groups;
183
+ }
184
+
185
+ // @gsd-api detectOrphans(tags, featureIds) -- Compare tags against Feature Map entries, fuzzy-match hints for orphans.
186
+ // Returns: Array of { tag, hint } where hint is the closest matching feature ID.
187
+ // @gsd-todo(ref:AC-15) Orphan tags flagged with fuzzy-match hint suggesting closest existing feature ID
188
+ /**
189
+ * @param {CapTag[]} tags - Array of extracted tags
190
+ * @param {string[]} featureIds - Known feature IDs from Feature Map (e.g., ['F-001', 'F-002'])
191
+ * @returns {{ tag: CapTag, hint: string|null }[]}
192
+ */
193
+ function detectOrphans(tags, featureIds) {
194
+ const orphans = [];
195
+ const featureSet = new Set(featureIds);
196
+
197
+ for (const tag of tags) {
198
+ const tagFeatureId = tag.metadata.feature;
199
+ if (!tagFeatureId) continue;
200
+ if (featureSet.has(tagFeatureId)) continue;
201
+
202
+ // Fuzzy match: find closest feature ID by Levenshtein-like similarity
203
+ const hint = findClosestMatch(tagFeatureId, featureIds);
204
+ orphans.push({ tag, hint });
205
+ }
206
+
207
+ return orphans;
208
+ }
209
+
210
+ // @gsd-decision Simple character-level distance for fuzzy matching -- no external library needed
211
+ /**
212
+ * Compute edit distance between two strings (Levenshtein).
213
+ * @param {string} a
214
+ * @param {string} b
215
+ * @returns {number}
216
+ */
217
+ function editDistance(a, b) {
218
+ const la = a.length;
219
+ const lb = b.length;
220
+ const dp = Array.from({ length: la + 1 }, () => Array(lb + 1).fill(0));
221
+ for (let i = 0; i <= la; i++) dp[i][0] = i;
222
+ for (let j = 0; j <= lb; j++) dp[0][j] = j;
223
+ for (let i = 1; i <= la; i++) {
224
+ for (let j = 1; j <= lb; j++) {
225
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
226
+ dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost);
227
+ }
228
+ }
229
+ return dp[la][lb];
230
+ }
231
+
232
+ /**
233
+ * Find the closest matching string from candidates using edit distance.
234
+ * @param {string} target
235
+ * @param {string[]} candidates
236
+ * @returns {string|null}
237
+ */
238
+ function findClosestMatch(target, candidates) {
239
+ if (candidates.length === 0) return null;
240
+ let bestDist = Infinity;
241
+ let bestMatch = null;
242
+ const lowerTarget = target.toLowerCase();
243
+ for (const candidate of candidates) {
244
+ const dist = editDistance(lowerTarget, candidate.toLowerCase());
245
+ if (dist < bestDist) {
246
+ bestDist = dist;
247
+ bestMatch = candidate;
248
+ }
249
+ }
250
+ // Only suggest if distance is reasonable (less than half the target length)
251
+ if (bestDist <= Math.ceil(target.length / 2)) return bestMatch;
252
+ return null;
253
+ }
254
+
255
+ // @gsd-todo(ref:AC-78) /cap:scan shall traverse all packages in a monorepo
256
+ // @gsd-todo(ref:AC-93) Zero runtime dependencies -- uses only Node.js built-ins
257
+ // @gsd-todo(ref:AC-94) Tag scanner uses native RegExp -- no comment-parser or AST parser
258
+ // @gsd-todo(ref:AC-95) File discovery uses fs.readdirSync with recursive walk -- no glob library
259
+ // @gsd-todo(ref:AC-96) CLI argument parsing uses existing parseNamedArgs() pattern
260
+
261
+ // @gsd-api detectWorkspaces(projectRoot) -- Detects monorepo workspaces from package.json and lerna.json.
262
+ // Returns: { isMonorepo: boolean, packages: string[] }
263
+ /**
264
+ * @param {string} projectRoot - Absolute path to project root
265
+ * @returns {{ isMonorepo: boolean, packages: string[] }}
266
+ */
267
+ function detectWorkspaces(projectRoot) {
268
+ const result = { isMonorepo: false, packages: [] };
269
+
270
+ // Check package.json workspaces (npm/yarn/pnpm)
271
+ const pkgPath = path.join(projectRoot, 'package.json');
272
+ if (fs.existsSync(pkgPath)) {
273
+ try {
274
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
275
+ if (pkg.workspaces) {
276
+ result.isMonorepo = true;
277
+ const patterns = Array.isArray(pkg.workspaces)
278
+ ? pkg.workspaces
279
+ : (pkg.workspaces.packages || []);
280
+ result.packages = resolveWorkspaceGlobs(projectRoot, patterns);
281
+ }
282
+ } catch (_e) {
283
+ // Malformed package.json
284
+ }
285
+ }
286
+
287
+ // Check lerna.json
288
+ if (!result.isMonorepo) {
289
+ const lernaPath = path.join(projectRoot, 'lerna.json');
290
+ if (fs.existsSync(lernaPath)) {
291
+ try {
292
+ const lerna = JSON.parse(fs.readFileSync(lernaPath, 'utf8'));
293
+ result.isMonorepo = true;
294
+ const patterns = lerna.packages || ['packages/*'];
295
+ result.packages = resolveWorkspaceGlobs(projectRoot, patterns);
296
+ } catch (_e) {
297
+ // Malformed lerna.json
298
+ }
299
+ }
300
+ }
301
+
302
+ return result;
303
+ }
304
+
305
+ // @gsd-api resolveWorkspaceGlobs(projectRoot, patterns) -- Expands workspace glob patterns to actual directories.
306
+ // @gsd-decision Uses fs.readdirSync instead of glob library for workspace pattern expansion. Handles only simple patterns (dir/* and dir/**).
307
+ /**
308
+ * @param {string} projectRoot - Absolute path to project root
309
+ * @param {string[]} patterns - Workspace glob patterns (e.g., ["packages/*", "apps/*"])
310
+ * @returns {string[]} - Array of relative package directory paths
311
+ */
312
+ function resolveWorkspaceGlobs(projectRoot, patterns) {
313
+ const packages = [];
314
+
315
+ for (const pattern of patterns) {
316
+ // Strip trailing glob: "packages/*" -> "packages", "apps/**" -> "apps"
317
+ const baseDir = pattern.replace(/\/\*+$/, '');
318
+ const fullPath = path.join(projectRoot, baseDir);
319
+
320
+ if (!fs.existsSync(fullPath)) continue;
321
+
322
+ const stat = fs.statSync(fullPath);
323
+ if (!stat.isDirectory()) continue;
324
+
325
+ // If pattern has no glob, it is a direct package reference
326
+ if (!pattern.includes('*')) {
327
+ packages.push(baseDir);
328
+ continue;
329
+ }
330
+
331
+ // Enumerate subdirectories
332
+ try {
333
+ const entries = fs.readdirSync(fullPath, { withFileTypes: true });
334
+ for (const entry of entries) {
335
+ if (entry.isDirectory() && !entry.name.startsWith('.')) {
336
+ packages.push(path.join(baseDir, entry.name));
337
+ }
338
+ }
339
+ } catch (_e) {
340
+ // Skip unreadable directories
341
+ }
342
+ }
343
+
344
+ return packages;
345
+ }
346
+
347
+ // @gsd-api scanMonorepo(projectRoot, options) -- Scans all workspace packages in a monorepo for @cap-* tags.
348
+ // @gsd-todo(ref:AC-79) Feature Map entries support cross-package file references (e.g., packages/core/src/auth.ts)
349
+ // @gsd-todo(ref:AC-80) Works seamlessly with single-repo projects -- returns regular scanDirectory results if not a monorepo
350
+ /**
351
+ * Scans a monorepo or single repo for @cap-* tags.
352
+ * In monorepo mode: scans root + each workspace package.
353
+ * In single-repo mode: delegates to scanDirectory.
354
+ * All file paths are relative to project root for cross-package references.
355
+ *
356
+ * @param {string} projectRoot - Absolute path to project root
357
+ * @param {Object} [options]
358
+ * @param {string[]} [options.extensions] - File extensions to include
359
+ * @param {string[]} [options.exclude] - Directory names to exclude
360
+ * @returns {{ tags: CapTag[], isMonorepo: boolean, packages: string[] }}
361
+ */
362
+ function scanMonorepo(projectRoot, options = {}) {
363
+ const workspaces = detectWorkspaces(projectRoot);
364
+
365
+ if (!workspaces.isMonorepo) {
366
+ // Single repo -- delegate to base scanner
367
+ const tags = scanDirectory(projectRoot, {
368
+ ...options,
369
+ projectRoot,
370
+ });
371
+ return { tags, isMonorepo: false, packages: [] };
372
+ }
373
+
374
+ // Monorepo -- scan root and each package
375
+ const allTags = [];
376
+ const seen = new Set();
377
+
378
+ // Scan root (excludes workspace dirs by default since they are scanned separately)
379
+ const rootTags = scanDirectory(projectRoot, {
380
+ ...options,
381
+ projectRoot,
382
+ });
383
+ for (const tag of rootTags) {
384
+ const key = `${tag.file}:${tag.line}`;
385
+ if (!seen.has(key)) {
386
+ seen.add(key);
387
+ allTags.push(tag);
388
+ }
389
+ }
390
+
391
+ // Scan each workspace package
392
+ for (const pkg of workspaces.packages) {
393
+ const pkgDir = path.join(projectRoot, pkg);
394
+ if (!fs.existsSync(pkgDir)) continue;
395
+
396
+ const pkgTags = scanDirectory(pkgDir, {
397
+ ...options,
398
+ projectRoot, // Paths relative to monorepo root, not package root
399
+ });
400
+
401
+ for (const tag of pkgTags) {
402
+ const key = `${tag.file}:${tag.line}`;
403
+ if (!seen.has(key)) {
404
+ seen.add(key);
405
+ allTags.push(tag);
406
+ }
407
+ }
408
+ }
409
+
410
+ return { tags: allTags, isMonorepo: true, packages: workspaces.packages };
411
+ }
412
+
413
+ // @gsd-api groupByPackage(tags) -- Groups tags by their workspace package based on file path prefix.
414
+ /**
415
+ * @param {CapTag[]} tags - Array of extracted tags
416
+ * @param {string[]} packages - Known workspace package paths
417
+ * @returns {Object<string, CapTag[]>}
418
+ */
419
+ function groupByPackage(tags, packages) {
420
+ const groups = { '(root)': [] };
421
+ for (const pkg of packages) {
422
+ groups[pkg] = [];
423
+ }
424
+
425
+ for (const tag of tags) {
426
+ let matched = false;
427
+ for (const pkg of packages) {
428
+ if (tag.file.startsWith(pkg + '/') || tag.file.startsWith(pkg + path.sep)) {
429
+ groups[pkg].push(tag);
430
+ matched = true;
431
+ break;
432
+ }
433
+ }
434
+ if (!matched) {
435
+ groups['(root)'].push(tag);
436
+ }
437
+ }
438
+
439
+ return groups;
440
+ }
441
+
442
+ module.exports = {
443
+ CAP_TAG_TYPES,
444
+ CAP_TAG_RE,
445
+ SUPPORTED_EXTENSIONS,
446
+ DEFAULT_EXCLUDE,
447
+ scanFile,
448
+ scanDirectory,
449
+ extractTags,
450
+ parseMetadata,
451
+ groupByFeature,
452
+ detectOrphans,
453
+ editDistance,
454
+ detectWorkspaces,
455
+ resolveWorkspaceGlobs,
456
+ scanMonorepo,
457
+ groupByPackage,
458
+ };