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,309 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// @cap-feature(feature:F-047) Migration tool: fragmented @cap-* tags -> unified anchor block.
|
|
4
|
+
// @cap-history(sessions:2, edits:7, since:2026-04-20, learned:2026-05-07) Frequently modified — 2 sessions, 7 edits
|
|
5
|
+
//
|
|
6
|
+
// v1 strategy is ADDITIVE: a unified anchor block is inserted near the top of each file
|
|
7
|
+
// that carries fragmented AC-level tags, but the legacy tags themselves are NOT removed.
|
|
8
|
+
// Both formats coexist during the deprecation window (AC-2). A future `--remove-legacy`
|
|
9
|
+
// mode can delete the fragmented tags once the ecosystem has fully switched.
|
|
10
|
+
//
|
|
11
|
+
// @cap-decision Additive migration keeps the blast radius tiny: no line deletes, no regex-
|
|
12
|
+
// based source rewrites of tag annotations, no risk of destroying surrounding code. The
|
|
13
|
+
// tradeoff is dual-tag files until the cleanup pass runs. Documented as an explicit choice.
|
|
14
|
+
|
|
15
|
+
const fs = require('node:fs');
|
|
16
|
+
const path = require('node:path');
|
|
17
|
+
const anchor = require('./cap-anchor.cjs');
|
|
18
|
+
const scanner = require('./cap-tag-scanner.cjs');
|
|
19
|
+
// @cap-feature(feature:F-085) Scope filter — same module as the scanner uses, ensures both
|
|
20
|
+
// tools share gitignore + path-pattern + plugin-mirror exclusions.
|
|
21
|
+
const scopeModule = require('./cap-scope-filter.cjs');
|
|
22
|
+
|
|
23
|
+
// @cap-todo(ac:F-047/AC-3) Per-file summary of what the migration would / did change.
|
|
24
|
+
/**
|
|
25
|
+
* @typedef {Object} FileMigrationResult
|
|
26
|
+
* @property {string} file - Relative path
|
|
27
|
+
* @property {boolean} changed - True when an anchor would be inserted
|
|
28
|
+
* @property {string|null} newContent - Proposed content (dry-run) or written content
|
|
29
|
+
* @property {string|null} anchorBlock - The anchor line that would be inserted (or null)
|
|
30
|
+
* @property {string[]} consolidatedFeatures - Feature IDs consolidated into the anchor
|
|
31
|
+
* @property {string[]} consolidatedAcs - AC IDs consolidated (e.g. 'F-001/AC-1')
|
|
32
|
+
* @property {string} reason - 'inserted', 'already-has-anchor', 'no-feature-tags'
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Decide the comment delimiter style appropriate for this file extension.
|
|
37
|
+
* Mirrors cap-anchor.emitAnchorBlock() styles: block (slash-star), line (hash), html.
|
|
38
|
+
*
|
|
39
|
+
* @param {string} filePath
|
|
40
|
+
* @returns {'block'|'line'|'html'}
|
|
41
|
+
*/
|
|
42
|
+
function commentStyleForFile(filePath) {
|
|
43
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
44
|
+
if (ext === '.html' || ext === '.htm' || ext === '.xml' || ext === '.vue' || ext === '.md') {
|
|
45
|
+
return 'html';
|
|
46
|
+
}
|
|
47
|
+
if (
|
|
48
|
+
ext === '.py' || ext === '.rb' || ext === '.sh' || ext === '.bash' || ext === '.zsh' ||
|
|
49
|
+
ext === '.yml' || ext === '.yaml' || ext === '.toml'
|
|
50
|
+
) {
|
|
51
|
+
return 'line';
|
|
52
|
+
}
|
|
53
|
+
// Default: block comment works in JS/TS/Go/Rust/C/C++/Java/SQL/CSS.
|
|
54
|
+
return 'block';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Build the anchor structure for a single feature group (the file may tag multiple features —
|
|
59
|
+
* in which case we emit one anchor per feature). Returns null when the group has no AC coverage
|
|
60
|
+
* AND no primary role (nothing worth consolidating).
|
|
61
|
+
*
|
|
62
|
+
* @param {string} featureId
|
|
63
|
+
* @param {CapTag[]} tags - Tags for this feature on this file (feature + todo subset)
|
|
64
|
+
* @returns {{feature:string, acs:string[], role:('primary'|'secondary'|null)}|null}
|
|
65
|
+
*/
|
|
66
|
+
function consolidateGroup(featureId, tags) {
|
|
67
|
+
const acs = new Set();
|
|
68
|
+
let role = null;
|
|
69
|
+
for (const t of tags) {
|
|
70
|
+
if (t.type === 'feature') {
|
|
71
|
+
if (t.metadata && t.metadata.primary === true) role = 'primary';
|
|
72
|
+
} else if (t.type === 'todo' && t.metadata && typeof t.metadata.ac === 'string') {
|
|
73
|
+
// ac format: 'F-XXX/AC-N' or 'AC-N' (resolved against feature). Anchor both ends
|
|
74
|
+
// so a stray 'notAC-12-ish' cannot accidentally match.
|
|
75
|
+
const m = t.metadata.ac.match(/^(?:F-\d{3,}\/)?(AC-\d+)$/);
|
|
76
|
+
if (m) acs.add(m[1]);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (acs.size === 0 && role === null) return null;
|
|
80
|
+
return { feature: featureId, acs: [...acs].sort(compareAcIds), role };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function compareAcIds(a, b) {
|
|
84
|
+
const na = parseInt(a.slice(3), 10);
|
|
85
|
+
const nb = parseInt(b.slice(3), 10);
|
|
86
|
+
return na - nb;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Find the line index where an anchor block should be inserted: after the shebang (if any)
|
|
91
|
+
* and after any 'use strict' directive, but before the first code line.
|
|
92
|
+
*
|
|
93
|
+
* @param {string[]} lines
|
|
94
|
+
* @returns {number} Zero-based index of the insertion point
|
|
95
|
+
*/
|
|
96
|
+
function findInsertionIndex(lines) {
|
|
97
|
+
let i = 0;
|
|
98
|
+
if (lines.length === 0) return 0;
|
|
99
|
+
if (lines[0].startsWith('#!')) i++;
|
|
100
|
+
// Skip blank + 'use strict' line
|
|
101
|
+
while (i < lines.length) {
|
|
102
|
+
const t = lines[i].trim();
|
|
103
|
+
if (t === '' || /^['"]use strict['"];?$/.test(t)) { i++; continue; }
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
return i;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Compute the migration for a single file WITHOUT writing. The caller decides whether to
|
|
111
|
+
* persist `newContent`. Scanner tags are required for the file; callers pass them in so this
|
|
112
|
+
* module stays decoupled from disk except for the actual read/write operations in helpers.
|
|
113
|
+
*
|
|
114
|
+
* @param {string} filePath - Absolute path
|
|
115
|
+
* @param {string} projectRoot - Absolute project root (for relative-path reporting)
|
|
116
|
+
* @param {CapTag[]} tags - Scanner output for this file (legacy tags only)
|
|
117
|
+
* @returns {FileMigrationResult}
|
|
118
|
+
*/
|
|
119
|
+
function planFileMigration(filePath, projectRoot, tags) {
|
|
120
|
+
const rel = path.relative(projectRoot, filePath);
|
|
121
|
+
/** @type {FileMigrationResult} */
|
|
122
|
+
const result = {
|
|
123
|
+
file: rel,
|
|
124
|
+
changed: false,
|
|
125
|
+
newContent: null,
|
|
126
|
+
anchorBlock: null,
|
|
127
|
+
consolidatedFeatures: [],
|
|
128
|
+
consolidatedAcs: [],
|
|
129
|
+
reason: 'no-feature-tags',
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
let content;
|
|
133
|
+
try {
|
|
134
|
+
content = fs.readFileSync(filePath, 'utf8');
|
|
135
|
+
} catch (_e) {
|
|
136
|
+
result.reason = 'read-error';
|
|
137
|
+
return result;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Short-circuit: if the file already contains a unified anchor, leave it alone.
|
|
141
|
+
const anchorTags = anchor.scanAnchorsInContent(content, rel);
|
|
142
|
+
if (anchorTags.length > 0) {
|
|
143
|
+
result.reason = 'already-has-anchor';
|
|
144
|
+
return result;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Group tags by feature. Non-feature-bound tags (@cap-risk, @cap-decision with no `feature`)
|
|
148
|
+
// are intentionally ignored for consolidation.
|
|
149
|
+
const byFeature = new Map();
|
|
150
|
+
for (const t of tags) {
|
|
151
|
+
const fid = t.metadata && t.metadata.feature;
|
|
152
|
+
if (fid) {
|
|
153
|
+
if (!byFeature.has(fid)) byFeature.set(fid, []);
|
|
154
|
+
byFeature.get(fid).push(t);
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
// @cap-todo with ac:F-XXX/AC-N (no feature key) — infer feature from ac prefix.
|
|
158
|
+
if (t.type === 'todo' && t.metadata && typeof t.metadata.ac === 'string') {
|
|
159
|
+
const m = t.metadata.ac.match(/^(F-\d{3,})\//);
|
|
160
|
+
if (m) {
|
|
161
|
+
const inferred = m[1];
|
|
162
|
+
if (!byFeature.has(inferred)) byFeature.set(inferred, []);
|
|
163
|
+
byFeature.get(inferred).push(t);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (byFeature.size === 0) return result;
|
|
169
|
+
|
|
170
|
+
// Build anchor lines per feature group. Stable order: features sorted alphanumerically.
|
|
171
|
+
const featureIds = [...byFeature.keys()].sort();
|
|
172
|
+
const style = commentStyleForFile(filePath);
|
|
173
|
+
const anchorLines = [];
|
|
174
|
+
for (const fid of featureIds) {
|
|
175
|
+
const consolidated = consolidateGroup(fid, byFeature.get(fid));
|
|
176
|
+
if (!consolidated) continue;
|
|
177
|
+
anchorLines.push(anchor.emitAnchorBlock(consolidated, style));
|
|
178
|
+
result.consolidatedFeatures.push(fid);
|
|
179
|
+
for (const ac of consolidated.acs) {
|
|
180
|
+
result.consolidatedAcs.push(`${fid}/${ac}`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (anchorLines.length === 0) return result;
|
|
185
|
+
|
|
186
|
+
const lines = content.split('\n');
|
|
187
|
+
const insertAt = findInsertionIndex(lines);
|
|
188
|
+
const newLines = [...lines];
|
|
189
|
+
// Insert anchor lines + one trailing blank separator if the next line isn't already blank
|
|
190
|
+
const needsSpacer = newLines[insertAt] !== undefined && newLines[insertAt].trim() !== '';
|
|
191
|
+
const toInsert = needsSpacer ? [...anchorLines, ''] : [...anchorLines];
|
|
192
|
+
newLines.splice(insertAt, 0, ...toInsert);
|
|
193
|
+
|
|
194
|
+
result.changed = true;
|
|
195
|
+
result.anchorBlock = anchorLines.join('\n');
|
|
196
|
+
result.newContent = newLines.join('\n');
|
|
197
|
+
result.reason = 'inserted';
|
|
198
|
+
return result;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Compute migrations for every file under the project. Does NOT write.
|
|
203
|
+
*
|
|
204
|
+
* @param {string} projectRoot
|
|
205
|
+
* @param {{ extensions?: string[], exclude?: string[] }} [options]
|
|
206
|
+
* @returns {FileMigrationResult[]}
|
|
207
|
+
*/
|
|
208
|
+
function planProjectMigration(projectRoot, options = {}) {
|
|
209
|
+
// @cap-todo(ac:F-085/AC-1) The migrator builds (or accepts) the same scope filter as the
|
|
210
|
+
// scanner so both tools agree on which files are in scope. Passing it explicitly into
|
|
211
|
+
// scanDirectory short-circuits the scanner's default-build path.
|
|
212
|
+
const scope = options.scope || scopeModule.buildScopeFilter(projectRoot, {
|
|
213
|
+
dirExcludes: options.exclude,
|
|
214
|
+
pathExcludes: options.pathExcludes,
|
|
215
|
+
excludes: options.excludes,
|
|
216
|
+
includes: options.includes,
|
|
217
|
+
respectGitignore: options.respectGitignore,
|
|
218
|
+
});
|
|
219
|
+
// We want legacy tags only — force unifiedAnchors:false so the scan baseline is clean.
|
|
220
|
+
const allTags = scanner.scanDirectory(projectRoot, { ...options, scope, unifiedAnchors: false });
|
|
221
|
+
|
|
222
|
+
// Group tags by file
|
|
223
|
+
const byFile = new Map();
|
|
224
|
+
for (const t of allTags) {
|
|
225
|
+
const abs = path.resolve(projectRoot, t.file);
|
|
226
|
+
if (!byFile.has(abs)) byFile.set(abs, []);
|
|
227
|
+
byFile.get(abs).push(t);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const results = [];
|
|
231
|
+
for (const [abs, tags] of byFile.entries()) {
|
|
232
|
+
results.push(planFileMigration(abs, projectRoot, tags));
|
|
233
|
+
}
|
|
234
|
+
return results.sort((a, b) => a.file.localeCompare(b.file));
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Write planned migrations to disk. Only files flagged changed=true are touched.
|
|
239
|
+
*
|
|
240
|
+
* @param {FileMigrationResult[]} results
|
|
241
|
+
* @param {string} projectRoot
|
|
242
|
+
* @param {{ allowLargeDiff?: boolean }} [options]
|
|
243
|
+
* @returns {{ written: string[], skipped: string[] }}
|
|
244
|
+
*/
|
|
245
|
+
function applyMigrations(results, projectRoot, options = {}) {
|
|
246
|
+
// @cap-todo(ac:F-085/AC-7) Large-diff guard: bare --apply against >500 candidate files
|
|
247
|
+
// is almost always a scope-filter bug, not user intent. We throw with an actionable
|
|
248
|
+
// error so the caller can re-run with allowLargeDiff:true once the scope is verified.
|
|
249
|
+
const changed = results.filter((r) => r.changed && r.newContent != null);
|
|
250
|
+
if (changed.length > scopeModule.LARGE_DIFF_THRESHOLD && !options.allowLargeDiff) {
|
|
251
|
+
const err = new Error(
|
|
252
|
+
`cap-migrate-tags: refusing to apply migration to ${changed.length} files ` +
|
|
253
|
+
`(threshold ${scopeModule.LARGE_DIFF_THRESHOLD}). This usually indicates a ` +
|
|
254
|
+
`scope-filter problem — verify the dry-run report is what you intended, then ` +
|
|
255
|
+
`re-run with allowLargeDiff:true to override.`
|
|
256
|
+
);
|
|
257
|
+
err.code = 'CAP_MIGRATE_LARGE_DIFF';
|
|
258
|
+
err.changedCount = changed.length;
|
|
259
|
+
err.threshold = scopeModule.LARGE_DIFF_THRESHOLD;
|
|
260
|
+
throw err;
|
|
261
|
+
}
|
|
262
|
+
const written = [];
|
|
263
|
+
const skipped = [];
|
|
264
|
+
for (const r of results) {
|
|
265
|
+
if (!r.changed || r.newContent == null) {
|
|
266
|
+
skipped.push(r.file);
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
const abs = path.resolve(projectRoot, r.file);
|
|
270
|
+
fs.writeFileSync(abs, r.newContent, 'utf8');
|
|
271
|
+
written.push(r.file);
|
|
272
|
+
}
|
|
273
|
+
return { written, skipped };
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Produce a human-readable dry-run report.
|
|
278
|
+
*
|
|
279
|
+
* @param {FileMigrationResult[]} results
|
|
280
|
+
* @returns {string}
|
|
281
|
+
*/
|
|
282
|
+
function formatMigrationReport(results) {
|
|
283
|
+
const changed = results.filter((r) => r.changed);
|
|
284
|
+
const alreadyHas = results.filter((r) => r.reason === 'already-has-anchor');
|
|
285
|
+
const noTags = results.filter((r) => r.reason === 'no-feature-tags');
|
|
286
|
+
|
|
287
|
+
const lines = [];
|
|
288
|
+
lines.push(`Migration plan — ${changed.length} file(s) would be updated:`);
|
|
289
|
+
lines.push('');
|
|
290
|
+
for (const r of changed) {
|
|
291
|
+
lines.push(` ${r.file}`);
|
|
292
|
+
lines.push(` anchor: ${r.anchorBlock}`);
|
|
293
|
+
lines.push(` consolidates: ${r.consolidatedAcs.join(', ') || '(feature-only, no ACs)'}`);
|
|
294
|
+
lines.push('');
|
|
295
|
+
}
|
|
296
|
+
lines.push(`${alreadyHas.length} file(s) already use unified anchors; skipped.`);
|
|
297
|
+
lines.push(`${noTags.length} file(s) had no feature-bound tags; skipped.`);
|
|
298
|
+
return lines.join('\n').trimEnd();
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
module.exports = {
|
|
302
|
+
commentStyleForFile,
|
|
303
|
+
consolidateGroup,
|
|
304
|
+
findInsertionIndex,
|
|
305
|
+
planFileMigration,
|
|
306
|
+
planProjectMigration,
|
|
307
|
+
applyMigrations,
|
|
308
|
+
formatMigrationReport,
|
|
309
|
+
};
|