gsd-code-first 1.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 (238) 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 +937 -0
  5. package/README.pt-BR.md +452 -0
  6. package/README.zh-CN.md +800 -0
  7. package/agents/gsd-advisor-researcher.md +104 -0
  8. package/agents/gsd-annotator.md +148 -0
  9. package/agents/gsd-arc-executor.md +537 -0
  10. package/agents/gsd-arc-planner.md +374 -0
  11. package/agents/gsd-assumptions-analyzer.md +105 -0
  12. package/agents/gsd-code-planner.md +155 -0
  13. package/agents/gsd-codebase-mapper.md +770 -0
  14. package/agents/gsd-debugger.md +1373 -0
  15. package/agents/gsd-executor.md +509 -0
  16. package/agents/gsd-integration-checker.md +443 -0
  17. package/agents/gsd-nyquist-auditor.md +176 -0
  18. package/agents/gsd-phase-researcher.md +698 -0
  19. package/agents/gsd-plan-checker.md +773 -0
  20. package/agents/gsd-planner.md +1354 -0
  21. package/agents/gsd-project-researcher.md +654 -0
  22. package/agents/gsd-prototyper.md +161 -0
  23. package/agents/gsd-research-synthesizer.md +247 -0
  24. package/agents/gsd-roadmapper.md +679 -0
  25. package/agents/gsd-ui-auditor.md +439 -0
  26. package/agents/gsd-ui-checker.md +300 -0
  27. package/agents/gsd-ui-researcher.md +357 -0
  28. package/agents/gsd-user-profiler.md +171 -0
  29. package/agents/gsd-verifier.md +700 -0
  30. package/bin/install.js +5009 -0
  31. package/commands/gsd/add-backlog.md +76 -0
  32. package/commands/gsd/add-phase.md +43 -0
  33. package/commands/gsd/add-tests.md +41 -0
  34. package/commands/gsd/add-todo.md +47 -0
  35. package/commands/gsd/annotate.md +54 -0
  36. package/commands/gsd/audit-milestone.md +36 -0
  37. package/commands/gsd/audit-uat.md +24 -0
  38. package/commands/gsd/autonomous.md +41 -0
  39. package/commands/gsd/check-todos.md +45 -0
  40. package/commands/gsd/cleanup.md +18 -0
  41. package/commands/gsd/complete-milestone.md +136 -0
  42. package/commands/gsd/debug.md +173 -0
  43. package/commands/gsd/deep-plan.md +52 -0
  44. package/commands/gsd/discuss-phase.md +64 -0
  45. package/commands/gsd/do.md +30 -0
  46. package/commands/gsd/execute-phase.md +59 -0
  47. package/commands/gsd/extract-plan.md +35 -0
  48. package/commands/gsd/fast.md +30 -0
  49. package/commands/gsd/forensics.md +56 -0
  50. package/commands/gsd/health.md +22 -0
  51. package/commands/gsd/help.md +22 -0
  52. package/commands/gsd/insert-phase.md +32 -0
  53. package/commands/gsd/iterate.md +124 -0
  54. package/commands/gsd/join-discord.md +18 -0
  55. package/commands/gsd/list-phase-assumptions.md +46 -0
  56. package/commands/gsd/list-workspaces.md +19 -0
  57. package/commands/gsd/manager.md +39 -0
  58. package/commands/gsd/map-codebase.md +71 -0
  59. package/commands/gsd/milestone-summary.md +51 -0
  60. package/commands/gsd/new-milestone.md +44 -0
  61. package/commands/gsd/new-project.md +42 -0
  62. package/commands/gsd/new-workspace.md +44 -0
  63. package/commands/gsd/next.md +24 -0
  64. package/commands/gsd/note.md +34 -0
  65. package/commands/gsd/pause-work.md +38 -0
  66. package/commands/gsd/plan-milestone-gaps.md +34 -0
  67. package/commands/gsd/plan-phase.md +47 -0
  68. package/commands/gsd/plant-seed.md +28 -0
  69. package/commands/gsd/pr-branch.md +25 -0
  70. package/commands/gsd/profile-user.md +46 -0
  71. package/commands/gsd/progress.md +24 -0
  72. package/commands/gsd/prototype.md +56 -0
  73. package/commands/gsd/quick.md +47 -0
  74. package/commands/gsd/reapply-patches.md +123 -0
  75. package/commands/gsd/remove-phase.md +31 -0
  76. package/commands/gsd/remove-workspace.md +26 -0
  77. package/commands/gsd/research-phase.md +195 -0
  78. package/commands/gsd/resume-work.md +40 -0
  79. package/commands/gsd/review-backlog.md +61 -0
  80. package/commands/gsd/review.md +37 -0
  81. package/commands/gsd/session-report.md +19 -0
  82. package/commands/gsd/set-mode.md +41 -0
  83. package/commands/gsd/set-profile.md +12 -0
  84. package/commands/gsd/settings.md +36 -0
  85. package/commands/gsd/ship.md +23 -0
  86. package/commands/gsd/stats.md +18 -0
  87. package/commands/gsd/thread.md +127 -0
  88. package/commands/gsd/ui-phase.md +34 -0
  89. package/commands/gsd/ui-review.md +32 -0
  90. package/commands/gsd/update.md +37 -0
  91. package/commands/gsd/validate-phase.md +35 -0
  92. package/commands/gsd/verify-work.md +38 -0
  93. package/commands/gsd/workstreams.md +63 -0
  94. package/get-shit-done/bin/gsd-tools.cjs +946 -0
  95. package/get-shit-done/bin/lib/arc-scanner.cjs +341 -0
  96. package/get-shit-done/bin/lib/commands.cjs +959 -0
  97. package/get-shit-done/bin/lib/config.cjs +466 -0
  98. package/get-shit-done/bin/lib/core.cjs +1230 -0
  99. package/get-shit-done/bin/lib/frontmatter.cjs +336 -0
  100. package/get-shit-done/bin/lib/init.cjs +1442 -0
  101. package/get-shit-done/bin/lib/milestone.cjs +252 -0
  102. package/get-shit-done/bin/lib/model-profiles.cjs +68 -0
  103. package/get-shit-done/bin/lib/phase.cjs +888 -0
  104. package/get-shit-done/bin/lib/profile-output.cjs +952 -0
  105. package/get-shit-done/bin/lib/profile-pipeline.cjs +539 -0
  106. package/get-shit-done/bin/lib/roadmap.cjs +329 -0
  107. package/get-shit-done/bin/lib/security.cjs +382 -0
  108. package/get-shit-done/bin/lib/state.cjs +1031 -0
  109. package/get-shit-done/bin/lib/template.cjs +222 -0
  110. package/get-shit-done/bin/lib/uat.cjs +282 -0
  111. package/get-shit-done/bin/lib/verify.cjs +888 -0
  112. package/get-shit-done/bin/lib/workstream.cjs +491 -0
  113. package/get-shit-done/commands/gsd/workstreams.md +63 -0
  114. package/get-shit-done/references/arc-standard.md +315 -0
  115. package/get-shit-done/references/checkpoints.md +778 -0
  116. package/get-shit-done/references/continuation-format.md +249 -0
  117. package/get-shit-done/references/decimal-phase-calculation.md +64 -0
  118. package/get-shit-done/references/git-integration.md +295 -0
  119. package/get-shit-done/references/git-planning-commit.md +38 -0
  120. package/get-shit-done/references/model-profile-resolution.md +36 -0
  121. package/get-shit-done/references/model-profiles.md +139 -0
  122. package/get-shit-done/references/phase-argument-parsing.md +61 -0
  123. package/get-shit-done/references/planning-config.md +202 -0
  124. package/get-shit-done/references/questioning.md +162 -0
  125. package/get-shit-done/references/tdd.md +263 -0
  126. package/get-shit-done/references/ui-brand.md +160 -0
  127. package/get-shit-done/references/user-profiling.md +681 -0
  128. package/get-shit-done/references/verification-patterns.md +612 -0
  129. package/get-shit-done/references/workstream-flag.md +58 -0
  130. package/get-shit-done/templates/DEBUG.md +164 -0
  131. package/get-shit-done/templates/UAT.md +265 -0
  132. package/get-shit-done/templates/UI-SPEC.md +100 -0
  133. package/get-shit-done/templates/VALIDATION.md +76 -0
  134. package/get-shit-done/templates/claude-md.md +122 -0
  135. package/get-shit-done/templates/codebase/architecture.md +255 -0
  136. package/get-shit-done/templates/codebase/concerns.md +310 -0
  137. package/get-shit-done/templates/codebase/conventions.md +307 -0
  138. package/get-shit-done/templates/codebase/integrations.md +280 -0
  139. package/get-shit-done/templates/codebase/stack.md +186 -0
  140. package/get-shit-done/templates/codebase/structure.md +285 -0
  141. package/get-shit-done/templates/codebase/testing.md +480 -0
  142. package/get-shit-done/templates/config.json +44 -0
  143. package/get-shit-done/templates/context.md +352 -0
  144. package/get-shit-done/templates/continue-here.md +78 -0
  145. package/get-shit-done/templates/copilot-instructions.md +7 -0
  146. package/get-shit-done/templates/debug-subagent-prompt.md +91 -0
  147. package/get-shit-done/templates/dev-preferences.md +21 -0
  148. package/get-shit-done/templates/discovery.md +146 -0
  149. package/get-shit-done/templates/discussion-log.md +63 -0
  150. package/get-shit-done/templates/milestone-archive.md +123 -0
  151. package/get-shit-done/templates/milestone.md +115 -0
  152. package/get-shit-done/templates/phase-prompt.md +610 -0
  153. package/get-shit-done/templates/planner-subagent-prompt.md +117 -0
  154. package/get-shit-done/templates/project.md +186 -0
  155. package/get-shit-done/templates/requirements.md +231 -0
  156. package/get-shit-done/templates/research-project/ARCHITECTURE.md +204 -0
  157. package/get-shit-done/templates/research-project/FEATURES.md +147 -0
  158. package/get-shit-done/templates/research-project/PITFALLS.md +200 -0
  159. package/get-shit-done/templates/research-project/STACK.md +120 -0
  160. package/get-shit-done/templates/research-project/SUMMARY.md +170 -0
  161. package/get-shit-done/templates/research.md +552 -0
  162. package/get-shit-done/templates/retrospective.md +54 -0
  163. package/get-shit-done/templates/roadmap.md +202 -0
  164. package/get-shit-done/templates/state.md +176 -0
  165. package/get-shit-done/templates/summary-complex.md +59 -0
  166. package/get-shit-done/templates/summary-minimal.md +41 -0
  167. package/get-shit-done/templates/summary-standard.md +48 -0
  168. package/get-shit-done/templates/summary.md +248 -0
  169. package/get-shit-done/templates/user-profile.md +146 -0
  170. package/get-shit-done/templates/user-setup.md +311 -0
  171. package/get-shit-done/templates/verification-report.md +322 -0
  172. package/get-shit-done/workflows/add-phase.md +112 -0
  173. package/get-shit-done/workflows/add-tests.md +351 -0
  174. package/get-shit-done/workflows/add-todo.md +158 -0
  175. package/get-shit-done/workflows/audit-milestone.md +340 -0
  176. package/get-shit-done/workflows/audit-uat.md +109 -0
  177. package/get-shit-done/workflows/autonomous.md +891 -0
  178. package/get-shit-done/workflows/check-todos.md +177 -0
  179. package/get-shit-done/workflows/cleanup.md +152 -0
  180. package/get-shit-done/workflows/complete-milestone.md +767 -0
  181. package/get-shit-done/workflows/diagnose-issues.md +231 -0
  182. package/get-shit-done/workflows/discovery-phase.md +289 -0
  183. package/get-shit-done/workflows/discuss-phase-assumptions.md +653 -0
  184. package/get-shit-done/workflows/discuss-phase.md +1049 -0
  185. package/get-shit-done/workflows/do.md +104 -0
  186. package/get-shit-done/workflows/execute-phase.md +846 -0
  187. package/get-shit-done/workflows/execute-plan.md +514 -0
  188. package/get-shit-done/workflows/fast.md +105 -0
  189. package/get-shit-done/workflows/forensics.md +265 -0
  190. package/get-shit-done/workflows/health.md +181 -0
  191. package/get-shit-done/workflows/help.md +634 -0
  192. package/get-shit-done/workflows/insert-phase.md +130 -0
  193. package/get-shit-done/workflows/list-phase-assumptions.md +178 -0
  194. package/get-shit-done/workflows/list-workspaces.md +56 -0
  195. package/get-shit-done/workflows/manager.md +362 -0
  196. package/get-shit-done/workflows/map-codebase.md +377 -0
  197. package/get-shit-done/workflows/milestone-summary.md +223 -0
  198. package/get-shit-done/workflows/new-milestone.md +486 -0
  199. package/get-shit-done/workflows/new-project.md +1250 -0
  200. package/get-shit-done/workflows/new-workspace.md +237 -0
  201. package/get-shit-done/workflows/next.md +97 -0
  202. package/get-shit-done/workflows/node-repair.md +92 -0
  203. package/get-shit-done/workflows/note.md +156 -0
  204. package/get-shit-done/workflows/pause-work.md +176 -0
  205. package/get-shit-done/workflows/plan-milestone-gaps.md +273 -0
  206. package/get-shit-done/workflows/plan-phase.md +859 -0
  207. package/get-shit-done/workflows/plant-seed.md +169 -0
  208. package/get-shit-done/workflows/pr-branch.md +129 -0
  209. package/get-shit-done/workflows/profile-user.md +450 -0
  210. package/get-shit-done/workflows/progress.md +507 -0
  211. package/get-shit-done/workflows/quick.md +757 -0
  212. package/get-shit-done/workflows/remove-phase.md +155 -0
  213. package/get-shit-done/workflows/remove-workspace.md +90 -0
  214. package/get-shit-done/workflows/research-phase.md +82 -0
  215. package/get-shit-done/workflows/resume-project.md +326 -0
  216. package/get-shit-done/workflows/review.md +228 -0
  217. package/get-shit-done/workflows/session-report.md +146 -0
  218. package/get-shit-done/workflows/settings.md +283 -0
  219. package/get-shit-done/workflows/ship.md +228 -0
  220. package/get-shit-done/workflows/stats.md +60 -0
  221. package/get-shit-done/workflows/transition.md +671 -0
  222. package/get-shit-done/workflows/ui-phase.md +302 -0
  223. package/get-shit-done/workflows/ui-review.md +165 -0
  224. package/get-shit-done/workflows/update.md +323 -0
  225. package/get-shit-done/workflows/validate-phase.md +174 -0
  226. package/get-shit-done/workflows/verify-phase.md +254 -0
  227. package/get-shit-done/workflows/verify-work.md +637 -0
  228. package/hooks/dist/gsd-check-update.js +114 -0
  229. package/hooks/dist/gsd-context-monitor.js +156 -0
  230. package/hooks/dist/gsd-prompt-guard.js +96 -0
  231. package/hooks/dist/gsd-statusline.js +119 -0
  232. package/hooks/dist/gsd-workflow-guard.js +94 -0
  233. package/package.json +52 -0
  234. package/scripts/base64-scan.sh +262 -0
  235. package/scripts/build-hooks.js +82 -0
  236. package/scripts/prompt-injection-scan.sh +198 -0
  237. package/scripts/run-tests.cjs +29 -0
  238. package/scripts/secret-scan.sh +227 -0
@@ -0,0 +1,341 @@
1
+ /**
2
+ * arc-scanner — Regex-based ARC annotation tag scanner
3
+ *
4
+ * Extracts @gsd-tags from source files anchored to comment tokens,
5
+ * preventing false positives from strings, URLs, and template literals.
6
+ *
7
+ * Requirements: SCAN-01, SCAN-02, SCAN-03, SCAN-04
8
+ */
9
+
10
+ 'use strict';
11
+
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+ const { output, error } = require('./core.cjs');
15
+
16
+ // COPY THIS REGEX VERBATIM — do not modify
17
+ // Anchors to: leading whitespace + comment token + optional space + @gsd-<type>[(metadata)] description
18
+ const TAG_LINE_RE = /^[ \t]*(?:\/\/+|\/\*+|\*+|#+|--+|"{3}|'{3})[ \t]*@gsd-(\w+)(?:\(([^)]*)\))?[ \t]*(.*?)[ \t]*$/gm;
19
+
20
+ const DEFAULT_EXCLUDES = ['node_modules', 'dist', 'build', '.planning', '.git'];
21
+
22
+ const VALID_TAG_TYPES = new Set([
23
+ 'context', 'decision', 'todo', 'constraint', 'pattern', 'ref', 'risk', 'api'
24
+ ]);
25
+
26
+ /**
27
+ * Parse the parenthesized metadata block.
28
+ * e.g. "phase:2, priority:high" → { phase: '2', priority: 'high' }
29
+ *
30
+ * @param {string|undefined} raw - Captured metadata string or undefined
31
+ * @returns {Object} Key-value pairs, all values as strings
32
+ */
33
+ function parseMetadata(raw) {
34
+ if (!raw || !raw.trim()) return {};
35
+ const result = {};
36
+ const parts = raw.split(/,\s*/);
37
+ for (const part of parts) {
38
+ const colonIdx = part.indexOf(':');
39
+ if (colonIdx === -1) continue;
40
+ const key = part.slice(0, colonIdx).trim();
41
+ const value = part.slice(colonIdx + 1).trim();
42
+ if (key) result[key] = value;
43
+ }
44
+ return result;
45
+ }
46
+
47
+ /**
48
+ * Scan a single file for @gsd-tags anchored to comment tokens.
49
+ *
50
+ * @param {string} filePath - Absolute or relative path to the file
51
+ * @returns {Array<Object>} Array of tag objects
52
+ */
53
+ function scanFile(filePath) {
54
+ let content;
55
+ try {
56
+ content = fs.readFileSync(filePath, 'utf-8');
57
+ } catch (err) {
58
+ return [];
59
+ }
60
+
61
+ // Create a fresh regex instance per call to avoid lastIndex state issues with /gm flag
62
+ const re = new RegExp(TAG_LINE_RE.source, 'gm');
63
+ const tags = [];
64
+
65
+ for (const match of content.matchAll(re)) {
66
+ const type = match[1];
67
+
68
+ // Only include known ARC tag types
69
+ if (!VALID_TAG_TYPES.has(type)) continue;
70
+
71
+ // Compute 1-based line number by counting newlines before match position
72
+ const line = (content.slice(0, match.index).match(/\n/g) || []).length + 1;
73
+
74
+ tags.push({
75
+ type,
76
+ file: filePath,
77
+ line,
78
+ metadata: parseMetadata(match[2]),
79
+ description: (match[3] || '').trim(),
80
+ raw: match[0].trim(),
81
+ });
82
+ }
83
+
84
+ return tags;
85
+ }
86
+
87
+ /**
88
+ * Recursively walk a directory, scanning all files for @gsd-tags.
89
+ * Respects DEFAULT_EXCLUDES and optional .gsdignore at dirPath root.
90
+ *
91
+ * @param {string} dirPath - Root directory to scan
92
+ * @param {Object} [options] - Optional filters
93
+ * @param {string} [options.phaseFilter] - Only return tags where metadata.phase === phaseFilter
94
+ * @param {string} [options.typeFilter] - Only return tags where type === typeFilter
95
+ * @param {string[]} [options.excludes] - Additional directory/file names to skip
96
+ * @returns {Array<Object>} All matching tags across all files
97
+ */
98
+ function scanDirectory(dirPath, options) {
99
+ options = options || {};
100
+
101
+ // Load .gsdignore patterns from project root (simple line matching)
102
+ const gsdIgnorePath = path.join(dirPath, '.gsdignore');
103
+ const gsdIgnorePatterns = [];
104
+ try {
105
+ if (fs.existsSync(gsdIgnorePath)) {
106
+ const lines = fs.readFileSync(gsdIgnorePath, 'utf-8').split('\n');
107
+ for (const line of lines) {
108
+ const trimmed = line.trim();
109
+ if (trimmed && !trimmed.startsWith('#')) {
110
+ gsdIgnorePatterns.push(trimmed);
111
+ }
112
+ }
113
+ }
114
+ } catch {
115
+ // Ignore errors reading .gsdignore
116
+ }
117
+
118
+ /**
119
+ * Internal recursive walker.
120
+ *
121
+ * @param {string} dir - Current directory being walked
122
+ * @returns {Array<Object>} Tags found under dir
123
+ */
124
+ function walk(dir) {
125
+ let entries;
126
+ try {
127
+ entries = fs.readdirSync(dir, { withFileTypes: true });
128
+ } catch {
129
+ return [];
130
+ }
131
+
132
+ const allTags = [];
133
+ for (const entry of entries) {
134
+ const name = entry.name;
135
+
136
+ // Skip DEFAULT_EXCLUDES directories
137
+ if (DEFAULT_EXCLUDES.includes(name)) continue;
138
+
139
+ // Skip user-provided excludes
140
+ if (options.excludes && options.excludes.includes(name)) continue;
141
+
142
+ // Skip .gsdignore patterns
143
+ if (gsdIgnorePatterns.some(pat => name === pat || name.startsWith(pat))) continue;
144
+
145
+ const fullPath = path.join(dir, name);
146
+
147
+ if (entry.isDirectory()) {
148
+ allTags.push(...walk(fullPath));
149
+ } else if (entry.isFile()) {
150
+ allTags.push(...scanFile(fullPath));
151
+ }
152
+ }
153
+
154
+ return allTags;
155
+ }
156
+
157
+ let tags = walk(dirPath);
158
+
159
+ // Apply phase filter
160
+ if (options.phaseFilter !== undefined && options.phaseFilter !== null) {
161
+ const phaseStr = String(options.phaseFilter);
162
+ tags = tags.filter(tag => tag.metadata.phase === phaseStr);
163
+ }
164
+
165
+ // Apply type filter
166
+ if (options.typeFilter) {
167
+ tags = tags.filter(tag => tag.type === options.typeFilter);
168
+ }
169
+
170
+ return tags;
171
+ }
172
+
173
+ /**
174
+ * Format tags as a JSON string.
175
+ *
176
+ * @param {Array<Object>} tags - Array of tag objects
177
+ * @returns {string} Pretty-printed JSON string
178
+ */
179
+ function formatAsJson(tags) {
180
+ return JSON.stringify(tags, null, 2);
181
+ }
182
+
183
+ /**
184
+ * Format tags as a CODE-INVENTORY.md Markdown document.
185
+ *
186
+ * Structure:
187
+ * - Header with metadata
188
+ * - ## Summary Statistics (table of tag type counts)
189
+ * - ## Tags by Type (H3 per type, H4 per file, table of line/metadata/description)
190
+ * - ## Phase Reference Index (table of phase/count/files)
191
+ *
192
+ * @param {Array<Object>} tags - Array of tag objects
193
+ * @param {string} [projectName] - Project name for the header
194
+ * @returns {string} CODE-INVENTORY.md Markdown string
195
+ */
196
+ function formatAsMarkdown(tags, projectName) {
197
+ const now = new Date().toISOString();
198
+ const name = projectName || 'Unknown Project';
199
+
200
+ // Gather unique files
201
+ const fileSet = new Set(tags.map(t => t.file));
202
+ const totalFiles = fileSet.size;
203
+ const totalTags = tags.length;
204
+
205
+ // Build type → tags map (preserve ARC order)
206
+ const TYPE_ORDER = ['context', 'decision', 'todo', 'constraint', 'pattern', 'ref', 'risk', 'api'];
207
+ const byType = {};
208
+ for (const type of TYPE_ORDER) {
209
+ byType[type] = [];
210
+ }
211
+ for (const tag of tags) {
212
+ if (byType[tag.type]) {
213
+ byType[tag.type].push(tag);
214
+ } else {
215
+ byType[tag.type] = [tag];
216
+ }
217
+ }
218
+
219
+ // ── Header ────────────────────────────────────────────────────────────────
220
+ const lines = [
221
+ `# CODE-INVENTORY.md`,
222
+ ``,
223
+ `**Generated:** ${now}`,
224
+ `**Project:** ${name}`,
225
+ `**Schema version:** 1.0`,
226
+ `**Tags found:** ${totalTags} across ${totalFiles} file${totalFiles !== 1 ? 's' : ''}`,
227
+ ``,
228
+ ];
229
+
230
+ // ── Summary Statistics ────────────────────────────────────────────────────
231
+ lines.push(`## Summary Statistics`, ``);
232
+ lines.push(`| Tag Type | Count |`);
233
+ lines.push(`|----------|-------|`);
234
+ for (const type of TYPE_ORDER) {
235
+ const count = byType[type] ? byType[type].length : 0;
236
+ if (count > 0) {
237
+ lines.push(`| @gsd-${type} | ${count} |`);
238
+ }
239
+ }
240
+ lines.push(``);
241
+
242
+ // ── Tags by Type ──────────────────────────────────────────────────────────
243
+ lines.push(`## Tags by Type`, ``);
244
+
245
+ for (const type of TYPE_ORDER) {
246
+ const typeTags = byType[type];
247
+ if (!typeTags || typeTags.length === 0) continue;
248
+
249
+ lines.push(`### @gsd-${type}`, ``);
250
+
251
+ // Group by file
252
+ const byFile = {};
253
+ for (const tag of typeTags) {
254
+ if (!byFile[tag.file]) byFile[tag.file] = [];
255
+ byFile[tag.file].push(tag);
256
+ }
257
+
258
+ for (const [filePath, fileTags] of Object.entries(byFile)) {
259
+ lines.push(`#### ${filePath}`, ``);
260
+ lines.push(`| Line | Metadata | Description |`);
261
+ lines.push(`|------|----------|-------------|`);
262
+ for (const tag of fileTags) {
263
+ const metaStr = Object.keys(tag.metadata).length > 0
264
+ ? Object.entries(tag.metadata).map(([k, v]) => `${k}:${v}`).join(', ')
265
+ : '—';
266
+ lines.push(`| ${tag.line} | ${metaStr} | ${tag.description || '—'} |`);
267
+ }
268
+ lines.push(``);
269
+ }
270
+ }
271
+
272
+ // ── Phase Reference Index ─────────────────────────────────────────────────
273
+ lines.push(`## Phase Reference Index`, ``);
274
+ lines.push(`| Phase | Tag Count | Files |`);
275
+ lines.push(`|-------|-----------|-------|`);
276
+
277
+ // Group by phase value
278
+ const byPhase = {};
279
+ for (const tag of tags) {
280
+ const phase = tag.metadata.phase || '(untagged)';
281
+ if (!byPhase[phase]) byPhase[phase] = { count: 0, files: new Set() };
282
+ byPhase[phase].count++;
283
+ byPhase[phase].files.add(tag.file);
284
+ }
285
+
286
+ // Sort: numbered phases first, then (untagged)
287
+ const phases = Object.keys(byPhase).sort((a, b) => {
288
+ if (a === '(untagged)') return 1;
289
+ if (b === '(untagged)') return -1;
290
+ return Number(a) - Number(b);
291
+ });
292
+
293
+ for (const phase of phases) {
294
+ const { count, files } = byPhase[phase];
295
+ lines.push(`| ${phase} | ${count} | ${[...files].join(', ')} |`);
296
+ }
297
+
298
+ lines.push(``);
299
+
300
+ return lines.join('\n');
301
+ }
302
+
303
+ /**
304
+ * CLI entry point: scan a target path, format, and write output.
305
+ * Called by gsd-tools.cjs case 'extract-tags'.
306
+ *
307
+ * @param {string} cwd - Current working directory
308
+ * @param {string} targetPath - Path to scan (file or directory)
309
+ * @param {Object} [opts] - Options
310
+ * @param {string} [opts.phaseFilter] - Phase filter
311
+ * @param {string} [opts.typeFilter] - Type filter
312
+ * @param {string} [opts.format] - 'json' (default) or 'md'
313
+ * @param {string} [opts.outputFile] - Write to file instead of stdout
314
+ */
315
+ function cmdExtractTags(cwd, targetPath, opts) {
316
+ opts = opts || {};
317
+ const format = opts.format || 'json';
318
+ const resolvedPath = path.resolve(cwd, targetPath || cwd);
319
+
320
+ const tags = scanDirectory(resolvedPath, {
321
+ phaseFilter: opts.phaseFilter,
322
+ typeFilter: opts.typeFilter,
323
+ });
324
+
325
+ let result;
326
+ if (format === 'md') {
327
+ result = formatAsMarkdown(tags, opts.projectName);
328
+ } else {
329
+ result = formatAsJson(tags);
330
+ }
331
+
332
+ if (opts.outputFile) {
333
+ const outDir = path.dirname(opts.outputFile);
334
+ fs.mkdirSync(outDir, { recursive: true });
335
+ fs.writeFileSync(opts.outputFile, result, 'utf-8');
336
+ } else {
337
+ process.stdout.write(result + '\n');
338
+ }
339
+ }
340
+
341
+ module.exports = { scanFile, scanDirectory, formatAsJson, formatAsMarkdown, cmdExtractTags };