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,598 @@
1
+ // @gsd-context CAP v2.0 stack docs manager -- wraps Context7 CLI for library documentation fetch and caching in .cap/stack-docs/.
2
+ // @gsd-decision Wraps npx ctx7@latest (not a direct API call) -- Context7 is already the user's standard tool per CLAUDE.md. This module provides programmatic access for agent workflows.
3
+ // @gsd-decision Docs cached as markdown files in .cap/stack-docs/{library-name}.md -- simple, readable, committable for offline use.
4
+ // @gsd-constraint Zero external dependencies at runtime -- Context7 is invoked via child_process.execSync (npx), not imported.
5
+ // @gsd-risk Context7 requires network access and may hit rate limits. Module must handle failures gracefully and report to caller.
6
+
7
+ 'use strict';
8
+
9
+ const fs = require('node:fs');
10
+ const path = require('node:path');
11
+ const { execSync } = require('node:child_process');
12
+
13
+ const STACK_DOCS_DIR = '.cap/stack-docs';
14
+
15
+ // @gsd-todo(ref:AC-27) Tag scanner uses stack docs path for enrichment context
16
+ const FRESHNESS_DAYS = 7;
17
+ const FRESHNESS_HOURS = FRESHNESS_DAYS * 24; // 168 hours default freshness window
18
+
19
+ /**
20
+ * @typedef {Object} LibraryInfo
21
+ * @property {string} id - Context7 library ID (e.g., "/vercel/next.js")
22
+ * @property {string} name - Display name
23
+ * @property {string} description - Library description
24
+ */
25
+
26
+ /**
27
+ * @typedef {Object} FetchResult
28
+ * @property {boolean} success - Whether the fetch succeeded
29
+ * @property {string|null} filePath - Path to cached docs file (null on failure)
30
+ * @property {string|null} error - Error message on failure
31
+ */
32
+
33
+ /**
34
+ * @typedef {Object} DependencyInfo
35
+ * @property {string[]} dependencies - Production dependency names
36
+ * @property {string[]} devDependencies - Dev dependency names
37
+ * @property {string} type - Project type: 'node', 'python', 'go', 'rust', 'unknown'
38
+ */
39
+
40
+ // @gsd-api detectDependencies(projectRoot) -- Reads package.json/requirements.txt/etc to discover project dependencies.
41
+ // Returns: DependencyInfo with categorized dependency lists and project type.
42
+ /**
43
+ * @param {string} projectRoot - Absolute path to project root
44
+ * @returns {DependencyInfo}
45
+ */
46
+ function detectDependencies(projectRoot) {
47
+ const result = { dependencies: [], devDependencies: [], type: 'unknown' };
48
+
49
+ // Node.js: package.json
50
+ const pkgPath = path.join(projectRoot, 'package.json');
51
+ if (fs.existsSync(pkgPath)) {
52
+ try {
53
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
54
+ result.type = 'node';
55
+ if (pkg.dependencies) result.dependencies = Object.keys(pkg.dependencies);
56
+ if (pkg.devDependencies) result.devDependencies = Object.keys(pkg.devDependencies);
57
+ } catch (_e) {
58
+ // Malformed package.json -- continue to other detectors
59
+ }
60
+ }
61
+
62
+ // Python: requirements.txt
63
+ const reqPath = path.join(projectRoot, 'requirements.txt');
64
+ if (fs.existsSync(reqPath) && result.type === 'unknown') {
65
+ try {
66
+ const content = fs.readFileSync(reqPath, 'utf8');
67
+ const depRE = /^([a-zA-Z0-9_-]+)/gm;
68
+ let match;
69
+ result.type = 'python';
70
+ while ((match = depRE.exec(content)) !== null) {
71
+ result.dependencies.push(match[1]);
72
+ }
73
+ } catch (_e) {
74
+ // Ignore
75
+ }
76
+ }
77
+
78
+ // Python: pyproject.toml (basic extraction)
79
+ const pyprojectPath = path.join(projectRoot, 'pyproject.toml');
80
+ if (fs.existsSync(pyprojectPath) && result.type === 'unknown') {
81
+ try {
82
+ const content = fs.readFileSync(pyprojectPath, 'utf8');
83
+ result.type = 'python';
84
+ // Extract dependency names from [project.dependencies] or [tool.poetry.dependencies]
85
+ const depRE = /^\s*"?([a-zA-Z0-9_-]+)/gm;
86
+ const depsSection = content.match(/\[(?:project\.)?dependencies\]([\s\S]*?)(?:\[|$)/);
87
+ if (depsSection) {
88
+ let m;
89
+ while ((m = depRE.exec(depsSection[1])) !== null) {
90
+ if (m[1] !== 'python') result.dependencies.push(m[1]);
91
+ }
92
+ }
93
+ } catch (_e) {
94
+ // Ignore
95
+ }
96
+ }
97
+
98
+ // Go: go.mod
99
+ const goModPath = path.join(projectRoot, 'go.mod');
100
+ if (fs.existsSync(goModPath) && result.type === 'unknown') {
101
+ try {
102
+ const content = fs.readFileSync(goModPath, 'utf8');
103
+ result.type = 'go';
104
+ const requireRE = /^\s+([^\s]+)/gm;
105
+ const requireBlock = content.match(/require\s*\(([\s\S]*?)\)/);
106
+ if (requireBlock) {
107
+ let m;
108
+ while ((m = requireRE.exec(requireBlock[1])) !== null) {
109
+ result.dependencies.push(m[1]);
110
+ }
111
+ }
112
+ } catch (_e) {
113
+ // Ignore
114
+ }
115
+ }
116
+
117
+ // Rust: Cargo.toml
118
+ const cargoPath = path.join(projectRoot, 'Cargo.toml');
119
+ if (fs.existsSync(cargoPath) && result.type === 'unknown') {
120
+ try {
121
+ const content = fs.readFileSync(cargoPath, 'utf8');
122
+ result.type = 'rust';
123
+ const depSection = content.match(/\[dependencies\]([\s\S]*?)(?:\[|$)/);
124
+ if (depSection) {
125
+ const depRE = /^([a-zA-Z0-9_-]+)/gm;
126
+ let m;
127
+ while ((m = depRE.exec(depSection[1])) !== null) {
128
+ result.dependencies.push(m[1]);
129
+ }
130
+ }
131
+ } catch (_e) {
132
+ // Ignore
133
+ }
134
+ }
135
+
136
+ return result;
137
+ }
138
+
139
+ // @gsd-api resolveLibrary(libraryName, query) -- Resolves a library name to a Context7 library ID.
140
+ // Returns: LibraryInfo or null if not found.
141
+ /**
142
+ * @param {string} libraryName - Library name (e.g., "react", "express")
143
+ * @param {string} [query] - Optional query for better matching
144
+ * @returns {LibraryInfo|null}
145
+ */
146
+ function resolveLibrary(libraryName, query) {
147
+ const queryStr = query ? `"${query}"` : `"${libraryName}"`;
148
+ try {
149
+ const output = execSync(
150
+ `npx ctx7@latest library ${libraryName} ${queryStr}`,
151
+ { encoding: 'utf8', timeout: 30000, stdio: ['pipe', 'pipe', 'pipe'] }
152
+ );
153
+
154
+ // Parse the first result from ctx7 library output
155
+ // Expected format: lines with ID, name, description
156
+ const lines = output.trim().split('\n').filter(l => l.trim());
157
+ if (lines.length === 0) return null;
158
+
159
+ // ctx7 outputs a table or JSON-like structure -- extract the first match
160
+ // Look for a line containing a library ID in /org/project format
161
+ for (const line of lines) {
162
+ const idMatch = line.match(/\/[a-zA-Z0-9_-]+\/[a-zA-Z0-9._-]+/);
163
+ if (idMatch) {
164
+ return {
165
+ id: idMatch[0],
166
+ name: libraryName,
167
+ description: line.replace(idMatch[0], '').trim(),
168
+ };
169
+ }
170
+ }
171
+
172
+ return null;
173
+ } catch (e) {
174
+ // ctx7 not available, network error, or timeout
175
+ return null;
176
+ }
177
+ }
178
+
179
+ // @gsd-api fetchDocs(projectRoot, libraryId, query) -- Fetches library docs via Context7 and caches them.
180
+ // Returns: FetchResult with success status and cached file path.
181
+ /**
182
+ * @param {string} projectRoot - Absolute path to project root
183
+ * @param {string} libraryId - Context7 library ID (e.g., "/vercel/next.js")
184
+ * @param {string} [query] - Optional query to focus documentation
185
+ * @returns {FetchResult}
186
+ */
187
+ function fetchDocs(projectRoot, libraryId, query) {
188
+ const docsDir = path.join(projectRoot, STACK_DOCS_DIR);
189
+ // Ensure .cap/stack-docs/ exists
190
+ fs.mkdirSync(docsDir, { recursive: true });
191
+
192
+ // Derive filename from library ID: /vercel/next.js -> next.js.md
193
+ const libName = libraryId.split('/').pop() || libraryId.replace(/\//g, '-');
194
+ const filePath = path.join(docsDir, `${libName}.md`);
195
+
196
+ const queryStr = query ? `"${query}"` : '""';
197
+ try {
198
+ const output = execSync(
199
+ `npx ctx7@latest docs ${libraryId} ${queryStr}`,
200
+ { encoding: 'utf8', timeout: 60000, stdio: ['pipe', 'pipe', 'pipe'] }
201
+ );
202
+
203
+ if (!output || output.trim().length === 0) {
204
+ return { success: false, filePath: null, error: 'Empty response from Context7' };
205
+ }
206
+
207
+ // Write docs with metadata header
208
+ const header = [
209
+ `<!-- CAP Stack Docs: ${libraryId} -->`,
210
+ `<!-- Fetched: ${new Date().toISOString()} -->`,
211
+ `<!-- Query: ${query || 'general'} -->`,
212
+ '',
213
+ ].join('\n');
214
+
215
+ fs.writeFileSync(filePath, header + output, 'utf8');
216
+ return { success: true, filePath, error: null };
217
+ } catch (e) {
218
+ const errorMsg = e.message || 'Unknown error fetching docs';
219
+ return { success: false, filePath: null, error: errorMsg };
220
+ }
221
+ }
222
+
223
+ // @gsd-api writeDocs(projectRoot, libraryName, content) -- Writes documentation content directly to .cap/stack-docs/.
224
+ // Returns: string -- path to written file.
225
+ /**
226
+ * @param {string} projectRoot - Absolute path to project root
227
+ * @param {string} libraryName - Library name for filename
228
+ * @param {string} content - Documentation content to write
229
+ * @returns {string}
230
+ */
231
+ function writeDocs(projectRoot, libraryName, content) {
232
+ const docsDir = path.join(projectRoot, STACK_DOCS_DIR);
233
+ fs.mkdirSync(docsDir, { recursive: true });
234
+
235
+ const filePath = path.join(docsDir, `${libraryName}.md`);
236
+ const header = [
237
+ `<!-- CAP Stack Docs: ${libraryName} -->`,
238
+ `<!-- Written: ${new Date().toISOString()} -->`,
239
+ '',
240
+ ].join('\n');
241
+
242
+ fs.writeFileSync(filePath, header + content, 'utf8');
243
+ return filePath;
244
+ }
245
+
246
+ // @gsd-api listCachedDocs(projectRoot) -- Lists all cached library docs.
247
+ // Returns: Array of { libraryName, filePath, lastModified }.
248
+ /**
249
+ * @param {string} projectRoot - Absolute path to project root
250
+ * @returns {Array<{libraryName: string, filePath: string, lastModified: Date}>}
251
+ */
252
+ function listCachedDocs(projectRoot) {
253
+ const docsDir = path.join(projectRoot, STACK_DOCS_DIR);
254
+ if (!fs.existsSync(docsDir)) return [];
255
+
256
+ try {
257
+ const files = fs.readdirSync(docsDir).filter(f => f.endsWith('.md'));
258
+ return files.map(f => {
259
+ const filePath = path.join(docsDir, f);
260
+ const stat = fs.statSync(filePath);
261
+ return {
262
+ libraryName: f.replace(/\.md$/, ''),
263
+ filePath,
264
+ lastModified: stat.mtime,
265
+ };
266
+ });
267
+ } catch (_e) {
268
+ return [];
269
+ }
270
+ }
271
+
272
+ // @gsd-api checkFreshness(projectRoot, libraryName, maxAgeHours) -- Checks if cached docs are still fresh.
273
+ // Returns: { fresh: boolean, ageHours: number | null, filePath: string | null }
274
+ /**
275
+ * @param {string} projectRoot - Absolute path to project root
276
+ * @param {string} libraryName - Library name
277
+ * @param {number} [maxAgeHours] - Maximum age in hours (default: 168 = 7 days)
278
+ * @returns {{ fresh: boolean, ageHours: number|null, filePath: string|null }}
279
+ */
280
+ function checkFreshness(projectRoot, libraryName, maxAgeHours) {
281
+ const maxAge = maxAgeHours != null ? maxAgeHours : FRESHNESS_HOURS;
282
+ const filePath = getDocsPath(projectRoot, libraryName);
283
+
284
+ if (!fs.existsSync(filePath)) {
285
+ return { fresh: false, ageHours: null, filePath: null };
286
+ }
287
+
288
+ try {
289
+ const stat = fs.statSync(filePath);
290
+ const ageMs = Math.max(0, Date.now() - stat.mtime.getTime());
291
+ const ageHours = Math.floor(ageMs / (1000 * 60 * 60));
292
+ return {
293
+ fresh: ageHours <= maxAge,
294
+ ageHours,
295
+ filePath,
296
+ };
297
+ } catch (_e) {
298
+ return { fresh: false, ageHours: null, filePath: null };
299
+ }
300
+ }
301
+
302
+ // @gsd-api getDocsPath(projectRoot, libraryName) -- Returns the expected path for a library's cached docs.
303
+ /**
304
+ * @param {string} projectRoot - Absolute path to project root
305
+ * @param {string} libraryName - Library name
306
+ * @returns {string}
307
+ */
308
+ function getDocsPath(projectRoot, libraryName) {
309
+ return path.join(projectRoot, STACK_DOCS_DIR, `${libraryName}.md`);
310
+ }
311
+
312
+ // @gsd-api parseFreshnessFromContent(content) -- Extracts freshness date from doc file header comment.
313
+ // @gsd-todo(ref:AC-84) Stack-docs carry freshness marker (fetch date). Docs older than 7 days auto-refreshed.
314
+ /**
315
+ * Parse the fetch date from a stack doc file's header.
316
+ * Looks for: <!-- Fetched: ISO_DATE --> or <!-- Written: ISO_DATE -->
317
+ *
318
+ * @param {string} content - File content
319
+ * @returns {string|null} - ISO date string or null if not found
320
+ */
321
+ function parseFreshnessFromContent(content) {
322
+ const match = content.match(/<!--\s*(?:Fetched|Written):\s*(\d{4}-\d{2}-\d{2}T[^\s>]+)\s*-->/);
323
+ return match ? match[1] : null;
324
+ }
325
+
326
+ // @gsd-api checkFreshnessEnhanced(projectRoot, libraryName, maxAgeDays) -- Checks freshness using embedded date marker.
327
+ /**
328
+ * @param {string} projectRoot - Absolute path to project root
329
+ * @param {string} libraryName - Library name
330
+ * @param {number} [maxAgeDays] - Maximum age in days (default: 7)
331
+ * @returns {{ fresh: boolean, ageHours: number|null, fetchDate: string|null, filePath: string|null }}
332
+ */
333
+ function checkFreshnessEnhanced(projectRoot, libraryName, maxAgeDays) {
334
+ const maxDays = maxAgeDays != null ? maxAgeDays : FRESHNESS_DAYS;
335
+ const filePath = path.join(projectRoot, STACK_DOCS_DIR, `${libraryName}.md`);
336
+
337
+ if (!fs.existsSync(filePath)) {
338
+ return { fresh: false, ageHours: null, fetchDate: null, filePath: null };
339
+ }
340
+
341
+ try {
342
+ const content = fs.readFileSync(filePath, 'utf8');
343
+ const fetchDate = parseFreshnessFromContent(content);
344
+
345
+ if (!fetchDate) {
346
+ // No freshness marker -- treat as stale, use file mtime as fallback
347
+ const stat = fs.statSync(filePath);
348
+ const ageMs = Math.max(0, Date.now() - stat.mtime.getTime());
349
+ const ageHours = Math.floor(ageMs / (1000 * 60 * 60));
350
+ return {
351
+ fresh: ageHours <= maxDays * 24,
352
+ ageHours,
353
+ fetchDate: null,
354
+ filePath,
355
+ };
356
+ }
357
+
358
+ const fetchTime = new Date(fetchDate).getTime();
359
+ const ageMs = Math.max(0, Date.now() - fetchTime);
360
+ const ageHours = Math.floor(ageMs / (1000 * 60 * 60));
361
+
362
+ return {
363
+ fresh: ageHours <= maxDays * 24,
364
+ ageHours,
365
+ fetchDate,
366
+ filePath,
367
+ };
368
+ } catch (_e) {
369
+ return { fresh: false, ageHours: null, fetchDate: null, filePath: null };
370
+ }
371
+ }
372
+
373
+ // @gsd-api fetchDocsWithFreshness(projectRoot, libraryId, query) -- Fetches docs with embedded freshness marker.
374
+ // @gsd-todo(ref:AC-82) Store fetched stack docs in .cap/stack-docs/{library-name}.md
375
+ /**
376
+ * @param {string} projectRoot - Absolute path to project root
377
+ * @param {string} libraryId - Context7 library ID (e.g., "/vercel/next.js")
378
+ * @param {string} [query] - Optional query to focus documentation
379
+ * @returns {{ success: boolean, filePath: string|null, error: string|null }}
380
+ */
381
+ function fetchDocsWithFreshness(projectRoot, libraryId, query) {
382
+ const docsDir = path.join(projectRoot, STACK_DOCS_DIR);
383
+ fs.mkdirSync(docsDir, { recursive: true });
384
+
385
+ const libName = libraryId.split('/').pop() || libraryId.replace(/\//g, '-');
386
+ const filePath = path.join(docsDir, `${libName}.md`);
387
+
388
+ const queryStr = query ? `"${query}"` : '""';
389
+ try {
390
+ const output = execSync(
391
+ `npx ctx7@latest docs ${libraryId} ${queryStr}`,
392
+ { encoding: 'utf8', timeout: 60000, stdio: ['pipe', 'pipe', 'pipe'] }
393
+ );
394
+
395
+ if (!output || output.trim().length === 0) {
396
+ return { success: false, filePath: null, error: 'Empty response from Context7' };
397
+ }
398
+
399
+ // Write docs with freshness metadata header
400
+ const now = new Date().toISOString();
401
+ const header = [
402
+ `<!-- CAP Stack Docs: ${libraryId} -->`,
403
+ `<!-- Fetched: ${now} -->`,
404
+ `<!-- Query: ${query || 'general'} -->`,
405
+ `<!-- Freshness: valid until ${new Date(Date.now() + FRESHNESS_DAYS * 24 * 60 * 60 * 1000).toISOString()} -->`,
406
+ '',
407
+ ].join('\n');
408
+
409
+ fs.writeFileSync(filePath, header + output, 'utf8');
410
+ return { success: true, filePath, error: null };
411
+ } catch (e) {
412
+ return { success: false, filePath: null, error: e.message || 'Unknown error' };
413
+ }
414
+ }
415
+
416
+ // @gsd-api batchFetchDocs(projectRoot, dependencies, options) -- Orchestrates batch fetch for /cap:init.
417
+ // @gsd-todo(ref:AC-85) Context7 fetching is MANDATORY at init. If unreachable, warning emitted and init continues.
418
+ /**
419
+ * Fetch stack docs for multiple dependencies. Skips already-fresh docs.
420
+ *
421
+ * @param {string} projectRoot - Absolute path to project root
422
+ * @param {string[]} dependencies - Array of dependency names to fetch
423
+ * @param {Object} [options]
424
+ * @param {number} [options.maxDeps] - Maximum number of deps to fetch (default: 15)
425
+ * @param {boolean} [options.force] - Force refresh even if fresh (default: false)
426
+ * @returns {{ total: number, fetched: number, failed: number, skipped: number, context7Available: boolean, errors: string[] }}
427
+ */
428
+ function batchFetchDocs(projectRoot, dependencies, options = {}) {
429
+ const maxDeps = options.maxDeps || 15;
430
+ const force = options.force || false;
431
+
432
+ // Filter out internal/scoped packages that Context7 likely does not have
433
+ const fetchable = dependencies
434
+ .filter(dep => !dep.startsWith('@') || dep.startsWith('@angular/') || dep.startsWith('@nestjs/'))
435
+ .slice(0, maxDeps);
436
+
437
+ const result = {
438
+ total: fetchable.length,
439
+ fetched: 0,
440
+ failed: 0,
441
+ skipped: 0,
442
+ context7Available: false,
443
+ errors: [],
444
+ };
445
+
446
+ for (const dep of fetchable) {
447
+ // Check freshness first (skip if already fresh and not forced)
448
+ if (!force) {
449
+ const freshness = checkFreshnessEnhanced(projectRoot, dep);
450
+ if (freshness.fresh) {
451
+ result.skipped++;
452
+ continue;
453
+ }
454
+ }
455
+
456
+ // Resolve library in Context7
457
+ // @gsd-risk Context7 resolution may fail for less popular libraries. Graceful skip per dep.
458
+ try {
459
+ const lib = resolveLibrary(dep, 'API surface and configuration');
460
+ if (!lib) {
461
+ result.failed++;
462
+ result.errors.push(`${dep}: not found in Context7`);
463
+ continue;
464
+ }
465
+
466
+ const fetchResult = fetchDocsWithFreshness(
467
+ projectRoot,
468
+ lib.id,
469
+ 'API surface, configuration, breaking changes'
470
+ );
471
+
472
+ if (fetchResult.success) {
473
+ result.fetched++;
474
+ result.context7Available = true;
475
+ } else {
476
+ result.failed++;
477
+ result.errors.push(`${dep}: ${fetchResult.error}`);
478
+ }
479
+ } catch (e) {
480
+ result.failed++;
481
+ result.errors.push(`${dep}: ${e.message}`);
482
+ }
483
+ }
484
+
485
+ return result;
486
+ }
487
+
488
+ // @gsd-api getStaleLibraries(projectRoot) -- Returns list of libraries with stale (>7 day) docs.
489
+ /**
490
+ * @param {string} projectRoot - Absolute path to project root
491
+ * @returns {Array<{libraryName: string, ageHours: number, fetchDate: string|null}>}
492
+ */
493
+ function getStaleLibraries(projectRoot) {
494
+ const docsDir = path.join(projectRoot, STACK_DOCS_DIR);
495
+ if (!fs.existsSync(docsDir)) return [];
496
+
497
+ const stale = [];
498
+ try {
499
+ const files = fs.readdirSync(docsDir).filter(f => f.endsWith('.md'));
500
+ for (const f of files) {
501
+ const libName = f.replace(/\.md$/, '');
502
+ const freshness = checkFreshnessEnhanced(projectRoot, libName);
503
+ if (!freshness.fresh) {
504
+ stale.push({
505
+ libraryName: libName,
506
+ ageHours: freshness.ageHours,
507
+ fetchDate: freshness.fetchDate,
508
+ });
509
+ }
510
+ }
511
+ } catch (_e) {
512
+ // Ignore
513
+ }
514
+
515
+ return stale;
516
+ }
517
+
518
+ // @gsd-api detectWorkspacePackages(projectRoot) -- Detects monorepo workspace packages for cross-package scanning.
519
+ /**
520
+ * @param {string} projectRoot - Absolute path to project root
521
+ * @returns {{ isMonorepo: boolean, packages: string[] }}
522
+ */
523
+ function detectWorkspacePackages(projectRoot) {
524
+ const result = { isMonorepo: false, packages: [] };
525
+
526
+ const pkgPath = path.join(projectRoot, 'package.json');
527
+ if (fs.existsSync(pkgPath)) {
528
+ try {
529
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
530
+ if (pkg.workspaces) {
531
+ result.isMonorepo = true;
532
+ const wsPatterns = Array.isArray(pkg.workspaces)
533
+ ? pkg.workspaces
534
+ : (pkg.workspaces.packages || []);
535
+
536
+ for (const pattern of wsPatterns) {
537
+ const baseDir = pattern.replace(/\/\*.*$/, '');
538
+ const fullDir = path.join(projectRoot, baseDir);
539
+ if (fs.existsSync(fullDir) && fs.statSync(fullDir).isDirectory()) {
540
+ const entries = fs.readdirSync(fullDir, { withFileTypes: true });
541
+ for (const entry of entries) {
542
+ if (entry.isDirectory()) {
543
+ result.packages.push(path.join(baseDir, entry.name));
544
+ }
545
+ }
546
+ }
547
+ }
548
+ }
549
+ } catch (_e) {
550
+ // Ignore
551
+ }
552
+ }
553
+
554
+ // Check lerna.json
555
+ const lernaPath = path.join(projectRoot, 'lerna.json');
556
+ if (!result.isMonorepo && fs.existsSync(lernaPath)) {
557
+ try {
558
+ const lerna = JSON.parse(fs.readFileSync(lernaPath, 'utf8'));
559
+ result.isMonorepo = true;
560
+ const patterns = lerna.packages || ['packages/*'];
561
+ for (const pattern of patterns) {
562
+ const baseDir = pattern.replace(/\/\*.*$/, '');
563
+ const fullDir = path.join(projectRoot, baseDir);
564
+ if (fs.existsSync(fullDir) && fs.statSync(fullDir).isDirectory()) {
565
+ const entries = fs.readdirSync(fullDir, { withFileTypes: true });
566
+ for (const entry of entries) {
567
+ if (entry.isDirectory()) {
568
+ result.packages.push(path.join(baseDir, entry.name));
569
+ }
570
+ }
571
+ }
572
+ }
573
+ } catch (_e) {
574
+ // Ignore
575
+ }
576
+ }
577
+
578
+ return result;
579
+ }
580
+
581
+ module.exports = {
582
+ STACK_DOCS_DIR,
583
+ FRESHNESS_DAYS,
584
+ FRESHNESS_HOURS,
585
+ detectDependencies,
586
+ resolveLibrary,
587
+ fetchDocs,
588
+ writeDocs,
589
+ listCachedDocs,
590
+ checkFreshness,
591
+ getDocsPath,
592
+ parseFreshnessFromContent,
593
+ checkFreshnessEnhanced,
594
+ fetchDocsWithFreshness,
595
+ batchFetchDocs,
596
+ getStaleLibraries,
597
+ detectWorkspacePackages,
598
+ };