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,434 @@
1
+ // @cap-context CAP v2.0 checkpoint detector -- advisory logic for /cap:checkpoint slash command.
2
+ // @cap-history(sessions:2, edits:5, since:2026-04-20, learned:2026-04-21) Frequently modified — 2 sessions, 5 edits
3
+ // Detects natural breakpoints in the workflow so the user can be nudged toward /compact before
4
+ // auto-compact degrades context quality.
5
+
6
+ 'use strict';
7
+
8
+ // @cap-feature(feature:F-057) Checkpoint Command for Strategic Compact — pure logic
9
+ // @cap-decision Breakpoint detection is side-effect-free — returns a plan object, never mutates disk on its own. Orchestrator (the slash command) is responsible for invoking /cap:save and printing.
10
+ // @cap-constraint Zero runtime deps — node: built-ins only
11
+
12
+ const fs = require('node:fs');
13
+ const path = require('node:path');
14
+
15
+ const capSession = require('./cap-session.cjs');
16
+
17
+ // @cap-todo(ac:F-057/AC-2) Known terminal session-step markers that indicate a logical workflow phase has completed.
18
+ /**
19
+ * Session-step markers that constitute a breakpoint signal when reached.
20
+ * Kept as a Set for O(1) membership checks.
21
+ */
22
+ const TERMINAL_STEPS = new Set([
23
+ 'test-complete',
24
+ 'review-complete',
25
+ 'prototype-complete',
26
+ 'brainstorm-complete',
27
+ 'iterate-complete',
28
+ ]);
29
+
30
+ // @cap-decision Feature state ranking — used by pickBreakpoint to select the "biggest" transition when
31
+ // multiple features moved at once. Higher rank wins. 'planned' is rank 0 (not a breakpoint on its own).
32
+ /**
33
+ * Relative weight of each feature-state value.
34
+ * @type {Object<string, number>}
35
+ */
36
+ const STATE_RANK = {
37
+ shipped: 3,
38
+ tested: 2,
39
+ prototyped: 1,
40
+ planned: 0,
41
+ };
42
+
43
+ // Human-readable labels for session-step breakpoints.
44
+ const STEP_REASONS = {
45
+ 'test-complete': 'Test-Phase abgeschlossen',
46
+ 'review-complete': 'Review-Phase abgeschlossen',
47
+ 'prototype-complete': 'Prototype-Phase abgeschlossen',
48
+ 'brainstorm-complete': 'Brainstorm-Phase abgeschlossen',
49
+ 'iterate-complete': 'Iterate-Phase abgeschlossen',
50
+ };
51
+
52
+ /**
53
+ * @typedef {Object} FeatureSnapshot
54
+ * @property {Object<string,string>} featureStates - Map of feature ID -> state (e.g., "F-054" -> "tested")
55
+ * @property {Object<string,string>} acStatuses - Map of "F-NNN/AC-M" -> status
56
+ */
57
+
58
+ /**
59
+ * @typedef {Object} FeatureDiff
60
+ * @property {string} featureId - Feature ID that changed
61
+ * @property {'state-transition'|'ac-status-update'} type - Kind of change
62
+ * @property {string|null} from - Previous value (null if no prior snapshot)
63
+ * @property {string} to - New value
64
+ * @property {string} [acId] - AC identifier (only for ac-status-update)
65
+ */
66
+
67
+ /**
68
+ * @typedef {Object} Breakpoint
69
+ * @property {'feature-transition'|'ac-update'|'session-step'} kind - Which signal triggered the breakpoint
70
+ * @property {string} [featureId] - Feature ID involved (optional for session-step)
71
+ * @property {string} reason - Human-readable reason, used in the recommendation
72
+ */
73
+
74
+ /**
75
+ * @typedef {Object} CheckpointPlan
76
+ * @property {boolean} shouldSave - Whether the orchestrator should invoke /cap:save
77
+ * @property {string|null} saveLabel - Label to pass to /cap:save (positional [name] arg)
78
+ * @property {string} message - Human-readable recommendation or "no breakpoint" notice
79
+ */
80
+
81
+ /**
82
+ * @typedef {Object} AnalyzeResult
83
+ * @property {Breakpoint|null} breakpoint - Detected breakpoint, or null
84
+ * @property {CheckpointPlan} plan - Plan describing what the orchestrator should do
85
+ * @property {FeatureSnapshot} currentSnapshot - Fresh snapshot of current feature states (for applyCheckpoint)
86
+ */
87
+
88
+ // @cap-todo(ac:F-057/AC-2) captureFeatureSnapshot — transforms a FeatureMap feature[] array into the
89
+ // flat {featureStates, acStatuses} form persisted in SESSION.json.
90
+ /**
91
+ * Produce a minimal snapshot of feature states and AC statuses for later diffing.
92
+ * Pure function.
93
+ * @param {Array<{id: string, state: string, acs: Array<{id: string, status: string}>}>} features
94
+ * @returns {FeatureSnapshot}
95
+ */
96
+ function captureFeatureSnapshot(features) {
97
+ const snapshot = {
98
+ featureStates: {},
99
+ acStatuses: {},
100
+ };
101
+ if (!Array.isArray(features)) return snapshot;
102
+
103
+ for (const feature of features) {
104
+ if (!feature || typeof feature.id !== 'string') continue;
105
+ snapshot.featureStates[feature.id] = feature.state || 'planned';
106
+ if (Array.isArray(feature.acs)) {
107
+ for (const ac of feature.acs) {
108
+ if (!ac || typeof ac.id !== 'string') continue;
109
+ snapshot.acStatuses[`${feature.id}/${ac.id}`] = ac.status || 'pending';
110
+ }
111
+ }
112
+ }
113
+ return snapshot;
114
+ }
115
+
116
+ /**
117
+ * Diff a single feature's state against the prior snapshot. Pushes at most one
118
+ * state-transition diff into `out`.
119
+ * @param {{id:string, state:string}} feature
120
+ * @param {Object<string,string>} prevStates
121
+ * @param {boolean} isFirstTime
122
+ * @param {FeatureDiff[]} out
123
+ */
124
+ function diffFeatureState(feature, prevStates, isFirstTime, out) {
125
+ const curState = feature.state || 'planned';
126
+ const prevState = prevStates[feature.id];
127
+
128
+ if (isFirstTime || prevState === undefined) {
129
+ // Both cases emit only for non-planned features — "planned" is the baseline, not a transition.
130
+ if (curState !== 'planned') {
131
+ out.push({ featureId: feature.id, type: 'state-transition', from: null, to: curState });
132
+ }
133
+ return;
134
+ }
135
+ if (prevState !== curState) {
136
+ out.push({ featureId: feature.id, type: 'state-transition', from: prevState, to: curState });
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Diff a single feature's AC statuses against the prior snapshot. Pushes any
142
+ * ac-status-update diffs into `out`.
143
+ * @param {{id:string, acs?:Array<{id:string, status:string}>}} feature
144
+ * @param {Object<string,string>} prevAcs
145
+ * @param {boolean} isFirstTime
146
+ * @param {FeatureDiff[]} out
147
+ */
148
+ function diffFeatureACs(feature, prevAcs, isFirstTime, out) {
149
+ if (!Array.isArray(feature.acs)) return;
150
+ for (const ac of feature.acs) {
151
+ if (!ac || typeof ac.id !== 'string') continue;
152
+ const key = `${feature.id}/${ac.id}`;
153
+ const curStatus = ac.status || 'pending';
154
+ const prevStatus = prevAcs[key];
155
+
156
+ if (isFirstTime || prevStatus === undefined) {
157
+ if (curStatus !== 'pending') {
158
+ out.push({ featureId: feature.id, type: 'ac-status-update', acId: ac.id, from: null, to: curStatus });
159
+ }
160
+ continue;
161
+ }
162
+ if (prevStatus !== curStatus) {
163
+ out.push({ featureId: feature.id, type: 'ac-status-update', acId: ac.id, from: prevStatus, to: curStatus });
164
+ }
165
+ }
166
+ }
167
+
168
+ // @cap-todo(ac:F-057/AC-2) diffFeatureStates — finds every state-transition and ac-status-update between
169
+ // a prior snapshot and the current feature array. Returns an array of diff objects.
170
+ /**
171
+ * Compute the diff between a previous snapshot and the current feature list.
172
+ * When prevSnapshot is null/empty, treats all non-planned features and all non-pending ACs as new
173
+ * transitions (i.e. first-time checkpoint).
174
+ * @param {FeatureSnapshot|null} prevSnapshot
175
+ * @param {Array<{id: string, state: string, acs: Array<{id: string, status: string}>}>} currentFeatures
176
+ * @returns {FeatureDiff[]}
177
+ */
178
+ function diffFeatureStates(prevSnapshot, currentFeatures) {
179
+ const diffs = [];
180
+ if (!Array.isArray(currentFeatures)) return diffs;
181
+
182
+ const prevStates = (prevSnapshot && prevSnapshot.featureStates) || {};
183
+ const prevAcs = (prevSnapshot && prevSnapshot.acStatuses) || {};
184
+ const isFirstTime =
185
+ !prevSnapshot ||
186
+ (Object.keys(prevStates).length === 0 && Object.keys(prevAcs).length === 0);
187
+
188
+ for (const feature of currentFeatures) {
189
+ if (!feature || typeof feature.id !== 'string') continue;
190
+ diffFeatureState(feature, prevStates, isFirstTime, diffs);
191
+ diffFeatureACs(feature, prevAcs, isFirstTime, diffs);
192
+ }
193
+
194
+ return diffs;
195
+ }
196
+
197
+ /**
198
+ * Parse the numeric portion of a feature ID (e.g. "F-057" -> 57). Returns -1 if unparseable
199
+ * so such IDs sort before valid ones (i.e. valid numeric IDs win "younger" comparisons).
200
+ * @param {string} id
201
+ * @returns {number}
202
+ */
203
+ function featureNumericId(id) {
204
+ const m = /^F-(\d+)$/.exec(id || '');
205
+ return m ? parseInt(m[1], 10) : -1;
206
+ }
207
+
208
+ // @cap-todo(ac:F-057/AC-3) pickBreakpoint — applies priority rules to the diff array + session step to
209
+ // produce a single Breakpoint object (or null). Priority: feature-transition > ac-update > session-step.
210
+ /**
211
+ * Pick the single most significant breakpoint from a diff list and an optional session step.
212
+ * Priority:
213
+ * 1. Feature state transitions. Tie-break by higher STATE_RANK, then by larger feature number.
214
+ * 2. AC status updates. Tie-break by larger feature number, then by AC id string.
215
+ * 3. Session terminal step markers.
216
+ * Returns null if none of the above yield a signal.
217
+ * @param {FeatureDiff[]} diffs
218
+ * @param {string|null|undefined} sessionStep
219
+ * @param {Array<{id: string}>} [_features] - currently unused, reserved for future reason enrichment
220
+ * @returns {Breakpoint|null}
221
+ */
222
+ function pickBreakpoint(diffs, sessionStep, _features) {
223
+ const safeDiffs = Array.isArray(diffs) ? diffs : [];
224
+
225
+ // 1. Feature-state transitions (highest priority)
226
+ const stateTransitions = safeDiffs.filter(d => d.type === 'state-transition');
227
+ if (stateTransitions.length > 0) {
228
+ // Sort: higher STATE_RANK wins, then larger feature number wins.
229
+ const sorted = [...stateTransitions].sort((a, b) => {
230
+ const rankA = STATE_RANK[a.to] !== undefined ? STATE_RANK[a.to] : -1;
231
+ const rankB = STATE_RANK[b.to] !== undefined ? STATE_RANK[b.to] : -1;
232
+ if (rankA !== rankB) return rankB - rankA;
233
+ return featureNumericId(b.featureId) - featureNumericId(a.featureId);
234
+ });
235
+ const winner = sorted[0];
236
+ // Render the from-state so the reason reads as a transition, not a bare end-state.
237
+ // First-time observations have from=null → omit the arrow to keep the message clean.
238
+ const reason = winner.from
239
+ ? `${winner.featureId} von ${winner.from} → ${winner.to}`
240
+ : `${winner.featureId} auf state=${winner.to}`;
241
+ return {
242
+ kind: 'feature-transition',
243
+ featureId: winner.featureId,
244
+ reason,
245
+ };
246
+ }
247
+
248
+ // 2. AC-level updates
249
+ const acUpdates = safeDiffs.filter(d => d.type === 'ac-status-update');
250
+ if (acUpdates.length > 0) {
251
+ const sorted = [...acUpdates].sort((a, b) => {
252
+ const fnumDiff = featureNumericId(b.featureId) - featureNumericId(a.featureId);
253
+ if (fnumDiff !== 0) return fnumDiff;
254
+ return String(b.acId || '').localeCompare(String(a.acId || ''));
255
+ });
256
+ const winner = sorted[0];
257
+ return {
258
+ kind: 'ac-update',
259
+ featureId: winner.featureId,
260
+ reason: `${winner.featureId}/${winner.acId} auf status=${winner.to}`,
261
+ };
262
+ }
263
+
264
+ // 3. Session step marker
265
+ if (sessionStep && TERMINAL_STEPS.has(sessionStep)) {
266
+ return {
267
+ kind: 'session-step',
268
+ reason: STEP_REASONS[sessionStep] || `Session-Step ${sessionStep}`,
269
+ };
270
+ }
271
+
272
+ return null;
273
+ }
274
+
275
+ // @cap-todo(ac:F-057/AC-4) analyze — main entry point. Reads session + feature map, computes plan.
276
+ // Deviation from brainstorm-spec on AC-4: /cap:save accepts a positional [name], not a --label flag.
277
+ // @cap-decision Deviated from F-057/AC-4: /cap:save takes a positional [name] arg, not --label; using
278
+ // "checkpoint-{feature_id}" as the name. If the breakpoint is session-step (no feature context), the
279
+ // label falls back to "checkpoint-session".
280
+ // @cap-todo(ac:F-057/AC-5) analyze returns a plan with breakpoint=null and the "Kein natürlicher
281
+ // Kontextbruch erkannt." message when nothing changed and no terminal step was reached.
282
+ // @cap-todo(ac:F-057/AC-6) analyze never mutates disk and never triggers /compact — it only proposes.
283
+ /**
284
+ * Analyze current session + feature map against the last persisted checkpoint snapshot.
285
+ * Pure function: does NOT mutate SESSION.json.
286
+ * @param {Object} sessionJson - Full session object (loaded via capSession.loadSession)
287
+ * @param {{features: Array}} featureMap - Feature map object (loaded via cap-feature-map.readFeatureMap)
288
+ * @returns {AnalyzeResult}
289
+ */
290
+ function analyze(sessionJson, featureMap) {
291
+ const session = sessionJson || {};
292
+ const features = (featureMap && Array.isArray(featureMap.features)) ? featureMap.features : [];
293
+
294
+ const prevSnapshot = session.lastCheckpointSnapshot || null;
295
+ const currentSnapshot = captureFeatureSnapshot(features);
296
+ const diffs = diffFeatureStates(prevSnapshot, features);
297
+ const breakpoint = pickBreakpoint(diffs, session.step || null, features);
298
+
299
+ if (!breakpoint) {
300
+ return {
301
+ breakpoint: null,
302
+ plan: {
303
+ shouldSave: false,
304
+ saveLabel: null,
305
+ message: 'Kein natürlicher Kontextbruch erkannt.',
306
+ },
307
+ currentSnapshot,
308
+ };
309
+ }
310
+
311
+ // Sanitize featureId before interpolating into the /cap:save label — the label
312
+ // propagates into a slash-command chain and must not carry shell-metachars even
313
+ // if a malformed FEATURE-MAP.md ever slipped past the feature-map parser.
314
+ const safeFeatureId = /^F-\d+$/.test(breakpoint.featureId || '') ? breakpoint.featureId : null;
315
+ const saveLabel = safeFeatureId ? `checkpoint-${safeFeatureId}` : 'checkpoint-session';
316
+
317
+ return {
318
+ breakpoint,
319
+ plan: {
320
+ shouldSave: true,
321
+ saveLabel,
322
+ message: `Jetzt /compact, weil ${breakpoint.reason}.`,
323
+ },
324
+ currentSnapshot,
325
+ };
326
+ }
327
+
328
+ // @cap-todo(ac:F-057/AC-4) applyCheckpoint — side-effect function, persists the current snapshot and
329
+ // timestamp to SESSION.json. Kept separate from analyze() to preserve the pure-function boundary.
330
+ /**
331
+ * Persist the checkpoint state to SESSION.json. The path argument is the project root.
332
+ *
333
+ * Verifies the post-condition by re-reading SESSION.json and asserting the
334
+ * timestamp round-tripped. An updateSession that silently fails (e.g. EACCES
335
+ * after a race with another hook) would otherwise leave lastCheckpointAt
336
+ * stale and the next run would emit duplicate checkpoints.
337
+ *
338
+ * @param {string} projectRoot - Absolute path to the project root containing .cap/SESSION.json
339
+ * @param {FeatureSnapshot} newSnapshot - Snapshot to persist
340
+ * @param {Date} [now] - Override timestamp (for deterministic testing). Defaults to new Date().
341
+ * @returns {Object} Updated session object
342
+ */
343
+ function applyCheckpoint(projectRoot, newSnapshot, now) {
344
+ if (typeof projectRoot !== 'string' || projectRoot.length === 0) {
345
+ throw new TypeError('applyCheckpoint: projectRoot must be a non-empty string');
346
+ }
347
+ const timestamp = (now instanceof Date ? now : new Date()).toISOString();
348
+ const snapshot = newSnapshot || { featureStates: {}, acStatuses: {} };
349
+ const updated = capSession.updateSession(projectRoot, {
350
+ lastCheckpointAt: timestamp,
351
+ lastCheckpointSnapshot: snapshot,
352
+ });
353
+
354
+ // FS post-condition: read back SESSION.json and confirm the write landed.
355
+ // AC-4 mandates persistence; a silent write failure must throw, not pass.
356
+ const readback = capSession.loadSession(projectRoot);
357
+ if (!readback || readback.lastCheckpointAt !== timestamp) {
358
+ throw new Error(
359
+ `applyCheckpoint: SESSION.json post-condition failed — expected lastCheckpointAt=${timestamp}, got ${readback && readback.lastCheckpointAt}`,
360
+ );
361
+ }
362
+ return updated;
363
+ }
364
+
365
+ /**
366
+ * Convenience wrapper that runs analyze() and applyCheckpoint() in a single
367
+ * call against a freshly-loaded session + feature map, eliminating the TOCTOU
368
+ * window where the orchestrator's two Node invocations might observe
369
+ * different FEATURE-MAP.md contents between analyze and persist.
370
+ *
371
+ * Orchestrators that cannot collapse both steps into one process can still
372
+ * use analyze() + applyCheckpoint() separately, but must pass
373
+ * result.currentSnapshot from the first call through to the second —
374
+ * never recompute.
375
+ *
376
+ * @param {string} projectRoot
377
+ * @param {{now?:Date, loadSession?:Function, readFeatureMap?:Function}} [opts]
378
+ * @returns {AnalyzeResult & {persisted:boolean}}
379
+ */
380
+ function analyzeAndApply(projectRoot, opts = {}) {
381
+ const capFeatureMap = require('./cap-feature-map.cjs');
382
+ const loadSession = opts.loadSession || capSession.loadSession;
383
+ const readFeatureMap = opts.readFeatureMap || capFeatureMap.readFeatureMap;
384
+
385
+ const session = loadSession(projectRoot);
386
+ // @cap-todo(ac:F-081/AC-4 iter:2) Migrated to {safe: true} opt-in to preserve CLI on duplicate-ID FEATURE-MAP.
387
+ // @cap-decision(F-081/iter2) Warn on parseError; continue with partial map for read-only display.
388
+ const featureMap = readFeatureMap(projectRoot, undefined, { safe: true });
389
+ if (featureMap && featureMap.parseError) {
390
+ console.warn('cap: checkpoint analyzeAndApply — duplicate feature ID detected, using partial map: ' + String(featureMap.parseError.message).trim());
391
+ }
392
+ const result = analyze(session, featureMap);
393
+
394
+ if (!result.breakpoint) {
395
+ return { ...result, persisted: false };
396
+ }
397
+
398
+ applyCheckpoint(projectRoot, result.currentSnapshot, opts.now);
399
+ return { ...result, persisted: true };
400
+ }
401
+
402
+ /**
403
+ * Helper: confirm SESSION.json exists and return its path. Used only in tests/integration flows.
404
+ * @param {string} projectRoot
405
+ * @returns {string}
406
+ */
407
+ function sessionPath(projectRoot) {
408
+ return path.join(projectRoot, '.cap', 'SESSION.json');
409
+ }
410
+
411
+ /**
412
+ * Helper: SESSION.json existence check — tiny utility for the orchestrator.
413
+ * @param {string} projectRoot
414
+ * @returns {boolean}
415
+ */
416
+ function hasSession(projectRoot) {
417
+ return fs.existsSync(sessionPath(projectRoot));
418
+ }
419
+
420
+ module.exports = {
421
+ TERMINAL_STEPS,
422
+ STATE_RANK,
423
+ captureFeatureSnapshot,
424
+ diffFeatureState,
425
+ diffFeatureACs,
426
+ diffFeatureStates,
427
+ pickBreakpoint,
428
+ analyze,
429
+ applyCheckpoint,
430
+ analyzeAndApply,
431
+ // Exposed for test/diagnostic use
432
+ sessionPath,
433
+ hasSession,
434
+ };