cap-pro 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 (275) hide show
  1. package/.claude-plugin/README.md +26 -0
  2. package/.claude-plugin/marketplace.json +24 -0
  3. package/.claude-plugin/plugin.json +24 -0
  4. package/LICENSE +21 -0
  5. package/README.ja-JP.md +834 -0
  6. package/README.ko-KR.md +823 -0
  7. package/README.md +806 -0
  8. package/README.pt-BR.md +452 -0
  9. package/README.zh-CN.md +800 -0
  10. package/agents/cap-architect.md +269 -0
  11. package/agents/cap-brainstormer.md +207 -0
  12. package/agents/cap-curator.md +276 -0
  13. package/agents/cap-debugger.md +365 -0
  14. package/agents/cap-designer.md +246 -0
  15. package/agents/cap-historian.md +464 -0
  16. package/agents/cap-migrator.md +291 -0
  17. package/agents/cap-prototyper.md +197 -0
  18. package/agents/cap-validator.md +308 -0
  19. package/bin/install.js +5433 -0
  20. package/cap/bin/cap-tools.cjs +853 -0
  21. package/cap/bin/lib/arc-scanner.cjs +344 -0
  22. package/cap/bin/lib/cap-affinity-engine.cjs +862 -0
  23. package/cap/bin/lib/cap-anchor.cjs +228 -0
  24. package/cap/bin/lib/cap-annotation-writer.cjs +340 -0
  25. package/cap/bin/lib/cap-checkpoint.cjs +434 -0
  26. package/cap/bin/lib/cap-cluster-detect.cjs +945 -0
  27. package/cap/bin/lib/cap-cluster-display.cjs +52 -0
  28. package/cap/bin/lib/cap-cluster-format.cjs +245 -0
  29. package/cap/bin/lib/cap-cluster-helpers.cjs +295 -0
  30. package/cap/bin/lib/cap-cluster-io.cjs +212 -0
  31. package/cap/bin/lib/cap-completeness.cjs +540 -0
  32. package/cap/bin/lib/cap-deps.cjs +583 -0
  33. package/cap/bin/lib/cap-design-families.cjs +332 -0
  34. package/cap/bin/lib/cap-design.cjs +966 -0
  35. package/cap/bin/lib/cap-divergence-detector.cjs +400 -0
  36. package/cap/bin/lib/cap-doctor.cjs +752 -0
  37. package/cap/bin/lib/cap-feature-map-internals.cjs +19 -0
  38. package/cap/bin/lib/cap-feature-map-migrate.cjs +335 -0
  39. package/cap/bin/lib/cap-feature-map-monorepo.cjs +885 -0
  40. package/cap/bin/lib/cap-feature-map-shard.cjs +315 -0
  41. package/cap/bin/lib/cap-feature-map.cjs +1943 -0
  42. package/cap/bin/lib/cap-fitness-score.cjs +1075 -0
  43. package/cap/bin/lib/cap-impact-analysis.cjs +652 -0
  44. package/cap/bin/lib/cap-learn-review.cjs +1072 -0
  45. package/cap/bin/lib/cap-learning-signals.cjs +627 -0
  46. package/cap/bin/lib/cap-loader.cjs +227 -0
  47. package/cap/bin/lib/cap-logger.cjs +57 -0
  48. package/cap/bin/lib/cap-memory-bridge.cjs +764 -0
  49. package/cap/bin/lib/cap-memory-confidence.cjs +452 -0
  50. package/cap/bin/lib/cap-memory-dir.cjs +987 -0
  51. package/cap/bin/lib/cap-memory-engine.cjs +698 -0
  52. package/cap/bin/lib/cap-memory-extends.cjs +398 -0
  53. package/cap/bin/lib/cap-memory-graph.cjs +790 -0
  54. package/cap/bin/lib/cap-memory-migrate.cjs +2015 -0
  55. package/cap/bin/lib/cap-memory-pin.cjs +183 -0
  56. package/cap/bin/lib/cap-memory-platform.cjs +490 -0
  57. package/cap/bin/lib/cap-memory-prune.cjs +707 -0
  58. package/cap/bin/lib/cap-memory-schema.cjs +812 -0
  59. package/cap/bin/lib/cap-migrate-tags.cjs +309 -0
  60. package/cap/bin/lib/cap-migrate.cjs +540 -0
  61. package/cap/bin/lib/cap-pattern-apply.cjs +1203 -0
  62. package/cap/bin/lib/cap-pattern-pipeline.cjs +1034 -0
  63. package/cap/bin/lib/cap-plugin-manifest.cjs +80 -0
  64. package/cap/bin/lib/cap-realtime-affinity.cjs +399 -0
  65. package/cap/bin/lib/cap-reconcile.cjs +570 -0
  66. package/cap/bin/lib/cap-research-gate.cjs +218 -0
  67. package/cap/bin/lib/cap-scope-filter.cjs +402 -0
  68. package/cap/bin/lib/cap-semantic-pipeline.cjs +1038 -0
  69. package/cap/bin/lib/cap-session-extract.cjs +987 -0
  70. package/cap/bin/lib/cap-session.cjs +445 -0
  71. package/cap/bin/lib/cap-snapshot-linkage.cjs +963 -0
  72. package/cap/bin/lib/cap-stack-docs.cjs +646 -0
  73. package/cap/bin/lib/cap-tag-observer.cjs +371 -0
  74. package/cap/bin/lib/cap-tag-scanner.cjs +1766 -0
  75. package/cap/bin/lib/cap-telemetry.cjs +466 -0
  76. package/cap/bin/lib/cap-test-audit.cjs +1438 -0
  77. package/cap/bin/lib/cap-thread-migrator.cjs +307 -0
  78. package/cap/bin/lib/cap-thread-synthesis.cjs +545 -0
  79. package/cap/bin/lib/cap-thread-tracker.cjs +519 -0
  80. package/cap/bin/lib/cap-trace.cjs +399 -0
  81. package/cap/bin/lib/cap-trust-mode.cjs +336 -0
  82. package/cap/bin/lib/cap-ui-design-editor.cjs +642 -0
  83. package/cap/bin/lib/cap-ui-mind-map.cjs +712 -0
  84. package/cap/bin/lib/cap-ui-thread-nav.cjs +693 -0
  85. package/cap/bin/lib/cap-ui.cjs +1245 -0
  86. package/cap/bin/lib/cap-upgrade.cjs +1028 -0
  87. package/cap/bin/lib/cli/arg-helpers.cjs +49 -0
  88. package/cap/bin/lib/cli/frontmatter-router.cjs +31 -0
  89. package/cap/bin/lib/cli/init-router.cjs +68 -0
  90. package/cap/bin/lib/cli/phase-router.cjs +102 -0
  91. package/cap/bin/lib/cli/state-router.cjs +61 -0
  92. package/cap/bin/lib/cli/template-router.cjs +37 -0
  93. package/cap/bin/lib/cli/uat-router.cjs +29 -0
  94. package/cap/bin/lib/cli/validation-router.cjs +26 -0
  95. package/cap/bin/lib/cli/verification-router.cjs +31 -0
  96. package/cap/bin/lib/cli/workstream-router.cjs +39 -0
  97. package/cap/bin/lib/commands.cjs +961 -0
  98. package/cap/bin/lib/config.cjs +467 -0
  99. package/cap/bin/lib/convention-reader.cjs +258 -0
  100. package/cap/bin/lib/core.cjs +1241 -0
  101. package/cap/bin/lib/feature-aggregator.cjs +423 -0
  102. package/cap/bin/lib/frontmatter.cjs +337 -0
  103. package/cap/bin/lib/init.cjs +1443 -0
  104. package/cap/bin/lib/manifest-generator.cjs +383 -0
  105. package/cap/bin/lib/milestone.cjs +253 -0
  106. package/cap/bin/lib/model-profiles.cjs +69 -0
  107. package/cap/bin/lib/monorepo-context.cjs +226 -0
  108. package/cap/bin/lib/monorepo-migrator.cjs +509 -0
  109. package/cap/bin/lib/phase.cjs +889 -0
  110. package/cap/bin/lib/profile-output.cjs +989 -0
  111. package/cap/bin/lib/profile-pipeline.cjs +540 -0
  112. package/cap/bin/lib/roadmap.cjs +330 -0
  113. package/cap/bin/lib/security.cjs +394 -0
  114. package/cap/bin/lib/session-manager.cjs +292 -0
  115. package/cap/bin/lib/skeleton-generator.cjs +179 -0
  116. package/cap/bin/lib/state.cjs +1032 -0
  117. package/cap/bin/lib/template.cjs +231 -0
  118. package/cap/bin/lib/test-detector.cjs +62 -0
  119. package/cap/bin/lib/uat.cjs +283 -0
  120. package/cap/bin/lib/verify.cjs +889 -0
  121. package/cap/bin/lib/workspace-detector.cjs +371 -0
  122. package/cap/bin/lib/workstream.cjs +492 -0
  123. package/cap/commands/gsd/workstreams.md +63 -0
  124. package/cap/references/arc-standard.md +315 -0
  125. package/cap/references/cap-agent-architecture.md +101 -0
  126. package/cap/references/cap-gitignore-template +9 -0
  127. package/cap/references/cap-zero-deps.md +158 -0
  128. package/cap/references/checkpoints.md +778 -0
  129. package/cap/references/continuation-format.md +249 -0
  130. package/cap/references/contract-test-templates.md +312 -0
  131. package/cap/references/feature-map-template.md +25 -0
  132. package/cap/references/git-integration.md +295 -0
  133. package/cap/references/git-planning-commit.md +38 -0
  134. package/cap/references/model-profiles.md +174 -0
  135. package/cap/references/phase-numbering.md +126 -0
  136. package/cap/references/planning-config.md +202 -0
  137. package/cap/references/property-test-templates.md +316 -0
  138. package/cap/references/security-test-templates.md +347 -0
  139. package/cap/references/session-template.json +8 -0
  140. package/cap/references/tdd.md +263 -0
  141. package/cap/references/user-profiling.md +681 -0
  142. package/cap/references/verification-patterns.md +612 -0
  143. package/cap/templates/UAT.md +265 -0
  144. package/cap/templates/claude-md.md +175 -0
  145. package/cap/templates/codebase/architecture.md +255 -0
  146. package/cap/templates/codebase/concerns.md +310 -0
  147. package/cap/templates/codebase/conventions.md +307 -0
  148. package/cap/templates/codebase/integrations.md +280 -0
  149. package/cap/templates/codebase/stack.md +186 -0
  150. package/cap/templates/codebase/structure.md +285 -0
  151. package/cap/templates/codebase/testing.md +480 -0
  152. package/cap/templates/config.json +44 -0
  153. package/cap/templates/context.md +352 -0
  154. package/cap/templates/continue-here.md +78 -0
  155. package/cap/templates/copilot-instructions.md +7 -0
  156. package/cap/templates/debug-subagent-prompt.md +91 -0
  157. package/cap/templates/discussion-log.md +63 -0
  158. package/cap/templates/milestone-archive.md +123 -0
  159. package/cap/templates/milestone.md +115 -0
  160. package/cap/templates/phase-prompt.md +610 -0
  161. package/cap/templates/planner-subagent-prompt.md +117 -0
  162. package/cap/templates/project.md +186 -0
  163. package/cap/templates/requirements.md +231 -0
  164. package/cap/templates/research-project/ARCHITECTURE.md +204 -0
  165. package/cap/templates/research-project/FEATURES.md +147 -0
  166. package/cap/templates/research-project/PITFALLS.md +200 -0
  167. package/cap/templates/research-project/STACK.md +120 -0
  168. package/cap/templates/research-project/SUMMARY.md +170 -0
  169. package/cap/templates/research.md +552 -0
  170. package/cap/templates/roadmap.md +202 -0
  171. package/cap/templates/state.md +176 -0
  172. package/cap/templates/summary.md +364 -0
  173. package/cap/templates/user-preferences.md +498 -0
  174. package/cap/templates/verification-report.md +322 -0
  175. package/cap/workflows/add-phase.md +112 -0
  176. package/cap/workflows/add-tests.md +351 -0
  177. package/cap/workflows/add-todo.md +158 -0
  178. package/cap/workflows/audit-milestone.md +340 -0
  179. package/cap/workflows/audit-uat.md +109 -0
  180. package/cap/workflows/autonomous.md +891 -0
  181. package/cap/workflows/check-todos.md +177 -0
  182. package/cap/workflows/cleanup.md +152 -0
  183. package/cap/workflows/complete-milestone.md +767 -0
  184. package/cap/workflows/diagnose-issues.md +231 -0
  185. package/cap/workflows/discovery-phase.md +289 -0
  186. package/cap/workflows/discuss-phase-assumptions.md +653 -0
  187. package/cap/workflows/discuss-phase.md +1049 -0
  188. package/cap/workflows/do.md +104 -0
  189. package/cap/workflows/execute-phase.md +846 -0
  190. package/cap/workflows/execute-plan.md +514 -0
  191. package/cap/workflows/fast.md +105 -0
  192. package/cap/workflows/forensics.md +265 -0
  193. package/cap/workflows/health.md +181 -0
  194. package/cap/workflows/help.md +660 -0
  195. package/cap/workflows/insert-phase.md +130 -0
  196. package/cap/workflows/list-phase-assumptions.md +178 -0
  197. package/cap/workflows/list-workspaces.md +56 -0
  198. package/cap/workflows/manager.md +362 -0
  199. package/cap/workflows/map-codebase.md +377 -0
  200. package/cap/workflows/milestone-summary.md +223 -0
  201. package/cap/workflows/new-milestone.md +486 -0
  202. package/cap/workflows/new-project.md +1250 -0
  203. package/cap/workflows/new-workspace.md +237 -0
  204. package/cap/workflows/next.md +97 -0
  205. package/cap/workflows/node-repair.md +92 -0
  206. package/cap/workflows/note.md +156 -0
  207. package/cap/workflows/pause-work.md +176 -0
  208. package/cap/workflows/plan-milestone-gaps.md +273 -0
  209. package/cap/workflows/plan-phase.md +857 -0
  210. package/cap/workflows/plant-seed.md +169 -0
  211. package/cap/workflows/pr-branch.md +129 -0
  212. package/cap/workflows/profile-user.md +449 -0
  213. package/cap/workflows/progress.md +507 -0
  214. package/cap/workflows/quick.md +757 -0
  215. package/cap/workflows/remove-phase.md +155 -0
  216. package/cap/workflows/remove-workspace.md +90 -0
  217. package/cap/workflows/research-phase.md +82 -0
  218. package/cap/workflows/resume-project.md +326 -0
  219. package/cap/workflows/review.md +228 -0
  220. package/cap/workflows/session-report.md +146 -0
  221. package/cap/workflows/settings.md +283 -0
  222. package/cap/workflows/ship.md +228 -0
  223. package/cap/workflows/stats.md +60 -0
  224. package/cap/workflows/transition.md +671 -0
  225. package/cap/workflows/ui-phase.md +298 -0
  226. package/cap/workflows/ui-review.md +161 -0
  227. package/cap/workflows/update.md +323 -0
  228. package/cap/workflows/validate-phase.md +170 -0
  229. package/cap/workflows/verify-phase.md +254 -0
  230. package/cap/workflows/verify-work.md +637 -0
  231. package/commands/cap/annotate.md +165 -0
  232. package/commands/cap/brainstorm.md +393 -0
  233. package/commands/cap/checkpoint.md +106 -0
  234. package/commands/cap/completeness.md +94 -0
  235. package/commands/cap/continue.md +72 -0
  236. package/commands/cap/debug.md +588 -0
  237. package/commands/cap/deps.md +169 -0
  238. package/commands/cap/design.md +479 -0
  239. package/commands/cap/init.md +354 -0
  240. package/commands/cap/iterate.md +249 -0
  241. package/commands/cap/learn.md +459 -0
  242. package/commands/cap/memory.md +275 -0
  243. package/commands/cap/migrate-feature-map.md +91 -0
  244. package/commands/cap/migrate-memory.md +108 -0
  245. package/commands/cap/migrate-tags.md +91 -0
  246. package/commands/cap/migrate.md +131 -0
  247. package/commands/cap/prototype.md +510 -0
  248. package/commands/cap/reconcile.md +121 -0
  249. package/commands/cap/review.md +360 -0
  250. package/commands/cap/save.md +72 -0
  251. package/commands/cap/scan.md +404 -0
  252. package/commands/cap/start.md +356 -0
  253. package/commands/cap/status.md +118 -0
  254. package/commands/cap/test-audit.md +262 -0
  255. package/commands/cap/test.md +394 -0
  256. package/commands/cap/trace.md +133 -0
  257. package/commands/cap/ui.md +167 -0
  258. package/hooks/dist/cap-check-update.js +115 -0
  259. package/hooks/dist/cap-context-monitor.js +185 -0
  260. package/hooks/dist/cap-learn-review-hook.js +114 -0
  261. package/hooks/dist/cap-learning-hook.js +192 -0
  262. package/hooks/dist/cap-memory.js +299 -0
  263. package/hooks/dist/cap-prompt-guard.js +97 -0
  264. package/hooks/dist/cap-statusline.js +157 -0
  265. package/hooks/dist/cap-tag-observer.js +115 -0
  266. package/hooks/dist/cap-version-check.js +112 -0
  267. package/hooks/dist/cap-workflow-guard.js +175 -0
  268. package/hooks/hooks.json +55 -0
  269. package/package.json +58 -0
  270. package/scripts/base64-scan.sh +262 -0
  271. package/scripts/build-hooks.js +93 -0
  272. package/scripts/cap-removal-checklist.md +202 -0
  273. package/scripts/prompt-injection-scan.sh +199 -0
  274. package/scripts/run-tests.cjs +181 -0
  275. package/scripts/secret-scan.sh +227 -0
@@ -0,0 +1,583 @@
1
+ 'use strict';
2
+
3
+ // @cap-feature(feature:F-049, primary:true) Automatic dependency inference from source imports.
4
+ // Scans tagged files for require/import statements, resolves them to feature IDs via the tag scanner,
5
+ // then diffs inferred against declared DEPENDS_ON in FEATURE-MAP.md.
6
+ //
7
+ // @cap-decision Regex-based import detection keeps the module zero-dep per CAP constraints.
8
+ // AST parsers (e.g. acorn, @babel/parser) would handle dynamic/conditional imports better but add
9
+ // a runtime dependency. Dynamic imports and computed requires are explicitly documented as limitations
10
+ // (F-049 AC-4).
11
+ // @cap-decision Pure logic by default — all functions take data in and return data out.
12
+ // The only I/O boundaries are `loadDepsConfig()` (reads .cap/config.json) and `applyInferredDeps()`
13
+ // (writes FEATURE-MAP.md). Every other function is testable without touching disk.
14
+
15
+ const fs = require('node:fs');
16
+ const path = require('node:path');
17
+
18
+ const CONFIG_FILE = path.join('.cap', 'config.json');
19
+
20
+ // @cap-todo(ac:F-049/AC-4) CJS `require('...')` — static string argument only (no template literals / vars)
21
+ const CJS_REQUIRE_RE = /\brequire\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
22
+
23
+ // @cap-todo(ac:F-049/AC-4) ESM `import ... from '...'` (default, named, namespace, and side-effect imports)
24
+ // @cap-risk The `^\s*` anchor guards the common case but does not strip block comments that span
25
+ // multiple lines. A `/* import x from './y' */` block inside multi-line JSDoc above real imports
26
+ // could yield a false positive. Acceptable for a zero-dep regex parser; document limitation.
27
+ const ESM_IMPORT_RE = /^\s*import\s+(?:[^'"]*?\s+from\s+)?['"]([^'"]+)['"]/gm;
28
+
29
+ // @cap-todo(ac:F-049/AC-4) ESM re-exports: `export ... from '...'`
30
+ // @cap-risk Same block-comment false-positive surface as ESM_IMPORT_RE above.
31
+ const ESM_REEXPORT_RE = /^\s*export\s+(?:[^'"]*?\s+from\s+)?['"]([^'"]+)['"]/gm;
32
+
33
+ // @cap-todo(ac:F-049/AC-4) Dynamic imports: `import('...')` — static string arg only
34
+ const DYNAMIC_IMPORT_RE = /\bimport\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
35
+
36
+ // TS import types: `import type Foo from '...'` — captured by ESM_IMPORT_RE already.
37
+ // TS triple-slash references (/// <reference path="..." />) are doc-level, ignored on purpose.
38
+
39
+ const IMPORT_EXTENSIONS = ['.js', '.cjs', '.mjs', '.ts', '.tsx', '.jsx'];
40
+
41
+ /**
42
+ * @typedef {Object} ImportSpec
43
+ * @property {string} source - The raw import path as written (e.g. './foo', '../bar/baz.cjs')
44
+ * @property {('cjs'|'esm'|'reexport'|'dynamic')} kind
45
+ */
46
+
47
+ /**
48
+ * @typedef {Object} DepsConfig
49
+ * @property {boolean} enabled - Master switch for F-049 behaviour
50
+ * @property {boolean} autoFix - Whether /cap:deps may write without --confirm
51
+ */
52
+
53
+ const DEFAULT_CONFIG = {
54
+ enabled: false,
55
+ autoFix: false,
56
+ };
57
+
58
+ /**
59
+ * Extract all static import/require specifiers from a source file's content.
60
+ * Duplicates (same source path via different kinds) are preserved so callers can
61
+ * see whether something is imported both via CJS and ESM in the same file.
62
+ *
63
+ * @param {string} content - Source file content
64
+ * @returns {ImportSpec[]}
65
+ */
66
+ function parseImports(content) {
67
+ if (typeof content !== 'string' || content.length === 0) return [];
68
+ const out = [];
69
+ const sources = [
70
+ [CJS_REQUIRE_RE, 'cjs'],
71
+ [ESM_IMPORT_RE, 'esm'],
72
+ [ESM_REEXPORT_RE, 'reexport'],
73
+ [DYNAMIC_IMPORT_RE, 'dynamic'],
74
+ ];
75
+ for (const [re, kind] of sources) {
76
+ re.lastIndex = 0;
77
+ let m;
78
+ while ((m = re.exec(content)) !== null) {
79
+ out.push({ source: m[1], kind });
80
+ }
81
+ }
82
+ return out;
83
+ }
84
+
85
+ /**
86
+ * Resolve an import specifier to an absolute file path, Node-style.
87
+ * Only relative/absolute imports are resolved — bare specifiers (e.g. 'node:fs',
88
+ * 'react') return null because they map to node_modules/core, which cannot carry
89
+ * CAP feature tags by definition.
90
+ *
91
+ * @param {string} importSource - The string from parseImports().source
92
+ * @param {string} fromFile - Absolute path to the file containing the import
93
+ * @returns {string|null} Absolute resolved path, or null when unresolvable
94
+ */
95
+ function resolveImportToFile(importSource, fromFile) {
96
+ if (!importSource || typeof importSource !== 'string') return null;
97
+
98
+ // Bare specifier — package name or Node builtin. Not a CAP module.
99
+ if (!importSource.startsWith('.') && !path.isAbsolute(importSource)) return null;
100
+
101
+ const baseDir = path.dirname(fromFile);
102
+ const absolute = path.isAbsolute(importSource)
103
+ ? importSource
104
+ : path.resolve(baseDir, importSource);
105
+
106
+ // Try exact path first
107
+ if (fileExistsSync(absolute)) return absolute;
108
+
109
+ // Try extension suffixes
110
+ for (const ext of IMPORT_EXTENSIONS) {
111
+ const candidate = absolute + ext;
112
+ if (fileExistsSync(candidate)) return candidate;
113
+ }
114
+
115
+ // Try directory index resolution: ./foo -> ./foo/index.{js,cjs,mjs,ts}
116
+ if (dirExistsSync(absolute)) {
117
+ for (const ext of IMPORT_EXTENSIONS) {
118
+ const candidate = path.join(absolute, 'index' + ext);
119
+ if (fileExistsSync(candidate)) return candidate;
120
+ }
121
+ }
122
+
123
+ return null;
124
+ }
125
+
126
+ function fileExistsSync(p) {
127
+ try {
128
+ return fs.statSync(p).isFile();
129
+ } catch (_e) {
130
+ return false;
131
+ }
132
+ }
133
+
134
+ function dirExistsSync(p) {
135
+ try {
136
+ return fs.statSync(p).isDirectory();
137
+ } catch (_e) {
138
+ return false;
139
+ }
140
+ }
141
+
142
+ /**
143
+ * Build a map from absolute file path -> feature ID, derived from tag scanner output.
144
+ * A file with multiple @cap-feature tags is assigned its first listed feature.
145
+ * Mirror files under .claude/ are treated as aliases of their counterparts under cap/
146
+ * (both point to the same feature).
147
+ *
148
+ * @cap-todo(ac:F-049/AC-1) First-wins ownership is convenient but drops secondary feature
149
+ * ownership silently. A future `@cap-feature(secondary:true)` convention could let a file
150
+ * belong to multiple features without ambiguity; revisit this resolver at that time.
151
+ *
152
+ * @param {CapTag[]} tags - Output of scanner.scanDirectory()
153
+ * @param {string} projectRoot - Absolute path to project root (for normalising tag.file)
154
+ * @returns {Map<string, string>} absolute file path -> feature ID
155
+ */
156
+ function buildFileToFeatureMap(tags, projectRoot) {
157
+ const m = new Map();
158
+ if (!Array.isArray(tags)) return m;
159
+ for (const tag of tags) {
160
+ if (tag.type !== 'feature') continue;
161
+ const featureId = tag.metadata && tag.metadata.feature;
162
+ if (!featureId) continue;
163
+ const abs = path.isAbsolute(tag.file)
164
+ ? tag.file
165
+ : path.resolve(projectRoot, tag.file);
166
+ if (!m.has(abs)) m.set(abs, featureId);
167
+ }
168
+ return m;
169
+ }
170
+
171
+ /**
172
+ * @typedef {Object} InferredDeps
173
+ * @property {Object<string, string[]>} byFeature - featureId -> [featureId] (dependency targets)
174
+ * @property {Object<string, ImportEvidence[]>} evidence - featureId -> evidence rows explaining each inferred dep
175
+ */
176
+
177
+ /**
178
+ * @typedef {Object} ImportEvidence
179
+ * @property {string} fromFile - File that made the import (relative to projectRoot)
180
+ * @property {string} importSource - Raw import path as written
181
+ * @property {string} resolvedFile - Absolute resolved path
182
+ * @property {string} targetFeature - Feature ID the resolved file is tagged with
183
+ * @property {('cjs'|'esm'|'reexport'|'dynamic')} kind
184
+ */
185
+
186
+ /**
187
+ * Infer feature-level dependencies from source imports.
188
+ * For every tagged source file, parse its imports, resolve them, and map each
189
+ * resolved target to a feature ID. If the source file's feature imports from a
190
+ * file owned by another feature, that is an inferred dependency.
191
+ *
192
+ * Self-imports (same feature) are filtered out.
193
+ *
194
+ * @param {CapTag[]} tags - Output of scanner.scanDirectory()
195
+ * @param {string} projectRoot - Absolute path to project root
196
+ * @param {{ readFile?: (p: string) => string, resolveImport?: (source: string, fromFile: string) => string|null }} [opts] - Optional hooks for testing
197
+ * @returns {InferredDeps}
198
+ */
199
+ function inferFeatureDeps(tags, projectRoot, opts) {
200
+ const readFile = (opts && opts.readFile) || ((p) => fs.readFileSync(p, 'utf8'));
201
+ const resolve = (opts && opts.resolveImport) || resolveImportToFile;
202
+ const fileToFeature = buildFileToFeatureMap(tags, projectRoot);
203
+
204
+ const byFeature = {};
205
+ const evidence = {};
206
+
207
+ for (const [absFile, featureId] of fileToFeature.entries()) {
208
+ let content;
209
+ try {
210
+ content = readFile(absFile);
211
+ } catch (_e) {
212
+ continue; // file unreadable — no evidence for this path
213
+ }
214
+ const imports = parseImports(content);
215
+ for (const imp of imports) {
216
+ const resolved = resolve(imp.source, absFile);
217
+ if (!resolved) continue;
218
+ const targetFeature = fileToFeature.get(resolved);
219
+ if (!targetFeature) continue;
220
+ if (targetFeature === featureId) continue; // self-import, not a dep
221
+
222
+ if (!byFeature[featureId]) byFeature[featureId] = [];
223
+ if (!byFeature[featureId].includes(targetFeature)) {
224
+ byFeature[featureId].push(targetFeature);
225
+ }
226
+
227
+ if (!evidence[featureId]) evidence[featureId] = [];
228
+ evidence[featureId].push({
229
+ fromFile: path.relative(projectRoot, absFile),
230
+ importSource: imp.source,
231
+ resolvedFile: resolved,
232
+ targetFeature,
233
+ kind: imp.kind,
234
+ });
235
+ }
236
+ }
237
+
238
+ // Stable order: sort dependency lists alphabetically to keep diff output deterministic
239
+ for (const f of Object.keys(byFeature)) byFeature[f].sort();
240
+
241
+ return { byFeature, evidence };
242
+ }
243
+
244
+ /**
245
+ * @typedef {Object} DepDiffRow
246
+ * @property {string} feature - Feature ID under review
247
+ * @property {string[]} declared - Deps declared in FEATURE-MAP.md
248
+ * @property {string[]} inferred - Deps inferred from imports
249
+ * @property {string[]} missing - In `inferred` but not in `declared` (should be added)
250
+ * @property {string[]} extraneous - In `declared` but not in `inferred` (candidates for removal)
251
+ */
252
+
253
+ // @cap-todo(ac:F-049/AC-2) Diff declared vs inferred dependencies per feature.
254
+ /**
255
+ * Compare the FEATURE-MAP.md declared dependencies against the inferred set.
256
+ * Returns one row per feature that appears in either source; features with
257
+ * perfectly matching sets are included with empty missing/extraneous arrays so
258
+ * callers can decide whether to filter.
259
+ *
260
+ * @param {FeatureMap} featureMap - Output of cap-feature-map.readFeatureMap()
261
+ * @param {InferredDeps} inferred
262
+ * @returns {DepDiffRow[]}
263
+ */
264
+ function diffDeclaredVsInferred(featureMap, inferred) {
265
+ const rows = [];
266
+ const features = (featureMap && featureMap.features) || [];
267
+ const inferredByFeature = inferred && inferred.byFeature ? inferred.byFeature : {};
268
+
269
+ const seen = new Set();
270
+ for (const f of features) {
271
+ const declared = Array.isArray(f.dependencies) ? [...f.dependencies].sort() : [];
272
+ const inferredList = inferredByFeature[f.id] ? [...inferredByFeature[f.id]].sort() : [];
273
+ const missing = inferredList.filter((d) => !declared.includes(d));
274
+ const extraneous = declared.filter((d) => !inferredList.includes(d));
275
+ rows.push({
276
+ feature: f.id,
277
+ declared,
278
+ inferred: inferredList,
279
+ missing,
280
+ extraneous,
281
+ });
282
+ seen.add(f.id);
283
+ }
284
+
285
+ // Features that exist in inferred but not in the FEATURE-MAP at all —
286
+ // possible when scanning picks up unregistered feature IDs from tags.
287
+ for (const fid of Object.keys(inferredByFeature)) {
288
+ if (seen.has(fid)) continue;
289
+ rows.push({
290
+ feature: fid,
291
+ declared: [],
292
+ inferred: [...inferredByFeature[fid]].sort(),
293
+ missing: [...inferredByFeature[fid]].sort(),
294
+ extraneous: [],
295
+ });
296
+ }
297
+
298
+ rows.sort((a, b) => a.feature.localeCompare(b.feature));
299
+ return rows;
300
+ }
301
+
302
+ /**
303
+ * Produce a human-readable summary for /cap:deps without --graph.
304
+ * @param {DepDiffRow[]} diffRows
305
+ * @returns {string}
306
+ */
307
+ function formatDiffReport(diffRows) {
308
+ const changed = diffRows.filter((r) => r.missing.length > 0 || r.extraneous.length > 0);
309
+ if (changed.length === 0) {
310
+ return 'Dependency graph is consistent — no changes inferred.';
311
+ }
312
+ const lines = [];
313
+ lines.push(`Dependency drift detected in ${changed.length} feature(s):`);
314
+ lines.push('');
315
+ for (const row of changed) {
316
+ lines.push(`${row.feature}`);
317
+ lines.push(` declared: ${row.declared.length ? row.declared.join(', ') : '(none)'}`);
318
+ lines.push(` inferred: ${row.inferred.length ? row.inferred.join(', ') : '(none)'}`);
319
+ if (row.missing.length) lines.push(` + add: ${row.missing.join(', ')}`);
320
+ if (row.extraneous.length) lines.push(` - remove?: ${row.extraneous.join(', ')}`);
321
+ lines.push('');
322
+ }
323
+ return lines.join('\n').trimEnd();
324
+ }
325
+
326
+ // @cap-todo(ac:F-049/AC-5) Render a Mermaid flowchart of feature -> feature edges.
327
+ /**
328
+ * Render a Mermaid flowchart of feature dependencies. Nodes are labelled
329
+ * with the feature ID + first 30 chars of title (when available).
330
+ *
331
+ * @param {FeatureMap} featureMap
332
+ * @param {InferredDeps} inferred
333
+ * @param {{ source?: 'inferred'|'declared'|'union' }} [opts] - Which edge set to render; default 'union'
334
+ * @returns {string} Mermaid source (starting with ```mermaid fence)
335
+ */
336
+ function renderMermaidGraph(featureMap, inferred, opts) {
337
+ const source = (opts && opts.source) || 'union';
338
+ const features = (featureMap && featureMap.features) || [];
339
+ const inferredByFeature = inferred && inferred.byFeature ? inferred.byFeature : {};
340
+
341
+ const lines = ['```mermaid', 'flowchart TD'];
342
+
343
+ // Nodes
344
+ const idToTitle = {};
345
+ for (const f of features) {
346
+ idToTitle[f.id] = f.title || f.id;
347
+ const label = truncate(`${f.id}: ${idToTitle[f.id]}`, 40);
348
+ lines.push(` ${nodeId(f.id)}["${escapeLabel(label)}"]`);
349
+ }
350
+
351
+ // Edges
352
+ const seen = new Set();
353
+ const pushEdge = (from, to, kind) => {
354
+ const key = `${from}->${to}:${kind}`;
355
+ if (seen.has(key)) return;
356
+ seen.add(key);
357
+ const arrow = kind === 'inferred-only' ? '-.->|inferred|' : '-->';
358
+ lines.push(` ${nodeId(from)} ${arrow} ${nodeId(to)}`);
359
+ };
360
+
361
+ for (const f of features) {
362
+ const declared = Array.isArray(f.dependencies) ? f.dependencies : [];
363
+ const inferredList = inferredByFeature[f.id] || [];
364
+ if (source === 'declared' || source === 'union') {
365
+ for (const d of declared) pushEdge(f.id, d, 'declared');
366
+ }
367
+ if (source === 'inferred' || source === 'union') {
368
+ for (const d of inferredList) {
369
+ const isOnlyInferred = !declared.includes(d);
370
+ pushEdge(f.id, d, isOnlyInferred ? 'inferred-only' : 'declared');
371
+ }
372
+ }
373
+ }
374
+
375
+ lines.push('```');
376
+ return lines.join('\n');
377
+ }
378
+
379
+ function nodeId(featureId) {
380
+ return featureId.replace(/[^a-zA-Z0-9]/g, '_');
381
+ }
382
+
383
+ function escapeLabel(s) {
384
+ return String(s).replace(/"/g, '\\"');
385
+ }
386
+
387
+ function truncate(s, n) {
388
+ if (s.length <= n) return s;
389
+ return s.slice(0, Math.max(0, n - 1)) + '…';
390
+ }
391
+
392
+ // @cap-todo(ac:F-049/AC-6) Load F-049 config from .cap/config.json with safe defaults.
393
+ /**
394
+ * Load F-049 config from .cap/config.json. Returns defaults if file missing or
395
+ * autoDepsInference section absent.
396
+ * @param {string} cwd
397
+ * @returns {DepsConfig}
398
+ */
399
+ function loadDepsConfig(cwd) {
400
+ const configPath = path.join(cwd, CONFIG_FILE);
401
+ let cfg = { ...DEFAULT_CONFIG };
402
+ try {
403
+ const raw = fs.readFileSync(configPath, 'utf8');
404
+ const parsed = JSON.parse(raw);
405
+ const section = parsed && parsed.autoDepsInference;
406
+ if (section && typeof section === 'object') {
407
+ if (typeof section.enabled === 'boolean') cfg.enabled = section.enabled;
408
+ if (typeof section.autoFix === 'boolean') cfg.autoFix = section.autoFix;
409
+ }
410
+ } catch (_e) {
411
+ // No config or malformed — use defaults
412
+ }
413
+ return cfg;
414
+ }
415
+
416
+ // @cap-todo(ac:F-049/AC-3) Apply inferred deps to FEATURE-MAP.md — requires confirmation callback.
417
+ /**
418
+ * Write inferred dependencies back to FEATURE-MAP.md by replacing each feature's
419
+ * `**Depends on:**` line (or inserting one directly under the feature header if
420
+ * none exists). Extraneous declared deps are removed when removeExtraneous is true.
421
+ *
422
+ * @param {string} cwd - Project root
423
+ * @param {DepDiffRow[]} diffRows - Output of diffDeclaredVsInferred
424
+ * @param {{ removeExtraneous?: boolean, featureMapPath?: string }} [opts]
425
+ * @returns {{ updated: string[], unchanged: string[] }}
426
+ */
427
+ function applyInferredDeps(cwd, diffRows, opts) {
428
+ const options = opts || {};
429
+ const removeExtraneous = options.removeExtraneous === true;
430
+ const featureMapPath = options.featureMapPath || path.join(cwd, 'FEATURE-MAP.md');
431
+
432
+ const original = fs.readFileSync(featureMapPath, 'utf8');
433
+ let updated = original;
434
+ const changedFeatures = [];
435
+ const unchangedFeatures = [];
436
+
437
+ for (const row of diffRows) {
438
+ const shouldWrite = row.missing.length > 0 || (removeExtraneous && row.extraneous.length > 0);
439
+ if (!shouldWrite) {
440
+ unchangedFeatures.push(row.feature);
441
+ continue;
442
+ }
443
+
444
+ const mergedSet = new Set([...row.declared, ...row.missing]);
445
+ if (removeExtraneous) {
446
+ for (const ext of row.extraneous) mergedSet.delete(ext);
447
+ }
448
+ const merged = [...mergedSet].sort();
449
+ const newLine = merged.length > 0 ? `**Depends on:** ${merged.join(', ')}` : '';
450
+
451
+ const next = rewriteDependsOnLine(updated, row.feature, newLine);
452
+ if (next === updated) {
453
+ unchangedFeatures.push(row.feature);
454
+ } else {
455
+ updated = next;
456
+ changedFeatures.push(row.feature);
457
+ }
458
+ }
459
+
460
+ if (updated !== original) fs.writeFileSync(featureMapPath, updated, 'utf8');
461
+ return { updated: changedFeatures, unchanged: unchangedFeatures };
462
+ }
463
+
464
+ /**
465
+ * Rewrite the `**Depends on:**` line within a single feature block, inserting
466
+ * one directly below the feature header if it does not already exist. Pure
467
+ * string manipulation so it is unit-testable without touching disk.
468
+ *
469
+ * @param {string} contents - Full FEATURE-MAP.md markdown
470
+ * @param {string} featureId - e.g. 'F-049'
471
+ * @param {string} newLine - Full line content (without trailing newline), or '' to remove
472
+ * @returns {string} Updated markdown (unchanged if featureId not found)
473
+ */
474
+ function rewriteDependsOnLine(contents, featureId, newLine) {
475
+ const lines = contents.split('\n');
476
+ // Feature header: `### F-049: Title [state]`
477
+ const headerRE = new RegExp(`^###\\s+${escapeRe(featureId)}(?::|\\s|$)`);
478
+ const nextHeaderRE = /^###\s+F-\d{3}/;
479
+ const dependsRE = /^-?\s*\*\*Depend(?:s on|encies):\*\*/;
480
+
481
+ let start = -1;
482
+ for (let i = 0; i < lines.length; i++) {
483
+ if (headerRE.test(lines[i])) { start = i; break; }
484
+ }
485
+ if (start === -1) return contents;
486
+
487
+ let end = lines.length;
488
+ for (let i = start + 1; i < lines.length; i++) {
489
+ if (nextHeaderRE.test(lines[i])) { end = i; break; }
490
+ }
491
+
492
+ // Try to find an existing Depends on line inside the block
493
+ let depIdx = -1;
494
+ for (let i = start + 1; i < end; i++) {
495
+ if (dependsRE.test(lines[i])) { depIdx = i; break; }
496
+ }
497
+
498
+ if (depIdx !== -1) {
499
+ if (newLine === '') {
500
+ // Remove the line (and a possibly blank line immediately after)
501
+ const removeCount = (lines[depIdx + 1] === '') ? 2 : 1;
502
+ lines.splice(depIdx, removeCount);
503
+ } else {
504
+ lines[depIdx] = newLine;
505
+ }
506
+ } else if (newLine !== '') {
507
+ // Insert directly after the blank line that follows the feature header.
508
+ // Pattern: header \n "" \n <insert here> — do not add a second blank line.
509
+ if (lines[start + 1] === '') {
510
+ lines.splice(start + 2, 0, newLine, '');
511
+ } else {
512
+ lines.splice(start + 1, 0, '', newLine, '');
513
+ }
514
+ }
515
+
516
+ return lines.join('\n');
517
+ }
518
+
519
+ function escapeRe(s) {
520
+ return String(s).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
521
+ }
522
+
523
+ // @cap-feature(feature:F-063) Design-ID impact analysis — `/cap:deps --design DT-NNN`.
524
+ // @cap-todo(ac:F-063/AC-6) Given a DT-NNN or DC-NNN, list every feature that references it via usesDesign.
525
+ // @cap-decision Reuses the Feature Map as the single source of truth rather than re-scanning tags —
526
+ // /cap:scan is the canonical path that populates usesDesign, so dependency inference stays cheap.
527
+ /**
528
+ * @param {{features: Array<{id:string,title?:string,usesDesign?:string[]}>}} featureMap
529
+ * @param {string} designId - e.g. "DT-001" or "DC-001"
530
+ * @returns {Array<{id:string,title:string|null}>} features that reference the ID, in Feature-ID order
531
+ */
532
+ function findFeaturesUsingDesignId(featureMap, designId) {
533
+ const out = [];
534
+ if (!featureMap || !Array.isArray(featureMap.features) || !designId) return out;
535
+ if (!/^(DT-\d{3,}|DC-\d{3,})$/.test(designId)) return out;
536
+ for (const f of featureMap.features) {
537
+ const uses = Array.isArray(f.usesDesign) ? f.usesDesign : [];
538
+ if (uses.includes(designId)) out.push({ id: f.id, title: f.title || null });
539
+ }
540
+ out.sort((a, b) => a.id.localeCompare(b.id));
541
+ return out;
542
+ }
543
+
544
+ // @cap-api formatDesignImpactReport(designId, featuresUsing) -- Human-readable /cap:deps --design output.
545
+ /**
546
+ * @param {string} designId
547
+ * @param {Array<{id:string,title:string|null}>} featuresUsing
548
+ * @returns {string}
549
+ */
550
+ function formatDesignImpactReport(designId, featuresUsing) {
551
+ if (!featuresUsing || featuresUsing.length === 0) {
552
+ return `No features reference ${designId}.`;
553
+ }
554
+ const lines = [];
555
+ lines.push(`Features referencing ${designId}: ${featuresUsing.length}`);
556
+ lines.push('');
557
+ for (const f of featuresUsing) {
558
+ const title = f.title ? ` — ${f.title}` : '';
559
+ lines.push(` ${f.id}${title}`);
560
+ }
561
+ return lines.join('\n');
562
+ }
563
+
564
+ module.exports = {
565
+ // pure helpers (exported for tests)
566
+ parseImports,
567
+ resolveImportToFile,
568
+ buildFileToFeatureMap,
569
+ rewriteDependsOnLine,
570
+ // high-level API
571
+ inferFeatureDeps,
572
+ diffDeclaredVsInferred,
573
+ formatDiffReport,
574
+ renderMermaidGraph,
575
+ loadDepsConfig,
576
+ applyInferredDeps,
577
+ // F-063 design-ID impact analysis
578
+ findFeaturesUsingDesignId,
579
+ formatDesignImpactReport,
580
+ // constants (for tests / consumers)
581
+ DEFAULT_CONFIG,
582
+ IMPORT_EXTENSIONS,
583
+ };