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.
- package/.claude-plugin/README.md +26 -0
- package/.claude-plugin/marketplace.json +24 -0
- package/.claude-plugin/plugin.json +24 -0
- package/LICENSE +21 -0
- package/README.ja-JP.md +834 -0
- package/README.ko-KR.md +823 -0
- package/README.md +806 -0
- package/README.pt-BR.md +452 -0
- package/README.zh-CN.md +800 -0
- package/agents/cap-architect.md +269 -0
- package/agents/cap-brainstormer.md +207 -0
- package/agents/cap-curator.md +276 -0
- package/agents/cap-debugger.md +365 -0
- package/agents/cap-designer.md +246 -0
- package/agents/cap-historian.md +464 -0
- package/agents/cap-migrator.md +291 -0
- package/agents/cap-prototyper.md +197 -0
- package/agents/cap-validator.md +308 -0
- package/bin/install.js +5433 -0
- package/cap/bin/cap-tools.cjs +853 -0
- package/cap/bin/lib/arc-scanner.cjs +344 -0
- package/cap/bin/lib/cap-affinity-engine.cjs +862 -0
- package/cap/bin/lib/cap-anchor.cjs +228 -0
- package/cap/bin/lib/cap-annotation-writer.cjs +340 -0
- package/cap/bin/lib/cap-checkpoint.cjs +434 -0
- package/cap/bin/lib/cap-cluster-detect.cjs +945 -0
- package/cap/bin/lib/cap-cluster-display.cjs +52 -0
- package/cap/bin/lib/cap-cluster-format.cjs +245 -0
- package/cap/bin/lib/cap-cluster-helpers.cjs +295 -0
- package/cap/bin/lib/cap-cluster-io.cjs +212 -0
- package/cap/bin/lib/cap-completeness.cjs +540 -0
- package/cap/bin/lib/cap-deps.cjs +583 -0
- package/cap/bin/lib/cap-design-families.cjs +332 -0
- package/cap/bin/lib/cap-design.cjs +966 -0
- package/cap/bin/lib/cap-divergence-detector.cjs +400 -0
- package/cap/bin/lib/cap-doctor.cjs +752 -0
- package/cap/bin/lib/cap-feature-map-internals.cjs +19 -0
- package/cap/bin/lib/cap-feature-map-migrate.cjs +335 -0
- package/cap/bin/lib/cap-feature-map-monorepo.cjs +885 -0
- package/cap/bin/lib/cap-feature-map-shard.cjs +315 -0
- package/cap/bin/lib/cap-feature-map.cjs +1943 -0
- package/cap/bin/lib/cap-fitness-score.cjs +1075 -0
- package/cap/bin/lib/cap-impact-analysis.cjs +652 -0
- package/cap/bin/lib/cap-learn-review.cjs +1072 -0
- package/cap/bin/lib/cap-learning-signals.cjs +627 -0
- package/cap/bin/lib/cap-loader.cjs +227 -0
- package/cap/bin/lib/cap-logger.cjs +57 -0
- package/cap/bin/lib/cap-memory-bridge.cjs +764 -0
- package/cap/bin/lib/cap-memory-confidence.cjs +452 -0
- package/cap/bin/lib/cap-memory-dir.cjs +987 -0
- package/cap/bin/lib/cap-memory-engine.cjs +698 -0
- package/cap/bin/lib/cap-memory-extends.cjs +398 -0
- package/cap/bin/lib/cap-memory-graph.cjs +790 -0
- package/cap/bin/lib/cap-memory-migrate.cjs +2015 -0
- package/cap/bin/lib/cap-memory-pin.cjs +183 -0
- package/cap/bin/lib/cap-memory-platform.cjs +490 -0
- package/cap/bin/lib/cap-memory-prune.cjs +707 -0
- package/cap/bin/lib/cap-memory-schema.cjs +812 -0
- package/cap/bin/lib/cap-migrate-tags.cjs +309 -0
- package/cap/bin/lib/cap-migrate.cjs +540 -0
- package/cap/bin/lib/cap-pattern-apply.cjs +1203 -0
- package/cap/bin/lib/cap-pattern-pipeline.cjs +1034 -0
- package/cap/bin/lib/cap-plugin-manifest.cjs +80 -0
- package/cap/bin/lib/cap-realtime-affinity.cjs +399 -0
- package/cap/bin/lib/cap-reconcile.cjs +570 -0
- package/cap/bin/lib/cap-research-gate.cjs +218 -0
- package/cap/bin/lib/cap-scope-filter.cjs +402 -0
- package/cap/bin/lib/cap-semantic-pipeline.cjs +1038 -0
- package/cap/bin/lib/cap-session-extract.cjs +987 -0
- package/cap/bin/lib/cap-session.cjs +445 -0
- package/cap/bin/lib/cap-snapshot-linkage.cjs +963 -0
- package/cap/bin/lib/cap-stack-docs.cjs +646 -0
- package/cap/bin/lib/cap-tag-observer.cjs +371 -0
- package/cap/bin/lib/cap-tag-scanner.cjs +1766 -0
- package/cap/bin/lib/cap-telemetry.cjs +466 -0
- package/cap/bin/lib/cap-test-audit.cjs +1438 -0
- package/cap/bin/lib/cap-thread-migrator.cjs +307 -0
- package/cap/bin/lib/cap-thread-synthesis.cjs +545 -0
- package/cap/bin/lib/cap-thread-tracker.cjs +519 -0
- package/cap/bin/lib/cap-trace.cjs +399 -0
- package/cap/bin/lib/cap-trust-mode.cjs +336 -0
- package/cap/bin/lib/cap-ui-design-editor.cjs +642 -0
- package/cap/bin/lib/cap-ui-mind-map.cjs +712 -0
- package/cap/bin/lib/cap-ui-thread-nav.cjs +693 -0
- package/cap/bin/lib/cap-ui.cjs +1245 -0
- package/cap/bin/lib/cap-upgrade.cjs +1028 -0
- package/cap/bin/lib/cli/arg-helpers.cjs +49 -0
- package/cap/bin/lib/cli/frontmatter-router.cjs +31 -0
- package/cap/bin/lib/cli/init-router.cjs +68 -0
- package/cap/bin/lib/cli/phase-router.cjs +102 -0
- package/cap/bin/lib/cli/state-router.cjs +61 -0
- package/cap/bin/lib/cli/template-router.cjs +37 -0
- package/cap/bin/lib/cli/uat-router.cjs +29 -0
- package/cap/bin/lib/cli/validation-router.cjs +26 -0
- package/cap/bin/lib/cli/verification-router.cjs +31 -0
- package/cap/bin/lib/cli/workstream-router.cjs +39 -0
- package/cap/bin/lib/commands.cjs +961 -0
- package/cap/bin/lib/config.cjs +467 -0
- package/cap/bin/lib/convention-reader.cjs +258 -0
- package/cap/bin/lib/core.cjs +1241 -0
- package/cap/bin/lib/feature-aggregator.cjs +423 -0
- package/cap/bin/lib/frontmatter.cjs +337 -0
- package/cap/bin/lib/init.cjs +1443 -0
- package/cap/bin/lib/manifest-generator.cjs +383 -0
- package/cap/bin/lib/milestone.cjs +253 -0
- package/cap/bin/lib/model-profiles.cjs +69 -0
- package/cap/bin/lib/monorepo-context.cjs +226 -0
- package/cap/bin/lib/monorepo-migrator.cjs +509 -0
- package/cap/bin/lib/phase.cjs +889 -0
- package/cap/bin/lib/profile-output.cjs +989 -0
- package/cap/bin/lib/profile-pipeline.cjs +540 -0
- package/cap/bin/lib/roadmap.cjs +330 -0
- package/cap/bin/lib/security.cjs +394 -0
- package/cap/bin/lib/session-manager.cjs +292 -0
- package/cap/bin/lib/skeleton-generator.cjs +179 -0
- package/cap/bin/lib/state.cjs +1032 -0
- package/cap/bin/lib/template.cjs +231 -0
- package/cap/bin/lib/test-detector.cjs +62 -0
- package/cap/bin/lib/uat.cjs +283 -0
- package/cap/bin/lib/verify.cjs +889 -0
- package/cap/bin/lib/workspace-detector.cjs +371 -0
- package/cap/bin/lib/workstream.cjs +492 -0
- package/cap/commands/gsd/workstreams.md +63 -0
- package/cap/references/arc-standard.md +315 -0
- package/cap/references/cap-agent-architecture.md +101 -0
- package/cap/references/cap-gitignore-template +9 -0
- package/cap/references/cap-zero-deps.md +158 -0
- package/cap/references/checkpoints.md +778 -0
- package/cap/references/continuation-format.md +249 -0
- package/cap/references/contract-test-templates.md +312 -0
- package/cap/references/feature-map-template.md +25 -0
- package/cap/references/git-integration.md +295 -0
- package/cap/references/git-planning-commit.md +38 -0
- package/cap/references/model-profiles.md +174 -0
- package/cap/references/phase-numbering.md +126 -0
- package/cap/references/planning-config.md +202 -0
- package/cap/references/property-test-templates.md +316 -0
- package/cap/references/security-test-templates.md +347 -0
- package/cap/references/session-template.json +8 -0
- package/cap/references/tdd.md +263 -0
- package/cap/references/user-profiling.md +681 -0
- package/cap/references/verification-patterns.md +612 -0
- package/cap/templates/UAT.md +265 -0
- package/cap/templates/claude-md.md +175 -0
- package/cap/templates/codebase/architecture.md +255 -0
- package/cap/templates/codebase/concerns.md +310 -0
- package/cap/templates/codebase/conventions.md +307 -0
- package/cap/templates/codebase/integrations.md +280 -0
- package/cap/templates/codebase/stack.md +186 -0
- package/cap/templates/codebase/structure.md +285 -0
- package/cap/templates/codebase/testing.md +480 -0
- package/cap/templates/config.json +44 -0
- package/cap/templates/context.md +352 -0
- package/cap/templates/continue-here.md +78 -0
- package/cap/templates/copilot-instructions.md +7 -0
- package/cap/templates/debug-subagent-prompt.md +91 -0
- package/cap/templates/discussion-log.md +63 -0
- package/cap/templates/milestone-archive.md +123 -0
- package/cap/templates/milestone.md +115 -0
- package/cap/templates/phase-prompt.md +610 -0
- package/cap/templates/planner-subagent-prompt.md +117 -0
- package/cap/templates/project.md +186 -0
- package/cap/templates/requirements.md +231 -0
- package/cap/templates/research-project/ARCHITECTURE.md +204 -0
- package/cap/templates/research-project/FEATURES.md +147 -0
- package/cap/templates/research-project/PITFALLS.md +200 -0
- package/cap/templates/research-project/STACK.md +120 -0
- package/cap/templates/research-project/SUMMARY.md +170 -0
- package/cap/templates/research.md +552 -0
- package/cap/templates/roadmap.md +202 -0
- package/cap/templates/state.md +176 -0
- package/cap/templates/summary.md +364 -0
- package/cap/templates/user-preferences.md +498 -0
- package/cap/templates/verification-report.md +322 -0
- package/cap/workflows/add-phase.md +112 -0
- package/cap/workflows/add-tests.md +351 -0
- package/cap/workflows/add-todo.md +158 -0
- package/cap/workflows/audit-milestone.md +340 -0
- package/cap/workflows/audit-uat.md +109 -0
- package/cap/workflows/autonomous.md +891 -0
- package/cap/workflows/check-todos.md +177 -0
- package/cap/workflows/cleanup.md +152 -0
- package/cap/workflows/complete-milestone.md +767 -0
- package/cap/workflows/diagnose-issues.md +231 -0
- package/cap/workflows/discovery-phase.md +289 -0
- package/cap/workflows/discuss-phase-assumptions.md +653 -0
- package/cap/workflows/discuss-phase.md +1049 -0
- package/cap/workflows/do.md +104 -0
- package/cap/workflows/execute-phase.md +846 -0
- package/cap/workflows/execute-plan.md +514 -0
- package/cap/workflows/fast.md +105 -0
- package/cap/workflows/forensics.md +265 -0
- package/cap/workflows/health.md +181 -0
- package/cap/workflows/help.md +660 -0
- package/cap/workflows/insert-phase.md +130 -0
- package/cap/workflows/list-phase-assumptions.md +178 -0
- package/cap/workflows/list-workspaces.md +56 -0
- package/cap/workflows/manager.md +362 -0
- package/cap/workflows/map-codebase.md +377 -0
- package/cap/workflows/milestone-summary.md +223 -0
- package/cap/workflows/new-milestone.md +486 -0
- package/cap/workflows/new-project.md +1250 -0
- package/cap/workflows/new-workspace.md +237 -0
- package/cap/workflows/next.md +97 -0
- package/cap/workflows/node-repair.md +92 -0
- package/cap/workflows/note.md +156 -0
- package/cap/workflows/pause-work.md +176 -0
- package/cap/workflows/plan-milestone-gaps.md +273 -0
- package/cap/workflows/plan-phase.md +857 -0
- package/cap/workflows/plant-seed.md +169 -0
- package/cap/workflows/pr-branch.md +129 -0
- package/cap/workflows/profile-user.md +449 -0
- package/cap/workflows/progress.md +507 -0
- package/cap/workflows/quick.md +757 -0
- package/cap/workflows/remove-phase.md +155 -0
- package/cap/workflows/remove-workspace.md +90 -0
- package/cap/workflows/research-phase.md +82 -0
- package/cap/workflows/resume-project.md +326 -0
- package/cap/workflows/review.md +228 -0
- package/cap/workflows/session-report.md +146 -0
- package/cap/workflows/settings.md +283 -0
- package/cap/workflows/ship.md +228 -0
- package/cap/workflows/stats.md +60 -0
- package/cap/workflows/transition.md +671 -0
- package/cap/workflows/ui-phase.md +298 -0
- package/cap/workflows/ui-review.md +161 -0
- package/cap/workflows/update.md +323 -0
- package/cap/workflows/validate-phase.md +170 -0
- package/cap/workflows/verify-phase.md +254 -0
- package/cap/workflows/verify-work.md +637 -0
- package/commands/cap/annotate.md +165 -0
- package/commands/cap/brainstorm.md +393 -0
- package/commands/cap/checkpoint.md +106 -0
- package/commands/cap/completeness.md +94 -0
- package/commands/cap/continue.md +72 -0
- package/commands/cap/debug.md +588 -0
- package/commands/cap/deps.md +169 -0
- package/commands/cap/design.md +479 -0
- package/commands/cap/init.md +354 -0
- package/commands/cap/iterate.md +249 -0
- package/commands/cap/learn.md +459 -0
- package/commands/cap/memory.md +275 -0
- package/commands/cap/migrate-feature-map.md +91 -0
- package/commands/cap/migrate-memory.md +108 -0
- package/commands/cap/migrate-tags.md +91 -0
- package/commands/cap/migrate.md +131 -0
- package/commands/cap/prototype.md +510 -0
- package/commands/cap/reconcile.md +121 -0
- package/commands/cap/review.md +360 -0
- package/commands/cap/save.md +72 -0
- package/commands/cap/scan.md +404 -0
- package/commands/cap/start.md +356 -0
- package/commands/cap/status.md +118 -0
- package/commands/cap/test-audit.md +262 -0
- package/commands/cap/test.md +394 -0
- package/commands/cap/trace.md +133 -0
- package/commands/cap/ui.md +167 -0
- package/hooks/dist/cap-check-update.js +115 -0
- package/hooks/dist/cap-context-monitor.js +185 -0
- package/hooks/dist/cap-learn-review-hook.js +114 -0
- package/hooks/dist/cap-learning-hook.js +192 -0
- package/hooks/dist/cap-memory.js +299 -0
- package/hooks/dist/cap-prompt-guard.js +97 -0
- package/hooks/dist/cap-statusline.js +157 -0
- package/hooks/dist/cap-tag-observer.js +115 -0
- package/hooks/dist/cap-version-check.js +112 -0
- package/hooks/dist/cap-workflow-guard.js +175 -0
- package/hooks/hooks.json +55 -0
- package/package.json +58 -0
- package/scripts/base64-scan.sh +262 -0
- package/scripts/build-hooks.js +93 -0
- package/scripts/cap-removal-checklist.md +202 -0
- package/scripts/prompt-injection-scan.sh +199 -0
- package/scripts/run-tests.cjs +181 -0
- 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
|
+
};
|