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,228 @@
1
+ 'use strict';
2
+
3
+ // @cap-feature(feature:F-047, primary:true) Unified Feature Anchor Block parser.
4
+ // Parses the new single-block syntax introduced by CAP v3:
5
+ //
6
+ // /* @cap feature:F-001 acs:[AC-1,AC-3] role:primary */
7
+ // # @cap feature:F-001 acs:[AC-1,AC-3] role:primary
8
+ // <!-- @cap feature:F-001 acs:[AC-1,AC-3] role:primary -->
9
+ //
10
+ // The parser is language-agnostic — it is called by cap-tag-scanner AFTER comment
11
+ // delimiter stripping, so this module only sees the inner `@cap key:value ...` content.
12
+ //
13
+ // @cap-decision Pure logic, zero side effects. Scanner owns the "is this a comment?" layer
14
+ // (F-046 polylingual detection) and feeds stripped lines into parseAnchorLine(). This keeps
15
+ // the scanner single-source-of-truth for comment detection and avoids duplicating the
16
+ // polyglot rules here.
17
+ // @cap-decision expandAnchorToTags() emits tags in the SAME shape as scanner.extractTags()
18
+ // so all downstream code (buildAcFileMap, cap-deps, cap-completeness, cap-reconcile) works
19
+ // unchanged. Legacy fragmented tags and unified anchors are indistinguishable at the tag
20
+ // consumer layer — only the scanner layer sees the new syntax.
21
+
22
+ // @cap-risk Regex-based. Does not parse nested brackets or quoted key:value pairs.
23
+ // Limitations:
24
+ // - values with `[`, `]`, `,`, or whitespace must not appear unquoted (documented)
25
+ // - no support for multi-line anchor bodies (block must be single line inside its comment)
26
+ // - scanAnchorsInContent() reads raw file content, so a string literal containing
27
+ // `@cap feature:F-XXX` in source (e.g. inside a test fixture) can produce a false
28
+ // match. Callers needing strict string-literal awareness should wire in F-046's
29
+ // polyglot string-stripping upstream. KV_TOKEN_RE rejects most accidental text.
30
+ // These are intentional for v1 to keep parsing unambiguous.
31
+
32
+ // @cap-decision Space discriminator is load-bearing: `@cap ` (with trailing space) is the
33
+ // unified anchor marker, `@cap-` (with hyphen) is the legacy tag family. The two formats
34
+ // never collide because no legal tag name contains whitespace, and no legal anchor lacks
35
+ // the space between `@cap` and the first key. Documented in docs/F-047-decision.md.
36
+
37
+ // Matches `@cap <rest>` — called on an already-decommented line or the inner content of a
38
+ // block comment. Captures `rest` which is then tokenised into key:value pairs.
39
+ const ANCHOR_RE = /@cap\s+([^\n]+)/;
40
+
41
+ // Matches `key:value` where value is either `[list,of,items]` or a bare token (no whitespace,
42
+ // no commas, no brackets). The anchor body is split into key:value pairs by whitespace.
43
+ const KV_TOKEN_RE = /^([a-zA-Z][a-zA-Z0-9_]*)\s*:\s*(\[[^\]]*\]|[^\s\[\]]+)$/;
44
+
45
+ /**
46
+ * @typedef {Object} ParsedAnchor
47
+ * @property {string} feature - Feature ID (e.g. 'F-001'); required
48
+ * @property {string[]} acs - AC IDs (e.g. ['AC-1','AC-3']); empty when not specified
49
+ * @property {('primary'|'secondary'|null)} role - 'primary', 'secondary', or null (unspecified)
50
+ * @property {string} raw - The original `@cap …` text for error reporting
51
+ * @property {string[]} warnings - Soft warnings (unknown keys, malformed AC ids, …)
52
+ */
53
+
54
+ /**
55
+ * Parse a single `@cap key:value …` body (the content inside the comment, already
56
+ * stripped of delimiters like `/*`, `*` /, `#`, `<!--`, `-->`).
57
+ *
58
+ * Returns null when no `@cap` token is present or the line is completely malformed.
59
+ * When the token is present but some keys are unrecognised or values are malformed,
60
+ * still returns a ParsedAnchor with the recognised subset plus a `warnings` array so
61
+ * callers can surface soft failures without losing usable information.
62
+ *
63
+ * @param {string} line
64
+ * @returns {ParsedAnchor|null}
65
+ */
66
+ function parseAnchorLine(line) {
67
+ if (typeof line !== 'string') return null;
68
+ const m = line.match(ANCHOR_RE);
69
+ if (!m) return null;
70
+
71
+ const body = m[1].trim();
72
+ // Strip trailing comment delimiters that may have leaked through (e.g. `-->` or `*/`)
73
+ const cleaned = body
74
+ .replace(/\s*-->\s*$/, '')
75
+ .replace(/\s*\*\/\s*$/, '')
76
+ .trim();
77
+
78
+ /** @type {ParsedAnchor} */
79
+ const out = { feature: '', acs: [], role: null, raw: m[0], warnings: [] };
80
+ if (cleaned.length === 0) {
81
+ out.warnings.push('empty anchor body');
82
+ return out;
83
+ }
84
+
85
+ // Tokenise by whitespace; each token must match key:value.
86
+ const tokens = cleaned.split(/\s+/);
87
+ for (const tok of tokens) {
88
+ const km = tok.match(KV_TOKEN_RE);
89
+ if (!km) {
90
+ out.warnings.push(`unparseable token: ${tok}`);
91
+ continue;
92
+ }
93
+ const key = km[1];
94
+ const value = km[2];
95
+ switch (key) {
96
+ case 'feature':
97
+ if (!/^F-\d{3,}$/.test(value)) {
98
+ out.warnings.push(`feature value must match /^F-\\d{3,}$/ (got ${value})`);
99
+ }
100
+ out.feature = value;
101
+ break;
102
+ case 'acs': {
103
+ if (!value.startsWith('[') || !value.endsWith(']')) {
104
+ out.warnings.push(`acs must be [bracketed,list] (got ${value})`);
105
+ break;
106
+ }
107
+ const inner = value.slice(1, -1).trim();
108
+ const items = inner.length === 0 ? [] : inner.split(',').map((s) => s.trim()).filter(Boolean);
109
+ for (const ac of items) {
110
+ if (!/^AC-\d+$/.test(ac)) {
111
+ out.warnings.push(`acs item must match /^AC-\\d+$/ (got ${ac})`);
112
+ }
113
+ }
114
+ out.acs = items;
115
+ break;
116
+ }
117
+ case 'role':
118
+ if (value !== 'primary' && value !== 'secondary') {
119
+ out.warnings.push(`role must be 'primary' or 'secondary' (got ${value})`);
120
+ }
121
+ out.role = value;
122
+ break;
123
+ default:
124
+ out.warnings.push(`unknown key: ${key}`);
125
+ break;
126
+ }
127
+ }
128
+
129
+ return out;
130
+ }
131
+
132
+ /**
133
+ * Expand a parsed anchor into the CapTag[] shape used elsewhere in CAP.
134
+ * Emits:
135
+ * - one @cap-feature tag (primary:true flag when role === 'primary')
136
+ * - one @cap-todo tag per AC listed in `anchor.acs`, with `ac: F-XXX/AC-N`
137
+ *
138
+ * When anchor.feature is empty (parse error), returns [] — the caller can still
139
+ * inspect anchor.warnings for diagnostics.
140
+ *
141
+ * @param {ParsedAnchor} anchor
142
+ * @param {string} filePath - Relative file path (for tag.file)
143
+ * @param {number} lineNumber - 1-based line number of the anchor in the source
144
+ * @returns {CapTag[]}
145
+ */
146
+ function expandAnchorToTags(anchor, filePath, lineNumber) {
147
+ if (!anchor || !anchor.feature) return [];
148
+ /** @type {CapTag[]} */
149
+ const tags = [];
150
+ const metadata = { feature: anchor.feature };
151
+ if (anchor.role === 'primary') metadata.primary = true;
152
+ tags.push({
153
+ type: 'feature',
154
+ file: filePath,
155
+ line: lineNumber,
156
+ metadata,
157
+ description: `unified anchor for ${anchor.feature}`,
158
+ raw: anchor.raw,
159
+ });
160
+ for (const ac of anchor.acs || []) {
161
+ tags.push({
162
+ type: 'todo',
163
+ file: filePath,
164
+ line: lineNumber,
165
+ metadata: { ac: `${anchor.feature}/${ac}` },
166
+ description: `AC reference expanded from unified anchor`,
167
+ raw: anchor.raw,
168
+ });
169
+ }
170
+ return tags;
171
+ }
172
+
173
+ /**
174
+ * Serialize a structured anchor into the canonical block string, using the
175
+ * requested comment style. Used by the migration tool to write the unified
176
+ * block back to source.
177
+ *
178
+ * @param {{feature:string, acs?:string[], role?:string}} anchor
179
+ * @param {('block'|'line'|'html')} [style='block'] - comment family
180
+ * @returns {string} Single-line block, no trailing newline
181
+ */
182
+ function emitAnchorBlock(anchor, style = 'block') {
183
+ const parts = [];
184
+ parts.push(`feature:${anchor.feature}`);
185
+ if (Array.isArray(anchor.acs) && anchor.acs.length > 0) {
186
+ parts.push(`acs:[${anchor.acs.join(',')}]`);
187
+ }
188
+ if (anchor.role === 'primary' || anchor.role === 'secondary') {
189
+ parts.push(`role:${anchor.role}`);
190
+ }
191
+ const body = `@cap ${parts.join(' ')}`;
192
+ if (style === 'line') return `# ${body}`;
193
+ if (style === 'html') return `<!-- ${body} -->`;
194
+ // default: block comment
195
+ return `/* ${body} */`;
196
+ }
197
+
198
+ /**
199
+ * Convenience: scan a full file content for unified anchor blocks and expand each.
200
+ * Internal use by cap-tag-scanner when unifiedAnchors.enabled is true.
201
+ *
202
+ * @param {string} content - Full file content
203
+ * @param {string} filePath - Relative path for tag.file
204
+ * @returns {CapTag[]} All tags expanded from every anchor in the file
205
+ */
206
+ function scanAnchorsInContent(content, filePath) {
207
+ if (typeof content !== 'string' || content.length === 0) return [];
208
+ const tags = [];
209
+ const lines = content.split('\n');
210
+ for (let i = 0; i < lines.length; i++) {
211
+ const line = lines[i];
212
+ if (!line.includes('@cap ')) continue; // fast-path filter; space distinguishes from `@cap-feature`
213
+ const parsed = parseAnchorLine(line);
214
+ if (!parsed || !parsed.feature) continue;
215
+ tags.push(...expandAnchorToTags(parsed, filePath, i + 1));
216
+ }
217
+ return tags;
218
+ }
219
+
220
+ module.exports = {
221
+ parseAnchorLine,
222
+ expandAnchorToTags,
223
+ emitAnchorBlock,
224
+ scanAnchorsInContent,
225
+ // constants (exported for tests)
226
+ ANCHOR_RE,
227
+ KV_TOKEN_RE,
228
+ };
@@ -0,0 +1,340 @@
1
+ // @cap-feature(feature:F-028) Code Annotation Writer — write @cap-history, @cap-pitfall, @cap-pattern annotations into source files
2
+ // @cap-decision Annotations placed at file-top block, after shebang/'use strict', alongside existing @cap-feature tags.
3
+ // @cap-decision Comment syntax detected per file extension — language-agnostic, same approach as tag scanner.
4
+ // @cap-constraint Zero external dependencies — uses only Node.js built-ins.
5
+
6
+ 'use strict';
7
+
8
+ // @cap-history(sessions:2, edits:7, since:2026-04-03, learned:2026-04-03) Frequently modified — 2 sessions, 7 edits
9
+ const fs = require('node:fs');
10
+ const path = require('node:path');
11
+
12
+ // --- Comment Syntax Detection (AC-2) ---
13
+
14
+ /** @type {Object<string, string>} Extension to single-line comment prefix mapping */
15
+ const COMMENT_PREFIX_MAP = {
16
+ // // style
17
+ '.js': '//', '.cjs': '//', '.mjs': '//', '.ts': '//', '.tsx': '//', '.jsx': '//',
18
+ '.go': '//', '.rs': '//', '.c': '//', '.cpp': '//', '.h': '//', '.java': '//',
19
+ '.swift': '//', '.kt': '//', '.scala': '//', '.cs': '//', '.dart': '//', '.zig': '//',
20
+ // # style
21
+ '.py': '#', '.rb': '#', '.sh': '#', '.bash': '#', '.zsh': '#', '.fish': '#',
22
+ '.yml': '#', '.yaml': '#', '.toml': '#', '.pl': '#', '.pm': '#', '.r': '#',
23
+ '.tf': '#', '.hcl': '#', '.dockerfile': '#', '.conf': '#', '.ini': '#',
24
+ // -- style
25
+ '.sql': '--', '.lua': '--', '.hs': '--', '.elm': '--',
26
+ // ; style
27
+ '.lisp': ';', '.clj': ';', '.el': ';', '.scm': ';',
28
+ // % style
29
+ '.erl': '%', '.tex': '%', '.m': '%',
30
+ };
31
+
32
+ // @cap-todo(ref:F-028:AC-2) Detect correct comment syntax for target file based on extension
33
+
34
+ /** File extensions that must never receive annotations (no valid comment syntax or structured format). */
35
+ const ANNOTATION_BLOCKLIST = new Set([
36
+ '.md', '.markdown', '.json', '.jsonl', '.lock', '.svg', '.xml', '.html', '.htm',
37
+ '.css', '.scss', '.less', '.png', '.jpg', '.jpeg', '.gif', '.ico', '.woff', '.woff2',
38
+ '.ttf', '.eot', '.map', '.min.js', '.min.css', '.patch', '.diff',
39
+ ]);
40
+
41
+ /**
42
+ * Check if a file can receive annotations.
43
+ * @param {string} filePath
44
+ * @returns {boolean}
45
+ */
46
+ function canAnnotate(filePath) {
47
+ const ext = path.extname(filePath).toLowerCase();
48
+ const basename = path.basename(filePath).toLowerCase();
49
+ if (ANNOTATION_BLOCKLIST.has(ext)) return false;
50
+ if (basename === 'package-lock.json' || basename === 'yarn.lock' || basename === 'pnpm-lock.yaml') return false;
51
+ if (basename.endsWith('.md')) return false;
52
+ return true;
53
+ }
54
+
55
+ /**
56
+ * Get the single-line comment prefix for a file.
57
+ * @param {string} filePath
58
+ * @returns {string} Comment prefix (defaults to '//')
59
+ */
60
+ function getCommentPrefix(filePath) {
61
+ const ext = path.extname(filePath).toLowerCase();
62
+ // Dockerfile has no extension but starts with FROM
63
+ if (path.basename(filePath).toLowerCase() === 'dockerfile') return '#';
64
+ return COMMENT_PREFIX_MAP[ext] || '//';
65
+ }
66
+
67
+ // --- Annotation Parsing ---
68
+
69
+ /** Regex to match existing memory annotations in a line */
70
+ const MEMORY_TAG_RE = /^(\s*(?:\/\/|#|--|;|%)\s*)@(cap-history|cap-pitfall|cap-pattern|cap-decision)\b/;
71
+
72
+ /**
73
+ * @typedef {Object} ParsedAnnotation
74
+ * @property {number} lineIndex - 0-based line index in file
75
+ * @property {string} tag - Tag name (e.g., 'cap-history')
76
+ * @property {string} fullLine - Complete line text
77
+ * @property {string} prefix - Comment prefix with whitespace
78
+ */
79
+
80
+ /**
81
+ * Parse existing memory annotations from file lines.
82
+ * @param {string[]} lines
83
+ * @returns {ParsedAnnotation[]}
84
+ */
85
+ function parseExistingAnnotations(lines) {
86
+ const annotations = [];
87
+ for (let i = 0; i < lines.length; i++) {
88
+ const match = lines[i].match(MEMORY_TAG_RE);
89
+ if (match) {
90
+ annotations.push({
91
+ lineIndex: i,
92
+ tag: match[2],
93
+ fullLine: lines[i],
94
+ prefix: match[1],
95
+ });
96
+ }
97
+ }
98
+ return annotations;
99
+ }
100
+
101
+ // --- Insertion Point Detection ---
102
+
103
+ // @cap-todo(ref:F-028:AC-1) Insert annotations at file-top block alongside existing @cap-feature tags
104
+
105
+ /**
106
+ * Find the line index where memory annotations should be inserted.
107
+ * After shebang, 'use strict', and existing @cap-* annotation block.
108
+ * @param {string[]} lines
109
+ * @returns {number} Line index for insertion
110
+ */
111
+ function findInsertionPoint(lines) {
112
+ let insertAt = 0;
113
+
114
+ for (let i = 0; i < lines.length; i++) {
115
+ const line = lines[i].trim();
116
+
117
+ // Skip shebang
118
+ if (i === 0 && line.startsWith('#!')) {
119
+ insertAt = i + 1;
120
+ continue;
121
+ }
122
+
123
+ // Skip 'use strict'
124
+ if (line === "'use strict';" || line === '"use strict";') {
125
+ insertAt = i + 1;
126
+ continue;
127
+ }
128
+
129
+ // Skip empty lines at top
130
+ if (line === '' && i <= insertAt) {
131
+ insertAt = i + 1;
132
+ continue;
133
+ }
134
+
135
+ // Skip existing @cap-* annotation lines
136
+ if (/^\s*(?:\/\/|#|--|;|%)\s*@cap-/.test(line)) {
137
+ insertAt = i + 1;
138
+ continue;
139
+ }
140
+
141
+ // Stop at first non-annotation, non-header line
142
+ break;
143
+ }
144
+
145
+ return insertAt;
146
+ }
147
+
148
+ // --- Write Operations ---
149
+
150
+ // @cap-todo(ref:F-028:AC-3) Update existing annotations in-place without creating duplicates
151
+ // @cap-todo(ref:F-028:AC-4) Remove annotations marked as stale
152
+ // @cap-todo(ref:F-028:AC-7) Support dry-run mode
153
+
154
+ /**
155
+ * @typedef {Object} AnnotationChange
156
+ * @property {'add'|'update'|'remove'} action
157
+ * @property {string} file
158
+ * @property {string} annotation - Formatted annotation text
159
+ * @property {number} [lineIndex] - For update/remove: existing line
160
+ */
161
+
162
+ /**
163
+ * Plan annotation changes for a single file.
164
+ * @param {string} filePath
165
+ * @param {string} fileContent - Current file content
166
+ * @param {import('./cap-memory-engine.cjs').MemoryEntry[]} entries - New entries for this file
167
+ * @param {string[]} [staleContentPrefixes] - Content prefixes of stale entries to remove
168
+ * @returns {AnnotationChange[]}
169
+ */
170
+ function planFileChanges(filePath, fileContent, entries, staleContentPrefixes = []) {
171
+ const commentPrefix = getCommentPrefix(filePath);
172
+ const lines = fileContent.split('\n');
173
+ const existing = parseExistingAnnotations(lines);
174
+ const changes = [];
175
+
176
+ const { formatAnnotation } = require('./cap-memory-engine.cjs');
177
+
178
+ // Plan removals for stale entries (AC-4)
179
+ for (const ann of existing) {
180
+ const lineContent = ann.fullLine.replace(ann.prefix, '').trim();
181
+ if (staleContentPrefixes.some(prefix => lineContent.startsWith(prefix))) {
182
+ changes.push({ action: 'remove', file: filePath, annotation: ann.fullLine, lineIndex: ann.lineIndex });
183
+ }
184
+ }
185
+
186
+ // Plan adds/updates for new entries
187
+ for (const entry of entries) {
188
+ const formatted = formatAnnotation(entry);
189
+ // @cap-todo(ac:F-086/AC-1) Dedup matcher: aggregate annotations (one-per-file) like
190
+ // @cap-history must match by TAG NAME alone, since their content carries live edit
191
+ // counts that change between runs. Matching by content-prefix caused a duplicate
192
+ // line every time the stats changed (observed on GoetzeInvest hub-types.ts).
193
+ // Per-occurrence annotations (@cap-pitfall, @cap-pattern, @cap-decision) keep the
194
+ // content-prefix match because multiple distinct ones can legitimately coexist.
195
+ const tagName = formatted.split('(')[0].split(' ')[0]; // e.g., @cap-history
196
+ const contentKey = entry.content.substring(0, 60).toLowerCase();
197
+ const isAggregateTag = tagName === '@cap-history';
198
+
199
+ const existingMatch = existing.find(ann => {
200
+ const annContent = ann.fullLine.replace(ann.prefix, '').trim();
201
+ if (!annContent.startsWith(tagName)) return false;
202
+ if (isAggregateTag) return true; // any existing @cap-history on this file is "the" one
203
+ return annContent.toLowerCase().includes(contentKey);
204
+ });
205
+
206
+ if (existingMatch) {
207
+ // Update in-place (AC-3)
208
+ const newLine = `${commentPrefix} ${formatted}`;
209
+ if (existingMatch.fullLine.trim() !== newLine.trim()) {
210
+ changes.push({ action: 'update', file: filePath, annotation: newLine, lineIndex: existingMatch.lineIndex });
211
+ }
212
+ } else {
213
+ // Add new
214
+ changes.push({ action: 'add', file: filePath, annotation: `${commentPrefix} ${formatted}` });
215
+ }
216
+ }
217
+
218
+ return changes;
219
+ }
220
+
221
+ /**
222
+ * Apply planned changes to a file's content.
223
+ * @param {string} fileContent
224
+ * @param {AnnotationChange[]} changes
225
+ * @returns {string} Updated file content
226
+ */
227
+ function applyChanges(fileContent, changes) {
228
+ const lines = fileContent.split('\n');
229
+
230
+ // Apply removals and updates (by line index, process from bottom to preserve indices)
231
+ const lineChanges = changes
232
+ .filter(c => c.action === 'remove' || c.action === 'update')
233
+ .sort((a, b) => b.lineIndex - a.lineIndex);
234
+
235
+ for (const change of lineChanges) {
236
+ if (change.action === 'remove') {
237
+ lines.splice(change.lineIndex, 1);
238
+ } else if (change.action === 'update') {
239
+ lines[change.lineIndex] = change.annotation;
240
+ }
241
+ }
242
+
243
+ // Apply additions at insertion point
244
+ const additions = changes.filter(c => c.action === 'add');
245
+ if (additions.length > 0) {
246
+ const insertAt = findInsertionPoint(lines);
247
+ const newLines = additions.map(a => a.annotation);
248
+ lines.splice(insertAt, 0, ...newLines);
249
+ }
250
+
251
+ return lines.join('\n');
252
+ }
253
+
254
+ // @cap-todo(ref:F-028:AC-5) Format annotations with parenthesized metadata matching existing tag conventions
255
+ // @cap-todo(ref:F-028:AC-6) Be parseable by existing tag scanner without modifications
256
+
257
+ /**
258
+ * Write memory annotations to files.
259
+ * @param {Object<string, import('./cap-memory-engine.cjs').MemoryEntry[]>} fileEntries - Map of filePath -> entries
260
+ * @param {Object} [options]
261
+ * @param {boolean} [options.dryRun] - If true, return changes without writing
262
+ * @param {Object<string, string[]>} [options.staleByFile] - Map of filePath -> stale content prefixes to remove
263
+ * @returns {{changes: AnnotationChange[], filesModified: number}}
264
+ */
265
+ function writeAnnotations(fileEntries, options = {}) {
266
+ const allChanges = [];
267
+ let filesModified = 0;
268
+
269
+ for (const [filePath, entries] of Object.entries(fileEntries)) {
270
+ if (!fs.existsSync(filePath)) continue;
271
+ if (!canAnnotate(filePath)) continue;
272
+
273
+ const content = fs.readFileSync(filePath, 'utf8');
274
+ const stale = options.staleByFile?.[filePath] || [];
275
+ const changes = planFileChanges(filePath, content, entries, stale);
276
+
277
+ if (changes.length === 0) continue;
278
+
279
+ allChanges.push(...changes);
280
+
281
+ if (!options.dryRun) {
282
+ const updated = applyChanges(content, changes);
283
+ fs.writeFileSync(filePath, updated, 'utf8');
284
+ }
285
+ filesModified++;
286
+ }
287
+
288
+ return { changes: allChanges, filesModified };
289
+ }
290
+
291
+ /**
292
+ * Remove stale annotations from files.
293
+ * @param {Array<{file: string, content: string}>} staleEntries
294
+ * @param {Object} [options]
295
+ * @param {boolean} [options.dryRun]
296
+ * @returns {{removed: number, filesModified: number}}
297
+ */
298
+ function removeStaleAnnotations(staleEntries, options = {}) {
299
+ const byFile = {};
300
+ for (const entry of staleEntries) {
301
+ if (!byFile[entry.file]) byFile[entry.file] = [];
302
+ byFile[entry.file].push(entry.content.substring(0, 60));
303
+ }
304
+
305
+ let removed = 0;
306
+ let filesModified = 0;
307
+
308
+ for (const [filePath, prefixes] of Object.entries(byFile)) {
309
+ if (!fs.existsSync(filePath)) continue;
310
+
311
+ const content = fs.readFileSync(filePath, 'utf8');
312
+ const changes = planFileChanges(filePath, content, [], prefixes);
313
+ const removals = changes.filter(c => c.action === 'remove');
314
+
315
+ if (removals.length === 0) continue;
316
+
317
+ if (!options.dryRun) {
318
+ const updated = applyChanges(content, removals);
319
+ fs.writeFileSync(filePath, updated, 'utf8');
320
+ }
321
+ removed += removals.length;
322
+ filesModified++;
323
+ }
324
+
325
+ return { removed, filesModified };
326
+ }
327
+
328
+ module.exports = {
329
+ canAnnotate,
330
+ getCommentPrefix,
331
+ parseExistingAnnotations,
332
+ findInsertionPoint,
333
+ planFileChanges,
334
+ applyChanges,
335
+ writeAnnotations,
336
+ removeStaleAnnotations,
337
+ COMMENT_PREFIX_MAP,
338
+ ANNOTATION_BLOCKLIST,
339
+ MEMORY_TAG_RE,
340
+ };