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,570 @@
|
|
|
1
|
+
// @cap-context CAP v2.0 status drift reconciliation -- one-shot tool that cleans up historical
|
|
2
|
+
// AC-status drift introduced before F-041 (parser bug) and F-042 (state-transition propagation)
|
|
3
|
+
// were merged. Combines two reconciliation phases plus a verification phase.
|
|
4
|
+
// @cap-decision Dry-run is the DEFAULT mode for safety. The reconciliation rewrites FEATURE-MAP.md
|
|
5
|
+
// in place once the user passes --apply, so a no-op preview that shows every proposed change is the
|
|
6
|
+
// correct safety net. Confirmation prompt is a second guard before any write.
|
|
7
|
+
// @cap-decision Phase 2 heuristic for state-from-code-presence is two-step: (1) collect impl files
|
|
8
|
+
// from @cap-feature tags grouped by feature ID, then (2) for each impl file check the filesystem
|
|
9
|
+
// for a sibling test file (basename + .test.<ext>) under tests/. If both impl and test exist
|
|
10
|
+
// propose 'tested', if only impl exists propose 'prototyped'. Filesystem check is required because
|
|
11
|
+
// test files in this project do NOT carry @cap-feature tags (test framework detects implementation
|
|
12
|
+
// via filename convention, not annotations). Lifecycle definition is in CLAUDE.md
|
|
13
|
+
// (planned -> prototyped -> tested -> shipped).
|
|
14
|
+
// @cap-decision Mirror-directory dedup: the project keeps cap/bin/lib/*.cjs mirrored at
|
|
15
|
+
// .claude/cap/bin/lib/*.cjs. The tag scan returns BOTH copies. Phase 2 dedupes by basename so a
|
|
16
|
+
// feature with one canonical impl + one mirror does not appear as "two impl files".
|
|
17
|
+
// @cap-decision F-043 (this feature) is excluded from Phase 2 because the user explicitly requires
|
|
18
|
+
// it to remain in the developer's hands -- promoting F-043 to prototyped via its own reconciliation
|
|
19
|
+
// run would be a circular self-promotion that masks intent.
|
|
20
|
+
// @cap-constraint Zero external dependencies. Uses only Node.js built-ins (fs, path, readline).
|
|
21
|
+
|
|
22
|
+
'use strict';
|
|
23
|
+
|
|
24
|
+
// @cap-feature(feature:F-043) Reconcile Status Drift in Existing Feature Map -- one-shot module that
|
|
25
|
+
// proposes, prints, and (with --apply) commits the AC-status and feature-state corrections needed
|
|
26
|
+
// to bring FEATURE-MAP.md back in sync with the implementation reality.
|
|
27
|
+
|
|
28
|
+
const fs = require('node:fs');
|
|
29
|
+
const path = require('node:path');
|
|
30
|
+
|
|
31
|
+
const featureMap = require('./cap-feature-map.cjs');
|
|
32
|
+
const tagScanner = require('./cap-tag-scanner.cjs');
|
|
33
|
+
|
|
34
|
+
const AUDIT_LOG_RELATIVE = path.join('.cap', 'memory', 'reconciliation-2026-04.md');
|
|
35
|
+
|
|
36
|
+
// Test-file detection is intentionally simple — basename suffix check. Avoids depending on
|
|
37
|
+
// test-detector.cjs (which uses package.json heuristics) and keeps Phase 2 self-contained.
|
|
38
|
+
const TEST_FILE_SUFFIXES = ['.test.cjs', '.test.js', '.test.mjs', '.test.ts', '.test.tsx'];
|
|
39
|
+
|
|
40
|
+
// Test directory candidates checked when probing the filesystem for a sibling test file.
|
|
41
|
+
// Order matters: the most common location wins.
|
|
42
|
+
const TEST_DIR_CANDIDATES = ['tests', 'test', '__tests__'];
|
|
43
|
+
|
|
44
|
+
// Mirror directory prefix that is treated as a duplicate of the canonical cap/ tree.
|
|
45
|
+
// Files seen here are folded into their cap/-rooted counterpart for dedup purposes.
|
|
46
|
+
const MIRROR_PREFIX = '.claude' + path.sep;
|
|
47
|
+
|
|
48
|
+
// Features explicitly excluded from Phase 2 (state-from-code-presence). F-043 is the
|
|
49
|
+
// reconciliation tool itself -- promoting it via its own reconciliation run would be a
|
|
50
|
+
// circular self-promotion that masks the developer's intent for the feature.
|
|
51
|
+
const PHASE2_EXCLUDED_FEATURES = new Set(['F-043']);
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @typedef {Object} AcChange
|
|
55
|
+
* @property {string} acId - AC identifier (e.g., "AC-1")
|
|
56
|
+
* @property {string} from - Previous AC status
|
|
57
|
+
* @property {string} to - New AC status
|
|
58
|
+
*/
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* @typedef {Object} Phase1Entry
|
|
62
|
+
* @property {string} featureId - Feature identifier (e.g., "F-019")
|
|
63
|
+
* @property {string} state - Feature state (always 'tested' or 'shipped')
|
|
64
|
+
* @property {AcChange[]} acChanges - Per-AC promotions
|
|
65
|
+
*/
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* @typedef {Object} Phase2Entry
|
|
69
|
+
* @property {string} featureId - Feature identifier
|
|
70
|
+
* @property {string} fromState - Previous feature state (always 'planned')
|
|
71
|
+
* @property {string} toState - Proposed feature state ('prototyped' or 'tested')
|
|
72
|
+
* @property {string[]} implFiles - Implementation files detected via tag scan
|
|
73
|
+
* @property {string[]} testFiles - Test files detected via tag scan
|
|
74
|
+
* @property {AcChange[]} propagatedAcChanges - AC promotions that follow from the state change
|
|
75
|
+
*/
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* @typedef {Object} ReconciliationPlan
|
|
79
|
+
* @property {Phase1Entry[]} phase1 - AC-status promotions for tested/shipped features
|
|
80
|
+
* @property {Phase2Entry[]} phase2 - Feature-state updates for planned features with code
|
|
81
|
+
* @property {number} preDriftCount - Drift count before reconciliation
|
|
82
|
+
* @property {number} totalAcPromotions - Total AC mutations across both phases
|
|
83
|
+
* @property {number} totalStateUpdates - Total feature-state mutations (Phase 2 only)
|
|
84
|
+
* @property {number} totalChanges - totalAcPromotions + totalStateUpdates
|
|
85
|
+
* @property {string} auditLogPath - Relative path to audit log target
|
|
86
|
+
*/
|
|
87
|
+
|
|
88
|
+
// @cap-api isTestFile(relativePath) -- internal helper for Phase 2 detection.
|
|
89
|
+
// Exported for the test suite so the heuristic can be exercised in isolation.
|
|
90
|
+
/**
|
|
91
|
+
* @param {string} filePath - Relative or absolute file path
|
|
92
|
+
* @returns {boolean}
|
|
93
|
+
*/
|
|
94
|
+
function isTestFile(filePath) {
|
|
95
|
+
if (!filePath) return false;
|
|
96
|
+
const lower = filePath.toLowerCase();
|
|
97
|
+
return TEST_FILE_SUFFIXES.some(suffix => lower.endsWith(suffix));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// @cap-api canonicalizePath(filePath) -- folds .claude/ mirror paths into their cap/ counterparts
|
|
101
|
+
// so dedup-by-canonical-path collapses both copies into one entry.
|
|
102
|
+
/**
|
|
103
|
+
* @param {string} filePath - Relative file path from project root
|
|
104
|
+
* @returns {string}
|
|
105
|
+
*/
|
|
106
|
+
function canonicalizePath(filePath) {
|
|
107
|
+
if (!filePath) return filePath;
|
|
108
|
+
if (filePath.startsWith(MIRROR_PREFIX)) {
|
|
109
|
+
return filePath.slice(MIRROR_PREFIX.length);
|
|
110
|
+
}
|
|
111
|
+
return filePath;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// @cap-api groupTagsByFeatureFiles(tags) -- aggregates @cap-feature tags into per-feature impl
|
|
115
|
+
// file sets, deduped by canonical path so cap/ + .claude/cap/ mirror pairs collapse.
|
|
116
|
+
// Returns: Object<featureId, { impl: string[] }>
|
|
117
|
+
// @cap-decision Only @cap-feature tags are considered for Phase 2 (not @cap-todo). Implementation
|
|
118
|
+
// presence is signalled by an explicit feature-tagged file; @cap-todo references are too lossy
|
|
119
|
+
// (an AC reference in a test alone shouldn't promote the feature past 'planned').
|
|
120
|
+
// @cap-decision Test-file presence is NOT inferred from tags (test files in this project are
|
|
121
|
+
// untagged). Use detectTestFileForImpl() instead, which checks the filesystem for sibling
|
|
122
|
+
// test/<basename>.test.<ext> files.
|
|
123
|
+
/**
|
|
124
|
+
* @param {import('./cap-tag-scanner.cjs').CapTag[]} tags
|
|
125
|
+
* @returns {Object<string, { impl: string[] }>}
|
|
126
|
+
*/
|
|
127
|
+
function groupTagsByFeatureFiles(tags) {
|
|
128
|
+
const groups = {};
|
|
129
|
+
const seen = new Set(); // dedupe by featureId+canonicalPath pair
|
|
130
|
+
for (const tag of tags) {
|
|
131
|
+
if (tag.type !== 'feature') continue;
|
|
132
|
+
const featureId = tag.metadata && tag.metadata.feature;
|
|
133
|
+
if (!featureId) continue;
|
|
134
|
+
if (isTestFile(tag.file)) continue; // test files are not impl-presence signals
|
|
135
|
+
const canonical = canonicalizePath(tag.file);
|
|
136
|
+
const key = `${featureId}::${canonical}`;
|
|
137
|
+
if (seen.has(key)) continue;
|
|
138
|
+
seen.add(key);
|
|
139
|
+
if (!groups[featureId]) groups[featureId] = { impl: [] };
|
|
140
|
+
groups[featureId].impl.push(canonical);
|
|
141
|
+
}
|
|
142
|
+
return groups;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// @cap-api detectTestFileForImpl(projectRoot, implPath) -- filesystem probe for a sibling test
|
|
146
|
+
// file matching the implementation file's basename. Returns the relative test path if found.
|
|
147
|
+
// @cap-decision Probes tests/, test/, __tests__/ at the project root in that order. The CAP
|
|
148
|
+
// project uses tests/ (per CLAUDE.md), but the probe is generalized so the reconciler stays
|
|
149
|
+
// usable in projects following other conventions.
|
|
150
|
+
/**
|
|
151
|
+
* @param {string} projectRoot - Absolute path to project root
|
|
152
|
+
* @param {string} implPath - Canonical (cap/-rooted) impl file path
|
|
153
|
+
* @returns {string|null} - Relative path to the test file, or null if none found
|
|
154
|
+
*/
|
|
155
|
+
function detectTestFileForImpl(projectRoot, implPath) {
|
|
156
|
+
if (!implPath) return null;
|
|
157
|
+
const ext = path.extname(implPath); // e.g. '.cjs'
|
|
158
|
+
const baseName = path.basename(implPath, ext); // e.g. 'cap-memory-engine'
|
|
159
|
+
// Try test extensions in the same family first (cjs->cjs, ts->ts, ...) then fall back to others.
|
|
160
|
+
const matchingSuffix = TEST_FILE_SUFFIXES.find(s => s.endsWith(ext));
|
|
161
|
+
const candidates = matchingSuffix
|
|
162
|
+
? [matchingSuffix, ...TEST_FILE_SUFFIXES.filter(s => s !== matchingSuffix)]
|
|
163
|
+
: TEST_FILE_SUFFIXES;
|
|
164
|
+
|
|
165
|
+
for (const dir of TEST_DIR_CANDIDATES) {
|
|
166
|
+
for (const suffix of candidates) {
|
|
167
|
+
const rel = path.join(dir, baseName + suffix);
|
|
168
|
+
const abs = path.join(projectRoot, rel);
|
|
169
|
+
if (fs.existsSync(abs)) {
|
|
170
|
+
return rel;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// @cap-api planReconciliation(projectRoot) -- pure planner that returns the structured plan
|
|
178
|
+
// without writing anything. Safe to call repeatedly; the result is the input to formatPlan
|
|
179
|
+
// and executeReconciliation.
|
|
180
|
+
// @cap-todo(ac:F-043/AC-1) Phase 1 scans every drifting feature reported by detectDrift and
|
|
181
|
+
// proposes promoting each pending AC to 'tested'. Covers F-019..F-026, F-036..F-040, F-041
|
|
182
|
+
// in the live FEATURE-MAP.md (and any other drifting feature added later — the planner is
|
|
183
|
+
// derived from detectDrift, not a hard-coded ID list, so it stays correct as new drift appears).
|
|
184
|
+
// @cap-todo(ac:F-043/AC-3) Phase 2 walks features still in 'planned' state, intersects their
|
|
185
|
+
// IDs with @cap-feature tag scan results, and proposes prototyped/tested based on impl + test
|
|
186
|
+
// file presence. F-027/F-028/F-029/F-034 are the immediate beneficiaries.
|
|
187
|
+
/**
|
|
188
|
+
* @param {string} projectRoot - Absolute path to project root
|
|
189
|
+
* @param {string|null} [appPath=null] - Relative app path for monorepo scoping
|
|
190
|
+
* @returns {ReconciliationPlan}
|
|
191
|
+
*/
|
|
192
|
+
function planReconciliation(projectRoot, appPath) {
|
|
193
|
+
// @cap-todo(ac:F-081/AC-4 iter:2) Migrated to {safe: true} opt-in to preserve CLI on duplicate-ID FEATURE-MAP.
|
|
194
|
+
// @cap-decision(F-081/iter2) Warn on parseError; continue with partial map for read-only display.
|
|
195
|
+
const fm = featureMap.readFeatureMap(projectRoot, appPath, { safe: true });
|
|
196
|
+
if (fm && fm.parseError) {
|
|
197
|
+
console.warn('cap: reconcile — duplicate feature ID detected, plan uses partial map: ' + String(fm.parseError.message).trim());
|
|
198
|
+
}
|
|
199
|
+
const drift = featureMap.detectDrift(projectRoot, appPath);
|
|
200
|
+
|
|
201
|
+
// Phase 1: AC-status promotions for shipped/tested features that still have pending ACs.
|
|
202
|
+
const phase1 = [];
|
|
203
|
+
let totalAcPromotions = 0;
|
|
204
|
+
for (const driftEntry of drift.features) {
|
|
205
|
+
const acChanges = driftEntry.pendingAcs.map(ac => ({
|
|
206
|
+
acId: ac.id,
|
|
207
|
+
from: 'pending',
|
|
208
|
+
to: 'tested',
|
|
209
|
+
}));
|
|
210
|
+
if (acChanges.length === 0) continue;
|
|
211
|
+
totalAcPromotions += acChanges.length;
|
|
212
|
+
phase1.push({
|
|
213
|
+
featureId: driftEntry.id,
|
|
214
|
+
state: driftEntry.state,
|
|
215
|
+
acChanges,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Phase 2: feature-state updates for planned features whose code presence implies progress.
|
|
220
|
+
// @cap-todo(ac:F-043/AC-3) Tag scanner is the source of truth for impl presence; the filesystem
|
|
221
|
+
// is the source of truth for test presence (test files in this project do not carry tags).
|
|
222
|
+
const tagScanRoot = appPath ? path.join(projectRoot, appPath) : projectRoot;
|
|
223
|
+
const tags = tagScanner.scanDirectory(tagScanRoot, { projectRoot });
|
|
224
|
+
const featureFiles = groupTagsByFeatureFiles(tags);
|
|
225
|
+
|
|
226
|
+
const phase2 = [];
|
|
227
|
+
let totalStateUpdates = 0;
|
|
228
|
+
for (const feature of fm.features) {
|
|
229
|
+
if (feature.state !== 'planned') continue;
|
|
230
|
+
if (PHASE2_EXCLUDED_FEATURES.has(feature.id)) continue;
|
|
231
|
+
const fileGroups = featureFiles[feature.id];
|
|
232
|
+
if (!fileGroups || fileGroups.impl.length === 0) continue;
|
|
233
|
+
const implFiles = fileGroups.impl;
|
|
234
|
+
|
|
235
|
+
// Probe the filesystem for sibling test files.
|
|
236
|
+
const testFiles = [];
|
|
237
|
+
const seenTests = new Set();
|
|
238
|
+
for (const impl of implFiles) {
|
|
239
|
+
const test = detectTestFileForImpl(projectRoot, impl);
|
|
240
|
+
if (test && !seenTests.has(test)) {
|
|
241
|
+
seenTests.add(test);
|
|
242
|
+
testFiles.push(test);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// @cap-decision Heuristic: impl + at least one test => 'tested'; impl-only => 'prototyped'.
|
|
247
|
+
const toState = testFiles.length > 0 ? 'tested' : 'prototyped';
|
|
248
|
+
|
|
249
|
+
// Compute propagated AC changes that updateFeatureState would apply when toState === 'tested'.
|
|
250
|
+
// updateFeatureState promotes pending|prototyped -> tested only on the 'tested' transition.
|
|
251
|
+
const propagatedAcChanges = [];
|
|
252
|
+
if (toState === 'tested') {
|
|
253
|
+
for (const ac of feature.acs) {
|
|
254
|
+
if (ac.status === 'pending' || ac.status === 'prototyped') {
|
|
255
|
+
propagatedAcChanges.push({
|
|
256
|
+
acId: ac.id,
|
|
257
|
+
from: ac.status,
|
|
258
|
+
to: 'tested',
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
totalStateUpdates += 1;
|
|
265
|
+
totalAcPromotions += propagatedAcChanges.length;
|
|
266
|
+
|
|
267
|
+
phase2.push({
|
|
268
|
+
featureId: feature.id,
|
|
269
|
+
fromState: feature.state,
|
|
270
|
+
toState,
|
|
271
|
+
implFiles,
|
|
272
|
+
testFiles,
|
|
273
|
+
propagatedAcChanges,
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return {
|
|
278
|
+
phase1,
|
|
279
|
+
phase2,
|
|
280
|
+
preDriftCount: drift.driftCount,
|
|
281
|
+
totalAcPromotions,
|
|
282
|
+
totalStateUpdates,
|
|
283
|
+
totalChanges: totalAcPromotions + totalStateUpdates,
|
|
284
|
+
auditLogPath: AUDIT_LOG_RELATIVE,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// @cap-api formatPlan(plan) -- markdown-friendly preview of the plan, used by --dry-run output.
|
|
289
|
+
// Pure function: input plan, output string. No I/O.
|
|
290
|
+
/**
|
|
291
|
+
* @param {ReconciliationPlan} plan
|
|
292
|
+
* @returns {string}
|
|
293
|
+
*/
|
|
294
|
+
function formatPlan(plan) {
|
|
295
|
+
if (!plan) return 'Status Drift Reconciliation -- no plan available.';
|
|
296
|
+
|
|
297
|
+
const lines = [];
|
|
298
|
+
lines.push('Status Drift Reconciliation -- Dry Run');
|
|
299
|
+
lines.push('');
|
|
300
|
+
|
|
301
|
+
lines.push(`Phase 1 -- AC promotion (${plan.phase1.length} features):`);
|
|
302
|
+
if (plan.phase1.length === 0) {
|
|
303
|
+
lines.push(' (no AC-status drift detected)');
|
|
304
|
+
} else {
|
|
305
|
+
for (const entry of plan.phase1) {
|
|
306
|
+
lines.push(` ${entry.featureId} [${entry.state}]: ${entry.acChanges.length} ACs pending -> tested`);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
lines.push('');
|
|
310
|
+
|
|
311
|
+
lines.push(`Phase 2 -- Feature state from code presence (${plan.phase2.length} features):`);
|
|
312
|
+
if (plan.phase2.length === 0) {
|
|
313
|
+
lines.push(' (no planned features with detected implementation code)');
|
|
314
|
+
} else {
|
|
315
|
+
for (const entry of plan.phase2) {
|
|
316
|
+
const implPart = entry.implFiles.length > 0
|
|
317
|
+
? `impl: ${entry.implFiles.map(f => path.basename(f)).join(', ')}`
|
|
318
|
+
: 'no impl';
|
|
319
|
+
const testPart = entry.testFiles.length > 0
|
|
320
|
+
? `test: ${entry.testFiles.map(f => path.basename(f)).join(', ')}`
|
|
321
|
+
: 'no test';
|
|
322
|
+
lines.push(` ${entry.featureId} ${entry.fromState} -> ${entry.toState} (${implPart}, ${testPart})`);
|
|
323
|
+
if (entry.propagatedAcChanges.length > 0) {
|
|
324
|
+
lines.push(` Propagates ${entry.propagatedAcChanges.length} AC promotions to 'tested'`);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
lines.push('');
|
|
329
|
+
|
|
330
|
+
lines.push('Phase 3 -- Post-reconciliation drift check:');
|
|
331
|
+
lines.push(' Would result in driftCount: 0 (verified after --apply)');
|
|
332
|
+
lines.push('');
|
|
333
|
+
|
|
334
|
+
lines.push(`Total proposed changes: ${plan.totalChanges} (${plan.totalAcPromotions} AC promotions + ${plan.totalStateUpdates} state updates)`);
|
|
335
|
+
lines.push(`Audit log target: ${plan.auditLogPath}`);
|
|
336
|
+
lines.push('');
|
|
337
|
+
lines.push('Run with --apply to commit changes.');
|
|
338
|
+
|
|
339
|
+
return lines.join('\n');
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// @cap-api lifecyclePath(from, to) -- returns the ordered list of single-step transitions that
|
|
343
|
+
// take a feature from `from` to `to`, or null if no path exists. Used by executeReconciliation
|
|
344
|
+
// to honour updateFeatureState's strict one-hop contract.
|
|
345
|
+
// @cap-decision Hard-codes the canonical lifecycle (planned -> prototyped -> tested -> shipped).
|
|
346
|
+
// Mirroring the order in cap-feature-map.STATE_TRANSITIONS would couple the two modules through
|
|
347
|
+
// graph-walking; the lifecycle is short and stable, so a literal list is clearer.
|
|
348
|
+
/**
|
|
349
|
+
* @param {string} from - Source lifecycle state
|
|
350
|
+
* @param {string} to - Target lifecycle state
|
|
351
|
+
* @returns {string[]|null} - Ordered transitions (excluding `from`), or null if `to` precedes `from`
|
|
352
|
+
*/
|
|
353
|
+
function lifecyclePath(from, to) {
|
|
354
|
+
const order = ['planned', 'prototyped', 'tested', 'shipped'];
|
|
355
|
+
const fromIdx = order.indexOf(from);
|
|
356
|
+
const toIdx = order.indexOf(to);
|
|
357
|
+
if (fromIdx === -1 || toIdx === -1) return null;
|
|
358
|
+
if (toIdx < fromIdx) return null;
|
|
359
|
+
if (toIdx === fromIdx) return [];
|
|
360
|
+
return order.slice(fromIdx + 1, toIdx + 1);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// @cap-api executeReconciliation(projectRoot, plan) -- mutates FEATURE-MAP.md per the plan.
|
|
364
|
+
// Side effects: rewrites FEATURE-MAP.md, writes audit log, runs detectDrift verification.
|
|
365
|
+
// Returns: { success, postDriftCount, auditLogPath, error? }
|
|
366
|
+
// @cap-todo(ac:F-043/AC-2) Caller is responsible for confirming with the user before invoking this
|
|
367
|
+
// function. The function itself does NOT prompt -- that is the CLI orchestrator's job (see
|
|
368
|
+
// commands/cap/reconcile.md). This separation keeps the module pure and unit-testable.
|
|
369
|
+
// @cap-todo(ac:F-043/AC-4) Audit log emission is part of execute, not plan -- the file content
|
|
370
|
+
// records the actual changes that were committed (with timestamp), not a hypothetical preview.
|
|
371
|
+
// @cap-todo(ac:F-043/AC-5) Final detectDrift call validates that the plan brought drift to zero.
|
|
372
|
+
// If drift remains the function reports the failure (does NOT roll back, since FEATURE-MAP.md
|
|
373
|
+
// is already source-controlled and the user can revert via git).
|
|
374
|
+
// @cap-risk Partial-failure rollback is NOT attempted. Each setAcStatus / updateFeatureState call
|
|
375
|
+
// writes to disk. If the process is killed mid-execution the Feature Map will be partially
|
|
376
|
+
// reconciled. Mitigation: git makes recovery one `git checkout -- FEATURE-MAP.md` away, and the
|
|
377
|
+
// audit log records exactly what was applied before the crash.
|
|
378
|
+
/**
|
|
379
|
+
* @param {string} projectRoot - Absolute path to project root
|
|
380
|
+
* @param {ReconciliationPlan} plan - Plan returned by planReconciliation
|
|
381
|
+
* @param {Object} [options]
|
|
382
|
+
* @param {string|null} [options.appPath=null] - Relative app path
|
|
383
|
+
* @param {boolean} [options.skipAuditLog=false] - Suppress audit log emission (used by tests)
|
|
384
|
+
* @param {string} [options.timestamp] - Override timestamp (used by tests for determinism)
|
|
385
|
+
* @returns {{ success: boolean, postDriftCount: number, auditLogPath: string|null, error?: string }}
|
|
386
|
+
*/
|
|
387
|
+
function executeReconciliation(projectRoot, plan, options = {}) {
|
|
388
|
+
const appPath = options.appPath || null;
|
|
389
|
+
const skipAuditLog = Boolean(options.skipAuditLog);
|
|
390
|
+
const timestamp = options.timestamp || new Date().toISOString();
|
|
391
|
+
|
|
392
|
+
if (!plan) {
|
|
393
|
+
return { success: false, postDriftCount: -1, auditLogPath: null, error: 'No plan provided' };
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Phase 1: per-AC promotions via setAcStatus.
|
|
397
|
+
for (const entry of plan.phase1) {
|
|
398
|
+
for (const change of entry.acChanges) {
|
|
399
|
+
const ok = featureMap.setAcStatus(projectRoot, entry.featureId, change.acId, change.to, appPath);
|
|
400
|
+
if (!ok) {
|
|
401
|
+
return {
|
|
402
|
+
success: false,
|
|
403
|
+
postDriftCount: -1,
|
|
404
|
+
auditLogPath: null,
|
|
405
|
+
error: `setAcStatus failed for ${entry.featureId}/${change.acId}`,
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Phase 2: feature-state transitions via updateFeatureState. Because the lifecycle only
|
|
412
|
+
// permits one-step transitions (planned -> prototyped -> tested -> shipped), we step through
|
|
413
|
+
// intermediate states when the target is more than one hop away. AC propagation happens
|
|
414
|
+
// automatically on the 'tested' hop.
|
|
415
|
+
// @cap-todo(ac:F-043/AC-3) Honour the strict one-step state transition contract enforced
|
|
416
|
+
// by updateFeatureState by walking the lifecycle path explicitly.
|
|
417
|
+
for (const entry of plan.phase2) {
|
|
418
|
+
const path2 = lifecyclePath(entry.fromState, entry.toState);
|
|
419
|
+
if (path2 === null) {
|
|
420
|
+
return {
|
|
421
|
+
success: false,
|
|
422
|
+
postDriftCount: -1,
|
|
423
|
+
auditLogPath: null,
|
|
424
|
+
error: `no lifecycle path from ${entry.fromState} to ${entry.toState} for ${entry.featureId}`,
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
for (const nextState of path2) {
|
|
428
|
+
const ok = featureMap.updateFeatureState(projectRoot, entry.featureId, nextState, appPath);
|
|
429
|
+
if (!ok) {
|
|
430
|
+
return {
|
|
431
|
+
success: false,
|
|
432
|
+
postDriftCount: -1,
|
|
433
|
+
auditLogPath: null,
|
|
434
|
+
error: `updateFeatureState failed for ${entry.featureId} -> ${nextState}`,
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// Phase 3: verify drift is now zero.
|
|
441
|
+
const postDrift = featureMap.detectDrift(projectRoot, appPath);
|
|
442
|
+
|
|
443
|
+
// Audit log written even if verification fails — the user needs the record either way.
|
|
444
|
+
let auditLogPath = null;
|
|
445
|
+
if (!skipAuditLog) {
|
|
446
|
+
try {
|
|
447
|
+
auditLogPath = writeAuditLog(projectRoot, plan, postDrift.driftCount, timestamp);
|
|
448
|
+
} catch (e) {
|
|
449
|
+
return {
|
|
450
|
+
success: false,
|
|
451
|
+
postDriftCount: postDrift.driftCount,
|
|
452
|
+
auditLogPath: null,
|
|
453
|
+
error: `audit log write failed: ${e.message}`,
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
if (postDrift.driftCount > 0) {
|
|
459
|
+
return {
|
|
460
|
+
success: false,
|
|
461
|
+
postDriftCount: postDrift.driftCount,
|
|
462
|
+
auditLogPath,
|
|
463
|
+
error: `Reconciliation incomplete: ${postDrift.driftCount} drift entries remain`,
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
return {
|
|
468
|
+
success: true,
|
|
469
|
+
postDriftCount: 0,
|
|
470
|
+
auditLogPath,
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// @cap-api writeAuditLog(projectRoot, plan, postDriftCount, timestamp) -- emits the markdown
|
|
475
|
+
// audit log to .cap/memory/reconciliation-2026-04.md.
|
|
476
|
+
// @cap-decision Audit log file name is fixed (reconciliation-2026-04.md) per the AC-4 spec.
|
|
477
|
+
// Re-running reconciliation will overwrite the file -- intentional, since the file is meant to
|
|
478
|
+
// record the *successful* run that brought drift to zero, not a per-invocation history.
|
|
479
|
+
/**
|
|
480
|
+
* @param {string} projectRoot - Absolute path to project root
|
|
481
|
+
* @param {ReconciliationPlan} plan
|
|
482
|
+
* @param {number} postDriftCount - Drift count after reconciliation (0 on success)
|
|
483
|
+
* @param {string} timestamp - ISO timestamp string for the run
|
|
484
|
+
* @returns {string} - Relative path to the audit log
|
|
485
|
+
*/
|
|
486
|
+
function writeAuditLog(projectRoot, plan, postDriftCount, timestamp) {
|
|
487
|
+
const logDir = path.join(projectRoot, '.cap', 'memory');
|
|
488
|
+
if (!fs.existsSync(logDir)) {
|
|
489
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
const lines = [];
|
|
493
|
+
lines.push('# Status Drift Reconciliation Audit');
|
|
494
|
+
lines.push('');
|
|
495
|
+
lines.push(`**Date:** ${timestamp}`);
|
|
496
|
+
lines.push('**Trigger:** Manual `cap reconcile --apply`');
|
|
497
|
+
lines.push(`**Total changes:** ${plan.totalChanges}`);
|
|
498
|
+
lines.push('');
|
|
499
|
+
|
|
500
|
+
lines.push('## Phase 1 -- AC Promotions');
|
|
501
|
+
lines.push('');
|
|
502
|
+
if (plan.phase1.length === 0) {
|
|
503
|
+
lines.push('_No AC-status drift detected._');
|
|
504
|
+
lines.push('');
|
|
505
|
+
} else {
|
|
506
|
+
for (const entry of plan.phase1) {
|
|
507
|
+
lines.push(`### ${entry.featureId} (${entry.state})`);
|
|
508
|
+
for (const change of entry.acChanges) {
|
|
509
|
+
lines.push(`- ${change.acId}: ${change.from} -> ${change.to}`);
|
|
510
|
+
}
|
|
511
|
+
lines.push('');
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
lines.push('## Phase 2 -- Feature State Updates');
|
|
516
|
+
lines.push('');
|
|
517
|
+
if (plan.phase2.length === 0) {
|
|
518
|
+
lines.push('_No planned features required state updates._');
|
|
519
|
+
lines.push('');
|
|
520
|
+
} else {
|
|
521
|
+
for (const entry of plan.phase2) {
|
|
522
|
+
lines.push(`### ${entry.featureId} ${entry.fromState} -> ${entry.toState}`);
|
|
523
|
+
for (const f of entry.implFiles) {
|
|
524
|
+
lines.push(`- Reason: implementation file detected (\`${path.basename(f)}\`)`);
|
|
525
|
+
}
|
|
526
|
+
for (const f of entry.testFiles) {
|
|
527
|
+
lines.push(`- Reason: test file detected (\`${path.basename(f)}\`)`);
|
|
528
|
+
}
|
|
529
|
+
if (entry.toState === 'prototyped' && entry.testFiles.length === 0) {
|
|
530
|
+
lines.push('- Reason: no test file detected -- state capped at prototyped');
|
|
531
|
+
}
|
|
532
|
+
if (entry.propagatedAcChanges.length > 0) {
|
|
533
|
+
const acIds = entry.propagatedAcChanges.map(c => c.acId).join(', ');
|
|
534
|
+
lines.push(`- Propagated AC promotions: ${acIds} -> tested`);
|
|
535
|
+
}
|
|
536
|
+
lines.push('');
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
lines.push('## Phase 3 -- Verification');
|
|
541
|
+
lines.push('');
|
|
542
|
+
lines.push(`- Pre-reconciliation drift count: ${plan.preDriftCount}`);
|
|
543
|
+
lines.push(`- Post-reconciliation drift count: ${postDriftCount}`);
|
|
544
|
+
if (postDriftCount === 0) {
|
|
545
|
+
lines.push('- Result: All drift resolved');
|
|
546
|
+
} else {
|
|
547
|
+
lines.push(`- Result: ${postDriftCount} drift entries remain -- inspect FEATURE-MAP.md`);
|
|
548
|
+
}
|
|
549
|
+
lines.push('');
|
|
550
|
+
|
|
551
|
+
const filePath = path.join(projectRoot, AUDIT_LOG_RELATIVE);
|
|
552
|
+
fs.writeFileSync(filePath, lines.join('\n'), 'utf8');
|
|
553
|
+
return AUDIT_LOG_RELATIVE;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
module.exports = {
|
|
557
|
+
AUDIT_LOG_RELATIVE,
|
|
558
|
+
TEST_FILE_SUFFIXES,
|
|
559
|
+
TEST_DIR_CANDIDATES,
|
|
560
|
+
PHASE2_EXCLUDED_FEATURES,
|
|
561
|
+
isTestFile,
|
|
562
|
+
canonicalizePath,
|
|
563
|
+
groupTagsByFeatureFiles,
|
|
564
|
+
detectTestFileForImpl,
|
|
565
|
+
lifecyclePath,
|
|
566
|
+
planReconciliation,
|
|
567
|
+
formatPlan,
|
|
568
|
+
executeReconciliation,
|
|
569
|
+
writeAuditLog,
|
|
570
|
+
};
|