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,540 @@
1
+ 'use strict';
2
+
3
+ // @cap-feature(feature:F-048, primary:true) Implementation Completeness Score — 4-point per-AC audit.
4
+ // Computes four independent signals per acceptance criterion:
5
+ // (a) tag exists in code — @cap-todo/@cap-feature tag references this AC
6
+ // (b) test exists referencing it — tagged test file (tests/** or *.test.*) also references this AC
7
+ // (c) test invokes tagged code — test's static imports reach the primary implementation file
8
+ // (d) reachable from public — primary file is reachable via imports from bin/install.js or hooks/
9
+ //
10
+ // Each signal is 0 or 1. The sum is the completeness score (0..4). A feature's
11
+ // average is the mean of its AC scores.
12
+ //
13
+ // @cap-decision Pure computation. scoreAc() takes a pre-computed context and returns
14
+ // a structured result. The only I/O surface is buildContext() (scans the project once)
15
+ // and loadCompletenessConfig() (reads .cap/config.json). Performance-critical code paths
16
+ // reuse the context across all ACs — expected wall-clock <5s for 100 features.
17
+ // @cap-decision Reachability uses static CJS/ESM imports only. Dynamic requires, runtime
18
+ // plugin loading, and command-markdown references are NOT followed — these are documented
19
+ // limitations consistent with F-049's constraints.
20
+
21
+ const fs = require('node:fs');
22
+ const path = require('node:path');
23
+ const deps = require('./cap-deps.cjs');
24
+
25
+ const CONFIG_FILE = path.join('.cap', 'config.json');
26
+
27
+ const DEFAULT_CONFIG = {
28
+ enabled: false,
29
+ shipThreshold: 3.5,
30
+ };
31
+
32
+ const TEST_FILE_PATTERNS = [
33
+ /\.test\.[cm]?js$/i,
34
+ /\.test\.tsx?$/i,
35
+ /\.spec\.[cm]?js$/i,
36
+ /\.spec\.tsx?$/i,
37
+ /^tests?\//, // path starts with tests/ or test/
38
+ /\/tests?\//,
39
+ /^__tests__\//, // Jest convention
40
+ /\/__tests__\//,
41
+ ];
42
+
43
+ /**
44
+ * @typedef {Object} CompletenessConfig
45
+ * @property {boolean} enabled
46
+ * @property {number} shipThreshold
47
+ */
48
+
49
+ /**
50
+ * @typedef {Object} CompletenessContext
51
+ * @property {Object} featureMap - Output of readFeatureMap()
52
+ * @property {Array} tags - Output of scanner.scanDirectory()
53
+ * @property {Object} acFileMap - Output of scanner.buildAcFileMap()
54
+ * @property {Map<string,string>} fileToFeature - from cap-deps
55
+ * @property {Set<string>} publicReachable - absolute paths reachable from public surface
56
+ * @property {Map<string,Array>} importsByFile - absolute path -> ImportSpec[] (cached)
57
+ * @property {string} projectRoot
58
+ */
59
+
60
+ /**
61
+ * @typedef {Object} AcScore
62
+ * @property {string} acRef - 'F-XXX/AC-N'
63
+ * @property {Object} signals
64
+ * @property {boolean} signals.tag
65
+ * @property {boolean} signals.test
66
+ * @property {boolean} signals.testInvokesCode
67
+ * @property {boolean} signals.reachable
68
+ * @property {number} score - 0..4
69
+ * @property {string[]} reasons - short strings explaining each signal's outcome
70
+ */
71
+
72
+ /**
73
+ * @typedef {Object} FeatureScore
74
+ * @property {string} featureId
75
+ * @property {string} state - lifecycle state from FEATURE-MAP
76
+ * @property {AcScore[]} acs
77
+ * @property {number} averageScore - arithmetic mean, 0..4, or NaN when feature has no ACs
78
+ * @property {number} acCount
79
+ * @property {number} scoreSum
80
+ */
81
+
82
+ /**
83
+ * Check whether a file path looks like a test file.
84
+ * @param {string} filePath - Relative or absolute
85
+ * @returns {boolean}
86
+ */
87
+ function isTestFile(filePath) {
88
+ if (!filePath || typeof filePath !== 'string') return false;
89
+ const normalized = filePath.replace(/\\/g, '/');
90
+ return TEST_FILE_PATTERNS.some((re) => re.test(normalized));
91
+ }
92
+
93
+ /**
94
+ * Score a single AC against a pre-computed context.
95
+ * @param {string} acRef - 'F-XXX/AC-N'
96
+ * @param {CompletenessContext} ctx
97
+ * @returns {AcScore}
98
+ */
99
+ function scoreAc(acRef, ctx) {
100
+ const entry = ctx.acFileMap[acRef];
101
+ const files = (entry && entry.files) || [];
102
+ const primary = (entry && entry.primary) || null;
103
+
104
+ const reasons = [];
105
+
106
+ // -------- Signal (a): tag exists in (non-test) code --------
107
+ const codeFiles = files.filter((f) => !isTestFile(f));
108
+ const tagSignal = codeFiles.length > 0;
109
+ reasons.push(
110
+ tagSignal
111
+ ? `tag: ${codeFiles.length} file(s) tagged`
112
+ : 'tag: no @cap-* tag references this AC in source files'
113
+ );
114
+
115
+ // -------- Signal (b): test exists referencing the AC --------
116
+ const testFiles = files.filter((f) => isTestFile(f));
117
+ const testSignal = testFiles.length > 0;
118
+ reasons.push(
119
+ testSignal
120
+ ? `test: ${testFiles.length} test file(s) tag this AC`
121
+ : 'test: no test file has a @cap-* tag referencing this AC'
122
+ );
123
+
124
+ // -------- Signal (c): test invokes the tagged code --------
125
+ let testInvokesCode = false;
126
+ if (testSignal && primary) {
127
+ const primaryAbs = path.isAbsolute(primary)
128
+ ? primary
129
+ : path.resolve(ctx.projectRoot, primary);
130
+ for (const tf of testFiles) {
131
+ const testAbs = path.isAbsolute(tf) ? tf : path.resolve(ctx.projectRoot, tf);
132
+ if (testReachesFile(testAbs, primaryAbs, ctx, /* maxDepth */ 3)) {
133
+ testInvokesCode = true;
134
+ break;
135
+ }
136
+ }
137
+ }
138
+ reasons.push(
139
+ testInvokesCode
140
+ ? 'invokes: at least one test imports the primary file (static graph)'
141
+ : testSignal
142
+ ? 'invokes: test does not import the primary file within 3 hops'
143
+ : 'invokes: skipped (no test present)'
144
+ );
145
+
146
+ // -------- Signal (d): tagged code reachable from public surface --------
147
+ let reachable = false;
148
+ if (primary) {
149
+ const primaryAbs = path.isAbsolute(primary)
150
+ ? primary
151
+ : path.resolve(ctx.projectRoot, primary);
152
+ reachable = ctx.publicReachable.has(primaryAbs);
153
+ }
154
+ reasons.push(
155
+ reachable
156
+ ? 'reachable: primary file is imported from public surface (bin/install.js, hooks/)'
157
+ : 'reachable: primary file not reachable from bin/install.js or hooks/'
158
+ );
159
+
160
+ const score =
161
+ (tagSignal ? 1 : 0) +
162
+ (testSignal ? 1 : 0) +
163
+ (testInvokesCode ? 1 : 0) +
164
+ (reachable ? 1 : 0);
165
+
166
+ return {
167
+ acRef,
168
+ signals: {
169
+ tag: tagSignal,
170
+ test: testSignal,
171
+ testInvokesCode,
172
+ reachable,
173
+ },
174
+ score,
175
+ reasons,
176
+ };
177
+ }
178
+
179
+ /**
180
+ * BFS over static imports from `startFile` looking for `targetFile`.
181
+ * Caches import lists per-file via ctx.importsByFile.
182
+ *
183
+ * @param {string} startFile - absolute
184
+ * @param {string} targetFile - absolute
185
+ * @param {CompletenessContext} ctx
186
+ * @param {number} maxDepth
187
+ * @returns {boolean}
188
+ */
189
+ function testReachesFile(startFile, targetFile, ctx, maxDepth) {
190
+ if (startFile === targetFile) return true;
191
+ const queue = [{ file: startFile, depth: 0 }];
192
+ const seen = new Set([startFile]);
193
+ while (queue.length > 0) {
194
+ const { file, depth } = queue.shift();
195
+ if (depth >= maxDepth) continue;
196
+ let imports = ctx.importsByFile.get(file);
197
+ if (imports === undefined) {
198
+ try {
199
+ const content = fs.readFileSync(file, 'utf8');
200
+ imports = deps.parseImports(content);
201
+ } catch (_e) {
202
+ imports = [];
203
+ }
204
+ ctx.importsByFile.set(file, imports);
205
+ }
206
+ for (const imp of imports) {
207
+ const resolved = deps.resolveImportToFile(imp.source, file);
208
+ if (!resolved) continue;
209
+ if (resolved === targetFile) return true;
210
+ if (!seen.has(resolved)) {
211
+ seen.add(resolved);
212
+ queue.push({ file: resolved, depth: depth + 1 });
213
+ }
214
+ }
215
+ }
216
+ return false;
217
+ }
218
+
219
+ /**
220
+ * Compute reachability set from public surface files (bin/install.js + hooks/*.js)
221
+ * outward via static imports. Returns absolute paths of all reachable files.
222
+ *
223
+ * @param {string} projectRoot
224
+ * @returns {Set<string>}
225
+ */
226
+ function computePublicReachable(projectRoot) {
227
+ const roots = collectPublicSurfaceFiles(projectRoot);
228
+ const reachable = new Set();
229
+ const queue = [];
230
+ for (const r of roots) {
231
+ reachable.add(r);
232
+ queue.push(r);
233
+ }
234
+ while (queue.length > 0) {
235
+ const file = queue.shift();
236
+ let content;
237
+ try {
238
+ content = fs.readFileSync(file, 'utf8');
239
+ } catch (_e) {
240
+ continue;
241
+ }
242
+ const imports = deps.parseImports(content);
243
+ for (const imp of imports) {
244
+ const resolved = deps.resolveImportToFile(imp.source, file);
245
+ if (!resolved) continue;
246
+ if (!reachable.has(resolved)) {
247
+ reachable.add(resolved);
248
+ queue.push(resolved);
249
+ }
250
+ }
251
+ }
252
+ return reachable;
253
+ }
254
+
255
+ /**
256
+ * Collect the public-surface entry points. Conservative set: package.json "bin"
257
+ * entries plus any *.js files under hooks/. Returns absolute paths.
258
+ * @param {string} projectRoot
259
+ * @returns {string[]}
260
+ */
261
+ function collectPublicSurfaceFiles(projectRoot) {
262
+ const entries = [];
263
+ // Defense-in-depth: resolved paths must stay inside projectRoot. A malicious
264
+ // package.json with bin: "../../evil.js" already owns the process (require
265
+ // would execute it anyway), but we don't want completeness reachability to
266
+ // follow pointers outside the project.
267
+ const rootPrefix = path.resolve(projectRoot) + path.sep;
268
+ const pushIfInRoot = (p) => {
269
+ const abs = path.resolve(projectRoot, p);
270
+ if (abs.startsWith(rootPrefix)) entries.push(abs);
271
+ };
272
+
273
+ // package.json bin entries
274
+ try {
275
+ const pkg = JSON.parse(fs.readFileSync(path.join(projectRoot, 'package.json'), 'utf8'));
276
+ const bin = pkg.bin;
277
+ if (typeof bin === 'string') {
278
+ pushIfInRoot(bin);
279
+ } else if (bin && typeof bin === 'object') {
280
+ for (const v of Object.values(bin)) pushIfInRoot(v);
281
+ }
282
+ } catch (_e) { /* no package.json */ }
283
+
284
+ // hooks/*.js
285
+ const hooksDir = path.join(projectRoot, 'hooks');
286
+ try {
287
+ const files = fs.readdirSync(hooksDir);
288
+ for (const f of files) {
289
+ if (f.endsWith('.js') || f.endsWith('.cjs') || f.endsWith('.mjs')) {
290
+ const full = path.join(hooksDir, f);
291
+ const st = fs.statSync(full);
292
+ if (st.isFile()) entries.push(full);
293
+ }
294
+ }
295
+ } catch (_e) { /* no hooks dir */ }
296
+
297
+ return entries;
298
+ }
299
+
300
+ /**
301
+ * Build the CompletenessContext used by all scoring functions.
302
+ * @param {string} projectRoot
303
+ * @param {{ scanner?: any, featureMap?: any }} [injected] - Optional pre-computed inputs (for tests)
304
+ * @returns {CompletenessContext}
305
+ */
306
+ function buildContext(projectRoot, injected) {
307
+ const scanner = (injected && injected.scanner) || require('./cap-tag-scanner.cjs');
308
+ const fm = (injected && injected.featureMapModule) || require('./cap-feature-map.cjs');
309
+ const deps = require('./cap-deps.cjs');
310
+
311
+ // @cap-todo(ac:F-081/AC-4 iter:2) Migrated to {safe: true} opt-in to preserve CLI on duplicate-ID FEATURE-MAP.
312
+ // @cap-decision(F-081/iter2) Warn on parseError; continue with partial map for read-only display.
313
+ let featureMap = (injected && injected.featureMap) || fm.readFeatureMap(projectRoot, undefined, { safe: true });
314
+ if (featureMap && featureMap.parseError) {
315
+ console.warn('cap: completeness — duplicate feature ID detected, scoring uses partial map: ' + String(featureMap.parseError.message).trim());
316
+ }
317
+ const tags = (injected && injected.tags) || scanner.scanDirectory(projectRoot);
318
+ const acFileMap = scanner.buildAcFileMap(tags);
319
+ const fileToFeature = deps.buildFileToFeatureMap(tags, projectRoot);
320
+ const publicReachable =
321
+ (injected && injected.publicReachable) || computePublicReachable(projectRoot);
322
+
323
+ return {
324
+ featureMap,
325
+ tags,
326
+ acFileMap,
327
+ fileToFeature,
328
+ publicReachable,
329
+ importsByFile: new Map(),
330
+ projectRoot,
331
+ };
332
+ }
333
+
334
+ /**
335
+ * Score every AC in every feature of the Feature Map.
336
+ * @param {CompletenessContext} ctx
337
+ * @returns {FeatureScore[]}
338
+ */
339
+ function scoreAllFeatures(ctx) {
340
+ const out = [];
341
+ const features = (ctx.featureMap && ctx.featureMap.features) || [];
342
+ for (const f of features) {
343
+ out.push(scoreFeature(f, ctx));
344
+ }
345
+ return out;
346
+ }
347
+
348
+ /**
349
+ * Score a single feature.
350
+ * @param {Object} feature - A feature entry from readFeatureMap()
351
+ * @param {CompletenessContext} ctx
352
+ * @returns {FeatureScore}
353
+ */
354
+ function scoreFeature(feature, ctx) {
355
+ const acs = (feature.acs || []).map((ac) => {
356
+ const acRef = `${feature.id}/${ac.id}`;
357
+ return scoreAc(acRef, ctx);
358
+ });
359
+ const scoreSum = acs.reduce((sum, a) => sum + a.score, 0);
360
+ const averageScore = acs.length > 0 ? scoreSum / acs.length : NaN;
361
+ return {
362
+ featureId: feature.id,
363
+ state: feature.state || null,
364
+ acs,
365
+ averageScore,
366
+ acCount: acs.length,
367
+ scoreSum,
368
+ };
369
+ }
370
+
371
+ /**
372
+ * Format a terse per-feature breakdown suitable for `/cap:status --completeness`.
373
+ * @param {FeatureScore[]} scores
374
+ * @returns {string}
375
+ */
376
+ function formatFeatureBreakdown(scores) {
377
+ if (!Array.isArray(scores) || scores.length === 0) {
378
+ return 'No features to score.';
379
+ }
380
+ const lines = ['Completeness Score (per feature — avg of 4-point AC signals)'];
381
+ lines.push('');
382
+ for (const s of scores) {
383
+ const avg = Number.isFinite(s.averageScore) ? s.averageScore.toFixed(2) : '—';
384
+ lines.push(`${s.featureId} [${s.state || '?'}] avg=${avg}/4 (${s.acCount} AC)`);
385
+ for (const ac of s.acs) {
386
+ const flags = [
387
+ ac.signals.tag ? 'T' : '·',
388
+ ac.signals.test ? 'S' : '·',
389
+ ac.signals.testInvokesCode ? 'I' : '·',
390
+ ac.signals.reachable ? 'R' : '·',
391
+ ].join('');
392
+ lines.push(` ${ac.acRef.padEnd(14)} ${flags} score=${ac.score}/4`);
393
+ }
394
+ lines.push('');
395
+ }
396
+ lines.push('Legend: T=tagged S=tested I=test-invokes-code R=reachable-from-public');
397
+ return lines.join('\n');
398
+ }
399
+
400
+ /**
401
+ * Format a full markdown audit report suitable for PR attachment.
402
+ * @param {FeatureScore[]} scores
403
+ * @returns {string}
404
+ */
405
+ function formatCompletenessReport(scores) {
406
+ const lines = [];
407
+ lines.push('# Completeness Report');
408
+ lines.push('');
409
+ lines.push(`Generated at: ${new Date().toISOString()}`);
410
+ lines.push('');
411
+ lines.push('Signal legend:');
412
+ lines.push('- **T** = `@cap-*` tag in source code references the AC');
413
+ lines.push('- **S** = a test file carries a `@cap-*` tag for the AC');
414
+ lines.push('- **I** = at least one test file statically imports the primary implementation');
415
+ lines.push('- **R** = primary file is reachable from public surface (`bin/install.js`, `hooks/*.js`)');
416
+ lines.push('');
417
+
418
+ const scoreboard = scores.map((s) => {
419
+ const avg = Number.isFinite(s.averageScore) ? s.averageScore.toFixed(2) : '—';
420
+ return `| ${s.featureId} | ${s.state || '?'} | ${s.acCount} | ${avg} |`;
421
+ });
422
+ lines.push('## Summary');
423
+ lines.push('');
424
+ lines.push('| Feature | State | ACs | Avg Score |');
425
+ lines.push('|---------|-------|-----|-----------|');
426
+ lines.push(...scoreboard);
427
+ lines.push('');
428
+
429
+ for (const s of scores) {
430
+ lines.push(`## ${s.featureId}`);
431
+ lines.push('');
432
+ lines.push(`State: ${s.state || '?'} — Avg: ${Number.isFinite(s.averageScore) ? s.averageScore.toFixed(2) : '—'}/4`);
433
+ lines.push('');
434
+ lines.push('| AC | T | S | I | R | Score | Reasons |');
435
+ lines.push('|----|---|---|---|---|-------|---------|');
436
+ for (const ac of s.acs) {
437
+ const mark = (b) => (b ? '✓' : '·');
438
+ const reasons = ac.reasons.join('; ').replace(/\|/g, '\\|');
439
+ lines.push(
440
+ `| ${ac.acRef} | ${mark(ac.signals.tag)} | ${mark(ac.signals.test)} | ${mark(ac.signals.testInvokesCode)} | ${mark(ac.signals.reachable)} | ${ac.score}/4 | ${reasons} |`
441
+ );
442
+ }
443
+ lines.push('');
444
+ }
445
+ return lines.join('\n');
446
+ }
447
+
448
+ /**
449
+ * Load F-048 config from .cap/config.json with safe defaults.
450
+ * @param {string} cwd
451
+ * @returns {CompletenessConfig}
452
+ */
453
+ function loadCompletenessConfig(cwd) {
454
+ const configPath = path.join(cwd, CONFIG_FILE);
455
+ const cfg = { ...DEFAULT_CONFIG };
456
+ let raw;
457
+ try {
458
+ raw = fs.readFileSync(configPath, 'utf8');
459
+ } catch (err) {
460
+ // ENOENT is normal (no config yet) — stay silent. Other fs errors propagate silently too
461
+ // because completeness is opt-in and defaults are safe.
462
+ return cfg;
463
+ }
464
+ try {
465
+ const parsed = JSON.parse(raw);
466
+ const section = parsed && parsed.completenessScore;
467
+ if (section && typeof section === 'object') {
468
+ if (typeof section.enabled === 'boolean') cfg.enabled = section.enabled;
469
+ if (typeof section.shipThreshold === 'number' && Number.isFinite(section.shipThreshold)) {
470
+ cfg.shipThreshold = section.shipThreshold;
471
+ }
472
+ }
473
+ } catch (err) {
474
+ // File exists but is malformed JSON — warn once so the user sees that their
475
+ // opt-in settings are silently ignored. A hook env var suppresses it for CI.
476
+ if (!process.env.CAP_SILENT_CONFIG_WARNINGS) {
477
+ // eslint-disable-next-line no-console
478
+ console.warn(`[cap-completeness] .cap/config.json is not valid JSON (${err.message}); using defaults.`);
479
+ }
480
+ }
481
+ return cfg;
482
+ }
483
+
484
+ /**
485
+ * Gate for `updateFeatureState(..., 'shipped')`. Returns { allowed, reason }.
486
+ * Only enforces when config.enabled is true. When disabled, always allows.
487
+ *
488
+ * @param {string} featureId
489
+ * @param {string} targetState
490
+ * @param {string} cwd
491
+ * @param {CompletenessContext} [ctx] - Optional pre-built context (for tests / perf)
492
+ * @returns {{ allowed: boolean, reason: string|null, score: number|null }}
493
+ */
494
+ function checkShipGate(featureId, targetState, cwd, ctx) {
495
+ if (targetState !== 'shipped') return { allowed: true, reason: null, score: null };
496
+ const cfg = loadCompletenessConfig(cwd);
497
+ if (!cfg.enabled) return { allowed: true, reason: null, score: null };
498
+
499
+ const context = ctx || buildContext(cwd);
500
+ const feature = (context.featureMap.features || []).find((f) => f.id === featureId);
501
+ if (!feature) {
502
+ return { allowed: true, reason: null, score: null };
503
+ }
504
+ const score = scoreFeature(feature, context);
505
+ if (!Number.isFinite(score.averageScore)) {
506
+ // No ACs — cannot compute. Allow (treat as out-of-scope for the gate).
507
+ return { allowed: true, reason: null, score: null };
508
+ }
509
+ if (score.averageScore < cfg.shipThreshold) {
510
+ return {
511
+ allowed: false,
512
+ reason:
513
+ `Completeness score for ${featureId} is ${score.averageScore.toFixed(2)}/4 — ` +
514
+ `below the configured shipThreshold=${cfg.shipThreshold}. ` +
515
+ `Run /cap:completeness-report for per-AC details.`,
516
+ score: score.averageScore,
517
+ };
518
+ }
519
+ return { allowed: true, reason: null, score: score.averageScore };
520
+ }
521
+
522
+ module.exports = {
523
+ // constants
524
+ DEFAULT_CONFIG,
525
+ // pure helpers
526
+ isTestFile,
527
+ scoreAc,
528
+ scoreFeature,
529
+ scoreAllFeatures,
530
+ formatFeatureBreakdown,
531
+ formatCompletenessReport,
532
+ // reachability
533
+ collectPublicSurfaceFiles,
534
+ computePublicReachable,
535
+ // context
536
+ buildContext,
537
+ // config + gate
538
+ loadCompletenessConfig,
539
+ checkShipGate,
540
+ };