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,652 @@
|
|
|
1
|
+
// @cap-feature(feature:F-033) Implement Feature Impact Analysis — detect overlap between proposed features and existing Feature Map entries, trace dependency chains, detect circular deps, propose resolutions
|
|
2
|
+
// @cap-decision AC-2 (auto-run during brainstorm) is enforced at the COMMAND layer (brainstorm.md), not in this module. This module exposes analyzeImpact() which the command calls before proposing new Feature Map entries.
|
|
3
|
+
// @cap-decision AC-6 (advisory only) — this module NEVER calls writeFeatureMap(). All proposals are returned as structured data for the caller to present. No Feature Map modifications, dependency reordering, or AC adjustments happen without explicit user approval.
|
|
4
|
+
// @cap-decision Pure logic module with explicit I/O for persistence — analysis functions are side-effect-free; only persistReport() writes to disk.
|
|
5
|
+
// @cap-constraint Zero external dependencies — uses only Node.js built-ins (fs, path).
|
|
6
|
+
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
const fs = require('node:fs');
|
|
10
|
+
const path = require('node:path');
|
|
11
|
+
|
|
12
|
+
const {
|
|
13
|
+
readFeatureMap,
|
|
14
|
+
} = require('./cap-feature-map.cjs');
|
|
15
|
+
|
|
16
|
+
const {
|
|
17
|
+
extractKeywords,
|
|
18
|
+
computeKeywordOverlap,
|
|
19
|
+
} = require('./cap-thread-tracker.cjs');
|
|
20
|
+
|
|
21
|
+
// --- Constants ---
|
|
22
|
+
|
|
23
|
+
/** Directory for impact analysis reports relative to project root. */
|
|
24
|
+
const IMPACT_DIR = path.join('.cap', 'memory', 'impact');
|
|
25
|
+
|
|
26
|
+
/** Minimum Jaccard similarity (0-1) for two ACs to be considered overlapping. */
|
|
27
|
+
const AC_SIMILARITY_THRESHOLD = 0.25;
|
|
28
|
+
|
|
29
|
+
/** Minimum number of shared keywords for AC overlap to count. */
|
|
30
|
+
const AC_MIN_SHARED_KEYWORDS = 2;
|
|
31
|
+
|
|
32
|
+
/** Minimum file path overlap ratio (0-1) for a file conflict to be reported. */
|
|
33
|
+
const FILE_OVERLAP_THRESHOLD = 0.1;
|
|
34
|
+
|
|
35
|
+
// --- Types ---
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @typedef {Object} ACOverlap
|
|
39
|
+
* @property {string} existingFeatureId - ID of the existing feature
|
|
40
|
+
* @property {string} existingFeatureTitle - Title of the existing feature
|
|
41
|
+
* @property {string} existingACId - ID of the existing AC
|
|
42
|
+
* @property {string} existingACDescription - Description of the existing AC
|
|
43
|
+
* @property {string} proposedACId - ID of the proposed AC
|
|
44
|
+
* @property {string} proposedACDescription - Description of the proposed AC
|
|
45
|
+
* @property {number} similarity - Jaccard similarity score (0-1)
|
|
46
|
+
* @property {string[]} sharedKeywords - Keywords shared between the two ACs
|
|
47
|
+
* @property {string} reason - Human-readable explanation of the overlap
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* @typedef {Object} DependencyChain
|
|
52
|
+
* @property {string[]} upstream - Feature IDs that the target depends on (transitively)
|
|
53
|
+
* @property {string[]} downstream - Feature IDs that depend on the target (transitively)
|
|
54
|
+
* @property {number} depth - Maximum traversal depth reached
|
|
55
|
+
*/
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* @typedef {Object} CircularDepResult
|
|
59
|
+
* @property {boolean} hasCycle - Whether a cycle was detected
|
|
60
|
+
* @property {string[]} cycle - The cycle path as feature IDs (e.g., ["F-001", "F-003", "F-001"])
|
|
61
|
+
*/
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* @typedef {Object} FileConflict
|
|
65
|
+
* @property {string} filePath - The conflicting file path
|
|
66
|
+
* @property {string[]} existingFeatureIds - Features already referencing this file
|
|
67
|
+
*/
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* @typedef {Object} Resolution
|
|
71
|
+
* @property {'merge'|'split'|'adjust'|'flag'} type - Resolution type
|
|
72
|
+
* @property {string} description - Human-readable description of the proposed resolution
|
|
73
|
+
* @property {string[]} affectedFeatures - Feature IDs involved in this resolution
|
|
74
|
+
*/
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* @typedef {Object} ImpactReport
|
|
78
|
+
* @property {string} proposedFeatureTitle - Title of the proposed feature
|
|
79
|
+
* @property {string} timestamp - ISO timestamp when the report was generated
|
|
80
|
+
* @property {ACOverlap[]} overlappingACs - AC-level overlaps found
|
|
81
|
+
* @property {DependencyChain} affectedChains - Dependency chain analysis
|
|
82
|
+
* @property {FileConflict[]} fileConflicts - File path conflicts
|
|
83
|
+
* @property {CircularDepResult} circularRisks - Circular dependency analysis
|
|
84
|
+
* @property {Resolution[]} resolutions - Proposed resolutions
|
|
85
|
+
*/
|
|
86
|
+
|
|
87
|
+
// --- AC Overlap Detection ---
|
|
88
|
+
|
|
89
|
+
// @cap-todo(ac:F-033/AC-1) Detect overlap between proposed feature and existing Feature Map entries by comparing AC descriptions, dependency chains, and referenced file paths
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Detect overlapping ACs between a proposed feature and existing features.
|
|
93
|
+
* Uses keyword extraction + Jaccard similarity for AC description matching
|
|
94
|
+
* and file path overlap detection.
|
|
95
|
+
*
|
|
96
|
+
* @param {import('./cap-feature-map.cjs').Feature} proposedFeature - The proposed feature
|
|
97
|
+
* @param {import('./cap-feature-map.cjs').Feature[]} existingFeatures - All existing features
|
|
98
|
+
* @param {Object} [options]
|
|
99
|
+
* @param {number} [options.similarityThreshold] - Override AC similarity threshold
|
|
100
|
+
* @param {number} [options.minSharedKeywords] - Override minimum shared keywords
|
|
101
|
+
* @returns {{ overlaps: ACOverlap[], fileConflicts: FileConflict[] }}
|
|
102
|
+
*/
|
|
103
|
+
function detectOverlap(proposedFeature, existingFeatures, options = {}) {
|
|
104
|
+
const {
|
|
105
|
+
similarityThreshold = AC_SIMILARITY_THRESHOLD,
|
|
106
|
+
minSharedKeywords = AC_MIN_SHARED_KEYWORDS,
|
|
107
|
+
} = options;
|
|
108
|
+
|
|
109
|
+
const overlaps = [];
|
|
110
|
+
const fileConflicts = [];
|
|
111
|
+
|
|
112
|
+
// --- AC description overlap ---
|
|
113
|
+
const proposedACs = proposedFeature.acs || [];
|
|
114
|
+
|
|
115
|
+
for (const proposedAC of proposedACs) {
|
|
116
|
+
const proposedKeywords = extractKeywords(proposedAC.description);
|
|
117
|
+
if (proposedKeywords.length === 0) continue;
|
|
118
|
+
|
|
119
|
+
for (const existing of existingFeatures) {
|
|
120
|
+
// Skip self-comparison
|
|
121
|
+
if (existing.id === proposedFeature.id) continue;
|
|
122
|
+
|
|
123
|
+
for (const existingAC of existing.acs || []) {
|
|
124
|
+
const existingKeywords = extractKeywords(existingAC.description);
|
|
125
|
+
if (existingKeywords.length === 0) continue;
|
|
126
|
+
|
|
127
|
+
const { shared, overlapRatio } = computeKeywordOverlap(proposedKeywords, existingKeywords);
|
|
128
|
+
|
|
129
|
+
if (overlapRatio >= similarityThreshold && shared.length >= minSharedKeywords) {
|
|
130
|
+
overlaps.push({
|
|
131
|
+
existingFeatureId: existing.id,
|
|
132
|
+
existingFeatureTitle: existing.title,
|
|
133
|
+
existingACId: existingAC.id,
|
|
134
|
+
existingACDescription: existingAC.description,
|
|
135
|
+
proposedACId: proposedAC.id,
|
|
136
|
+
proposedACDescription: proposedAC.description,
|
|
137
|
+
similarity: Math.round(overlapRatio * 1000) / 1000,
|
|
138
|
+
sharedKeywords: shared,
|
|
139
|
+
reason: `Shared keywords: ${shared.join(', ')} (Jaccard: ${(overlapRatio * 100).toFixed(1)}%)`,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// --- File path overlap ---
|
|
147
|
+
const proposedFiles = new Set(proposedFeature.files || []);
|
|
148
|
+
if (proposedFiles.size > 0) {
|
|
149
|
+
/** @type {Map<string, string[]>} filePath -> featureIds */
|
|
150
|
+
const fileOwners = new Map();
|
|
151
|
+
for (const existing of existingFeatures) {
|
|
152
|
+
if (existing.id === proposedFeature.id) continue;
|
|
153
|
+
for (const file of existing.files || []) {
|
|
154
|
+
if (!fileOwners.has(file)) fileOwners.set(file, []);
|
|
155
|
+
fileOwners.get(file).push(existing.id);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
for (const file of proposedFiles) {
|
|
160
|
+
const owners = fileOwners.get(file);
|
|
161
|
+
if (owners && owners.length > 0) {
|
|
162
|
+
fileConflicts.push({
|
|
163
|
+
filePath: file,
|
|
164
|
+
existingFeatureIds: [...owners],
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return { overlaps, fileConflicts };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// --- Dependency Chain Tracing ---
|
|
174
|
+
|
|
175
|
+
// @cap-todo(ac:F-033/AC-4) Trace full dependency chains — if A depends on B depends on C, changing B ACs shall surface impact on both A and C
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Trace full dependency chain for a feature in both directions.
|
|
179
|
+
* Given a feature, finds all upstream dependencies (things it depends on)
|
|
180
|
+
* and downstream dependents (things that depend on it), with cycle detection.
|
|
181
|
+
*
|
|
182
|
+
* @param {string} featureId - The feature to trace from
|
|
183
|
+
* @param {import('./cap-feature-map.cjs').Feature[]} features - All features
|
|
184
|
+
* @param {'both'|'upstream'|'downstream'} [direction='both'] - Which direction to trace
|
|
185
|
+
* @returns {DependencyChain}
|
|
186
|
+
*/
|
|
187
|
+
function traceDependencyChain(featureId, features, direction = 'both') {
|
|
188
|
+
const upstream = [];
|
|
189
|
+
const downstream = [];
|
|
190
|
+
|
|
191
|
+
// Build lookup maps
|
|
192
|
+
/** @type {Map<string, string[]>} featureId -> dependencies (what it depends on) */
|
|
193
|
+
const depsMap = new Map();
|
|
194
|
+
/** @type {Map<string, string[]>} featureId -> dependents (what depends on it) */
|
|
195
|
+
const dependentsMap = new Map();
|
|
196
|
+
|
|
197
|
+
for (const f of features) {
|
|
198
|
+
depsMap.set(f.id, f.dependencies || []);
|
|
199
|
+
for (const dep of f.dependencies || []) {
|
|
200
|
+
if (!dependentsMap.has(dep)) dependentsMap.set(dep, []);
|
|
201
|
+
dependentsMap.get(dep).push(f.id);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
let maxDepth = 0;
|
|
206
|
+
|
|
207
|
+
// Trace upstream (things this feature depends on, recursively)
|
|
208
|
+
if (direction === 'both' || direction === 'upstream') {
|
|
209
|
+
const visited = new Set();
|
|
210
|
+
const queue = [{ id: featureId, depth: 0 }];
|
|
211
|
+
while (queue.length > 0) {
|
|
212
|
+
const { id, depth } = queue.shift();
|
|
213
|
+
const deps = depsMap.get(id) || [];
|
|
214
|
+
for (const dep of deps) {
|
|
215
|
+
if (visited.has(dep)) continue;
|
|
216
|
+
visited.add(dep);
|
|
217
|
+
upstream.push(dep);
|
|
218
|
+
if (depth + 1 > maxDepth) maxDepth = depth + 1;
|
|
219
|
+
queue.push({ id: dep, depth: depth + 1 });
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Trace downstream (things that depend on this feature, recursively)
|
|
225
|
+
if (direction === 'both' || direction === 'downstream') {
|
|
226
|
+
const visited = new Set();
|
|
227
|
+
const queue = [{ id: featureId, depth: 0 }];
|
|
228
|
+
while (queue.length > 0) {
|
|
229
|
+
const { id, depth } = queue.shift();
|
|
230
|
+
const deps = dependentsMap.get(id) || [];
|
|
231
|
+
for (const dep of deps) {
|
|
232
|
+
if (visited.has(dep)) continue;
|
|
233
|
+
visited.add(dep);
|
|
234
|
+
downstream.push(dep);
|
|
235
|
+
if (depth + 1 > maxDepth) maxDepth = depth + 1;
|
|
236
|
+
queue.push({ id: dep, depth: depth + 1 });
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return { upstream, downstream, depth: maxDepth };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// --- Circular Dependency Detection ---
|
|
245
|
+
|
|
246
|
+
// @cap-todo(ac:F-033/AC-8) Detect circular dependency risks when new features are proposed and warn before Feature Map entries are written
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Detect if adding proposed dependencies would create a circular dependency.
|
|
250
|
+
* Uses DFS cycle detection on the dependency graph augmented with proposed edges.
|
|
251
|
+
*
|
|
252
|
+
* @param {string} proposedFeatureId - The feature ID being proposed
|
|
253
|
+
* @param {string[]} proposedDeps - Proposed dependency list for the new feature
|
|
254
|
+
* @param {import('./cap-feature-map.cjs').Feature[]} existingFeatures - All existing features
|
|
255
|
+
* @returns {CircularDepResult}
|
|
256
|
+
*/
|
|
257
|
+
function detectCircularDeps(proposedFeatureId, proposedDeps, existingFeatures) {
|
|
258
|
+
// Build adjacency list: featureId -> [dependencies]
|
|
259
|
+
/** @type {Map<string, string[]>} */
|
|
260
|
+
const graph = new Map();
|
|
261
|
+
|
|
262
|
+
for (const f of existingFeatures) {
|
|
263
|
+
graph.set(f.id, [...(f.dependencies || [])]);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Add proposed feature and its dependencies
|
|
267
|
+
graph.set(proposedFeatureId, [...proposedDeps]);
|
|
268
|
+
|
|
269
|
+
// DFS cycle detection
|
|
270
|
+
const WHITE = 0; // unvisited
|
|
271
|
+
const GRAY = 1; // in current path
|
|
272
|
+
const BLACK = 2; // fully processed
|
|
273
|
+
|
|
274
|
+
/** @type {Map<string, number>} */
|
|
275
|
+
const color = new Map();
|
|
276
|
+
/** @type {Map<string, string|null>} */
|
|
277
|
+
const parent = new Map();
|
|
278
|
+
|
|
279
|
+
for (const id of graph.keys()) {
|
|
280
|
+
color.set(id, WHITE);
|
|
281
|
+
parent.set(id, null);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* DFS from node, returns cycle path if found.
|
|
286
|
+
* @param {string} node
|
|
287
|
+
* @returns {string[]|null}
|
|
288
|
+
*/
|
|
289
|
+
function dfs(node) {
|
|
290
|
+
color.set(node, GRAY);
|
|
291
|
+
|
|
292
|
+
for (const neighbor of graph.get(node) || []) {
|
|
293
|
+
// Neighbor might not be in graph (e.g., references non-existent feature)
|
|
294
|
+
if (!graph.has(neighbor)) continue;
|
|
295
|
+
|
|
296
|
+
if (color.get(neighbor) === GRAY) {
|
|
297
|
+
// Found cycle — reconstruct path
|
|
298
|
+
const cycle = [neighbor, node];
|
|
299
|
+
let current = node;
|
|
300
|
+
while (current !== neighbor) {
|
|
301
|
+
current = parent.get(current);
|
|
302
|
+
if (current === null || current === neighbor) break;
|
|
303
|
+
cycle.push(current);
|
|
304
|
+
}
|
|
305
|
+
cycle.push(neighbor);
|
|
306
|
+
cycle.reverse();
|
|
307
|
+
return cycle;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (color.get(neighbor) === WHITE) {
|
|
311
|
+
parent.set(neighbor, node);
|
|
312
|
+
const result = dfs(neighbor);
|
|
313
|
+
if (result) return result;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
color.set(node, BLACK);
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Start DFS from proposed feature to find cycles involving it
|
|
322
|
+
const cycle = dfs(proposedFeatureId);
|
|
323
|
+
if (cycle) {
|
|
324
|
+
return { hasCycle: true, cycle };
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Also check all other unvisited nodes (in case proposed deps create a cycle elsewhere)
|
|
328
|
+
for (const id of graph.keys()) {
|
|
329
|
+
if (color.get(id) === WHITE) {
|
|
330
|
+
const result = dfs(id);
|
|
331
|
+
if (result) {
|
|
332
|
+
return { hasCycle: true, cycle: result };
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return { hasCycle: false, cycle: [] };
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// --- Impact Report Generation ---
|
|
341
|
+
|
|
342
|
+
// @cap-todo(ac:F-033/AC-3) Present structured impact report: overlapping ACs with similarity reasoning, affected dependency chains, implementation file conflicts
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Generate a full impact report for a proposed feature.
|
|
346
|
+
* Orchestrates overlap detection, dependency tracing, and circular dep checks.
|
|
347
|
+
*
|
|
348
|
+
* @param {import('./cap-feature-map.cjs').Feature} proposedFeature - The proposed feature (must have id, title, acs, files, dependencies)
|
|
349
|
+
* @param {import('./cap-feature-map.cjs').Feature[]} existingFeatures - All existing features from Feature Map
|
|
350
|
+
* @param {Object} [options]
|
|
351
|
+
* @param {number} [options.similarityThreshold] - Override AC similarity threshold
|
|
352
|
+
* @param {number} [options.minSharedKeywords] - Override minimum shared keywords
|
|
353
|
+
* @returns {ImpactReport}
|
|
354
|
+
*/
|
|
355
|
+
function generateImpactReport(proposedFeature, existingFeatures, options = {}) {
|
|
356
|
+
// AC overlap and file conflicts
|
|
357
|
+
const { overlaps, fileConflicts } = detectOverlap(proposedFeature, existingFeatures, options);
|
|
358
|
+
|
|
359
|
+
// Dependency chain analysis
|
|
360
|
+
// Merge existing features with proposed feature for full graph
|
|
361
|
+
const allFeatures = [...existingFeatures.filter(f => f.id !== proposedFeature.id), proposedFeature];
|
|
362
|
+
const affectedChains = traceDependencyChain(proposedFeature.id, allFeatures);
|
|
363
|
+
|
|
364
|
+
// Circular dependency check
|
|
365
|
+
const circularRisks = detectCircularDeps(
|
|
366
|
+
proposedFeature.id,
|
|
367
|
+
proposedFeature.dependencies || [],
|
|
368
|
+
existingFeatures
|
|
369
|
+
);
|
|
370
|
+
|
|
371
|
+
// Generate resolutions
|
|
372
|
+
const report = {
|
|
373
|
+
proposedFeatureTitle: proposedFeature.title || '',
|
|
374
|
+
timestamp: new Date().toISOString(),
|
|
375
|
+
overlappingACs: overlaps,
|
|
376
|
+
affectedChains,
|
|
377
|
+
fileConflicts,
|
|
378
|
+
circularRisks,
|
|
379
|
+
resolutions: [],
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
report.resolutions = proposeResolutions(report);
|
|
383
|
+
|
|
384
|
+
return report;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// --- Resolution Proposals ---
|
|
388
|
+
|
|
389
|
+
// @cap-todo(ac:F-033/AC-5) Propose concrete resolutions: merge ACs into existing feature, split into separate features, adjust dependency ordering, or flag as intentional duplication
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Propose concrete resolutions for issues found in the impact report.
|
|
393
|
+
* For each overlap/conflict, suggests merge, split, adjust, or flag.
|
|
394
|
+
*
|
|
395
|
+
* @param {ImpactReport} report - The impact report to generate resolutions for
|
|
396
|
+
* @returns {Resolution[]}
|
|
397
|
+
*/
|
|
398
|
+
function proposeResolutions(report) {
|
|
399
|
+
const resolutions = [];
|
|
400
|
+
|
|
401
|
+
// --- Circular dependency resolution ---
|
|
402
|
+
if (report.circularRisks.hasCycle) {
|
|
403
|
+
resolutions.push({
|
|
404
|
+
type: 'adjust',
|
|
405
|
+
description: `Circular dependency detected: ${report.circularRisks.cycle.join(' -> ')}. Reorder dependencies or break the cycle by extracting shared concerns into a separate feature.`,
|
|
406
|
+
affectedFeatures: [...new Set(report.circularRisks.cycle)],
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// --- Group AC overlaps by existing feature ---
|
|
411
|
+
/** @type {Map<string, ACOverlap[]>} existingFeatureId -> overlaps */
|
|
412
|
+
const overlapsByFeature = new Map();
|
|
413
|
+
for (const overlap of report.overlappingACs) {
|
|
414
|
+
if (!overlapsByFeature.has(overlap.existingFeatureId)) {
|
|
415
|
+
overlapsByFeature.set(overlap.existingFeatureId, []);
|
|
416
|
+
}
|
|
417
|
+
overlapsByFeature.get(overlap.existingFeatureId).push(overlap);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
for (const [featureId, overlaps] of overlapsByFeature) {
|
|
421
|
+
const featureTitle = overlaps[0].existingFeatureTitle;
|
|
422
|
+
const avgSimilarity = overlaps.reduce((sum, o) => sum + o.similarity, 0) / overlaps.length;
|
|
423
|
+
|
|
424
|
+
if (avgSimilarity >= 0.5) {
|
|
425
|
+
// High overlap — suggest merge
|
|
426
|
+
resolutions.push({
|
|
427
|
+
type: 'merge',
|
|
428
|
+
description: `High AC overlap (avg ${(avgSimilarity * 100).toFixed(0)}%) with ${featureId} (${featureTitle}). Consider merging ${overlaps.length} overlapping AC(s) into the existing feature.`,
|
|
429
|
+
affectedFeatures: [featureId],
|
|
430
|
+
});
|
|
431
|
+
} else if (overlaps.length >= 3) {
|
|
432
|
+
// Many partial overlaps — suggest split
|
|
433
|
+
resolutions.push({
|
|
434
|
+
type: 'split',
|
|
435
|
+
description: `Multiple partial overlaps (${overlaps.length} ACs) with ${featureId} (${featureTitle}). Consider extracting shared concerns into a separate shared feature.`,
|
|
436
|
+
affectedFeatures: [featureId],
|
|
437
|
+
});
|
|
438
|
+
} else {
|
|
439
|
+
// Low overlap — flag for awareness
|
|
440
|
+
resolutions.push({
|
|
441
|
+
type: 'flag',
|
|
442
|
+
description: `Minor AC overlap with ${featureId} (${featureTitle}): ${overlaps.map(o => `${o.proposedACId} ~ ${o.existingACId}`).join(', ')}. May be intentional duplication.`,
|
|
443
|
+
affectedFeatures: [featureId],
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// --- File conflict resolutions ---
|
|
449
|
+
if (report.fileConflicts.length > 0) {
|
|
450
|
+
const affectedFiles = report.fileConflicts.map(c => c.filePath);
|
|
451
|
+
const affectedFeatures = [...new Set(report.fileConflicts.flatMap(c => c.existingFeatureIds))];
|
|
452
|
+
|
|
453
|
+
resolutions.push({
|
|
454
|
+
type: 'adjust',
|
|
455
|
+
description: `File ownership conflicts on ${affectedFiles.length} file(s): ${affectedFiles.join(', ')}. Verify intended scope or split responsibilities.`,
|
|
456
|
+
affectedFeatures,
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
return resolutions;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// --- Report Persistence ---
|
|
464
|
+
|
|
465
|
+
// @cap-todo(ac:F-033/AC-7) Persist impact analysis results in .cap/memory/impact/{feature-id}.md for audit trail and future reference
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Persist an impact report to .cap/memory/impact/{feature-id}.md.
|
|
469
|
+
* Creates the directory if it does not exist.
|
|
470
|
+
*
|
|
471
|
+
* @param {string} cwd - Absolute path to project root
|
|
472
|
+
* @param {string} featureId - Feature ID for the report filename
|
|
473
|
+
* @param {ImpactReport} report - The impact report to persist
|
|
474
|
+
*/
|
|
475
|
+
function persistReport(cwd, featureId, report) {
|
|
476
|
+
const impactDir = path.join(cwd, IMPACT_DIR);
|
|
477
|
+
if (!fs.existsSync(impactDir)) {
|
|
478
|
+
fs.mkdirSync(impactDir, { recursive: true });
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
const filePath = path.join(impactDir, `${featureId}.md`);
|
|
482
|
+
const content = serializeReport(featureId, report);
|
|
483
|
+
fs.writeFileSync(filePath, content, 'utf8');
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Serialize an impact report to markdown format.
|
|
488
|
+
*
|
|
489
|
+
* @param {string} featureId - Feature ID
|
|
490
|
+
* @param {ImpactReport} report - The impact report
|
|
491
|
+
* @returns {string} Markdown content
|
|
492
|
+
*/
|
|
493
|
+
function serializeReport(featureId, report) {
|
|
494
|
+
const lines = [
|
|
495
|
+
`# Impact Analysis: ${featureId}`,
|
|
496
|
+
'',
|
|
497
|
+
`**Feature:** ${report.proposedFeatureTitle}`,
|
|
498
|
+
`**Generated:** ${report.timestamp}`,
|
|
499
|
+
'',
|
|
500
|
+
];
|
|
501
|
+
|
|
502
|
+
// --- Overlapping ACs ---
|
|
503
|
+
lines.push('## Overlapping ACs');
|
|
504
|
+
lines.push('');
|
|
505
|
+
if (report.overlappingACs.length === 0) {
|
|
506
|
+
lines.push('No AC overlaps detected.');
|
|
507
|
+
} else {
|
|
508
|
+
lines.push('| Proposed AC | Existing Feature | Existing AC | Similarity | Shared Keywords |');
|
|
509
|
+
lines.push('|-------------|-----------------|-------------|------------|-----------------|');
|
|
510
|
+
for (const o of report.overlappingACs) {
|
|
511
|
+
lines.push(`| ${o.proposedACId} | ${o.existingFeatureId} | ${o.existingACId} | ${(o.similarity * 100).toFixed(1)}% | ${o.sharedKeywords.join(', ')} |`);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
lines.push('');
|
|
515
|
+
|
|
516
|
+
// --- Dependency Chains ---
|
|
517
|
+
lines.push('## Dependency Chains');
|
|
518
|
+
lines.push('');
|
|
519
|
+
lines.push(`**Upstream (depends on):** ${report.affectedChains.upstream.length > 0 ? report.affectedChains.upstream.join(', ') : 'none'}`);
|
|
520
|
+
lines.push(`**Downstream (depended on by):** ${report.affectedChains.downstream.length > 0 ? report.affectedChains.downstream.join(', ') : 'none'}`);
|
|
521
|
+
lines.push(`**Max depth:** ${report.affectedChains.depth}`);
|
|
522
|
+
lines.push('');
|
|
523
|
+
|
|
524
|
+
// --- File Conflicts ---
|
|
525
|
+
lines.push('## File Conflicts');
|
|
526
|
+
lines.push('');
|
|
527
|
+
if (report.fileConflicts.length === 0) {
|
|
528
|
+
lines.push('No file conflicts detected.');
|
|
529
|
+
} else {
|
|
530
|
+
for (const c of report.fileConflicts) {
|
|
531
|
+
lines.push(`- \`${c.filePath}\` — also referenced by: ${c.existingFeatureIds.join(', ')}`);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
lines.push('');
|
|
535
|
+
|
|
536
|
+
// --- Circular Dependency Risks ---
|
|
537
|
+
lines.push('## Circular Dependency Risks');
|
|
538
|
+
lines.push('');
|
|
539
|
+
if (report.circularRisks.hasCycle) {
|
|
540
|
+
lines.push(`**WARNING:** Circular dependency detected: ${report.circularRisks.cycle.join(' -> ')}`);
|
|
541
|
+
} else {
|
|
542
|
+
lines.push('No circular dependencies detected.');
|
|
543
|
+
}
|
|
544
|
+
lines.push('');
|
|
545
|
+
|
|
546
|
+
// --- Proposed Resolutions ---
|
|
547
|
+
lines.push('## Proposed Resolutions');
|
|
548
|
+
lines.push('');
|
|
549
|
+
if (report.resolutions.length === 0) {
|
|
550
|
+
lines.push('No resolutions needed — clean integration.');
|
|
551
|
+
} else {
|
|
552
|
+
for (let i = 0; i < report.resolutions.length; i++) {
|
|
553
|
+
const r = report.resolutions[i];
|
|
554
|
+
lines.push(`${i + 1}. **[${r.type.toUpperCase()}]** ${r.description}`);
|
|
555
|
+
lines.push(` Affects: ${r.affectedFeatures.join(', ')}`);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
lines.push('');
|
|
559
|
+
|
|
560
|
+
lines.push('---');
|
|
561
|
+
lines.push('*This report is advisory only. No Feature Map modifications were made.*');
|
|
562
|
+
lines.push('');
|
|
563
|
+
|
|
564
|
+
return lines.join('\n');
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* Load a previously persisted impact report from disk.
|
|
569
|
+
*
|
|
570
|
+
* @param {string} cwd - Absolute path to project root
|
|
571
|
+
* @param {string} featureId - Feature ID to load report for
|
|
572
|
+
* @returns {string|null} Raw markdown content, or null if not found
|
|
573
|
+
*/
|
|
574
|
+
function loadReport(cwd, featureId) {
|
|
575
|
+
const filePath = path.join(cwd, IMPACT_DIR, `${featureId}.md`);
|
|
576
|
+
try {
|
|
577
|
+
if (!fs.existsSync(filePath)) return null;
|
|
578
|
+
return fs.readFileSync(filePath, 'utf8');
|
|
579
|
+
} catch (_e) {
|
|
580
|
+
return null;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// --- Main Entry Point ---
|
|
585
|
+
|
|
586
|
+
// @cap-todo(ac:F-033/AC-6) All proposals shall be advisory only — no Feature Map modifications without explicit user approval
|
|
587
|
+
// @cap-decision analyzeImpact reads Feature Map but NEVER writes to it. All returned data is advisory for caller to present.
|
|
588
|
+
|
|
589
|
+
/**
|
|
590
|
+
* Main entry point: analyze the impact of a proposed feature against the existing Feature Map.
|
|
591
|
+
* Reads Feature Map, runs all analysis, returns full report.
|
|
592
|
+
* Does NOT modify Feature Map.
|
|
593
|
+
*
|
|
594
|
+
* @param {string} cwd - Absolute path to project root
|
|
595
|
+
* @param {import('./cap-feature-map.cjs').Feature} proposedFeature - The proposed feature
|
|
596
|
+
* @param {Object} [options]
|
|
597
|
+
* @param {boolean} [options.persist] - If true, persist report to disk (default: false)
|
|
598
|
+
* @param {string|null} [options.appPath] - Relative app path for monorepo scoping
|
|
599
|
+
* @param {number} [options.similarityThreshold] - Override AC similarity threshold
|
|
600
|
+
* @param {number} [options.minSharedKeywords] - Override minimum shared keywords
|
|
601
|
+
* @returns {ImpactReport}
|
|
602
|
+
*/
|
|
603
|
+
function analyzeImpact(cwd, proposedFeature, options = {}) {
|
|
604
|
+
const {
|
|
605
|
+
persist = false,
|
|
606
|
+
appPath = null,
|
|
607
|
+
similarityThreshold,
|
|
608
|
+
minSharedKeywords,
|
|
609
|
+
} = options;
|
|
610
|
+
|
|
611
|
+
// @cap-todo(ac:F-081/AC-4 iter:2) Migrated to {safe: true} opt-in to preserve CLI on duplicate-ID FEATURE-MAP.
|
|
612
|
+
// @cap-decision(F-081/iter2) Warn on parseError; continue with partial map for read-only display.
|
|
613
|
+
const featureMap = readFeatureMap(cwd, appPath, { safe: true });
|
|
614
|
+
if (featureMap && featureMap.parseError) {
|
|
615
|
+
console.warn('cap: impact-analysis — duplicate feature ID detected, report uses partial map: ' + String(featureMap.parseError.message).trim());
|
|
616
|
+
}
|
|
617
|
+
const existingFeatures = featureMap.features || [];
|
|
618
|
+
|
|
619
|
+
const report = generateImpactReport(proposedFeature, existingFeatures, {
|
|
620
|
+
similarityThreshold,
|
|
621
|
+
minSharedKeywords,
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
if (persist && proposedFeature.id) {
|
|
625
|
+
persistReport(cwd, proposedFeature.id, report);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
return report;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
module.exports = {
|
|
632
|
+
// Core analysis
|
|
633
|
+
detectOverlap,
|
|
634
|
+
traceDependencyChain,
|
|
635
|
+
detectCircularDeps,
|
|
636
|
+
generateImpactReport,
|
|
637
|
+
proposeResolutions,
|
|
638
|
+
|
|
639
|
+
// Persistence
|
|
640
|
+
persistReport,
|
|
641
|
+
serializeReport,
|
|
642
|
+
loadReport,
|
|
643
|
+
|
|
644
|
+
// Main entry point
|
|
645
|
+
analyzeImpact,
|
|
646
|
+
|
|
647
|
+
// Constants (exposed for testing and configuration)
|
|
648
|
+
IMPACT_DIR,
|
|
649
|
+
AC_SIMILARITY_THRESHOLD,
|
|
650
|
+
AC_MIN_SHARED_KEYWORDS,
|
|
651
|
+
FILE_OVERLAP_THRESHOLD,
|
|
652
|
+
};
|