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,228 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// @cap-feature(feature:F-047, primary:true) Unified Feature Anchor Block parser.
|
|
4
|
+
// Parses the new single-block syntax introduced by CAP v3:
|
|
5
|
+
//
|
|
6
|
+
// /* @cap feature:F-001 acs:[AC-1,AC-3] role:primary */
|
|
7
|
+
// # @cap feature:F-001 acs:[AC-1,AC-3] role:primary
|
|
8
|
+
// <!-- @cap feature:F-001 acs:[AC-1,AC-3] role:primary -->
|
|
9
|
+
//
|
|
10
|
+
// The parser is language-agnostic — it is called by cap-tag-scanner AFTER comment
|
|
11
|
+
// delimiter stripping, so this module only sees the inner `@cap key:value ...` content.
|
|
12
|
+
//
|
|
13
|
+
// @cap-decision Pure logic, zero side effects. Scanner owns the "is this a comment?" layer
|
|
14
|
+
// (F-046 polylingual detection) and feeds stripped lines into parseAnchorLine(). This keeps
|
|
15
|
+
// the scanner single-source-of-truth for comment detection and avoids duplicating the
|
|
16
|
+
// polyglot rules here.
|
|
17
|
+
// @cap-decision expandAnchorToTags() emits tags in the SAME shape as scanner.extractTags()
|
|
18
|
+
// so all downstream code (buildAcFileMap, cap-deps, cap-completeness, cap-reconcile) works
|
|
19
|
+
// unchanged. Legacy fragmented tags and unified anchors are indistinguishable at the tag
|
|
20
|
+
// consumer layer — only the scanner layer sees the new syntax.
|
|
21
|
+
|
|
22
|
+
// @cap-risk Regex-based. Does not parse nested brackets or quoted key:value pairs.
|
|
23
|
+
// Limitations:
|
|
24
|
+
// - values with `[`, `]`, `,`, or whitespace must not appear unquoted (documented)
|
|
25
|
+
// - no support for multi-line anchor bodies (block must be single line inside its comment)
|
|
26
|
+
// - scanAnchorsInContent() reads raw file content, so a string literal containing
|
|
27
|
+
// `@cap feature:F-XXX` in source (e.g. inside a test fixture) can produce a false
|
|
28
|
+
// match. Callers needing strict string-literal awareness should wire in F-046's
|
|
29
|
+
// polyglot string-stripping upstream. KV_TOKEN_RE rejects most accidental text.
|
|
30
|
+
// These are intentional for v1 to keep parsing unambiguous.
|
|
31
|
+
|
|
32
|
+
// @cap-decision Space discriminator is load-bearing: `@cap ` (with trailing space) is the
|
|
33
|
+
// unified anchor marker, `@cap-` (with hyphen) is the legacy tag family. The two formats
|
|
34
|
+
// never collide because no legal tag name contains whitespace, and no legal anchor lacks
|
|
35
|
+
// the space between `@cap` and the first key. Documented in docs/F-047-decision.md.
|
|
36
|
+
|
|
37
|
+
// Matches `@cap <rest>` — called on an already-decommented line or the inner content of a
|
|
38
|
+
// block comment. Captures `rest` which is then tokenised into key:value pairs.
|
|
39
|
+
const ANCHOR_RE = /@cap\s+([^\n]+)/;
|
|
40
|
+
|
|
41
|
+
// Matches `key:value` where value is either `[list,of,items]` or a bare token (no whitespace,
|
|
42
|
+
// no commas, no brackets). The anchor body is split into key:value pairs by whitespace.
|
|
43
|
+
const KV_TOKEN_RE = /^([a-zA-Z][a-zA-Z0-9_]*)\s*:\s*(\[[^\]]*\]|[^\s\[\]]+)$/;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* @typedef {Object} ParsedAnchor
|
|
47
|
+
* @property {string} feature - Feature ID (e.g. 'F-001'); required
|
|
48
|
+
* @property {string[]} acs - AC IDs (e.g. ['AC-1','AC-3']); empty when not specified
|
|
49
|
+
* @property {('primary'|'secondary'|null)} role - 'primary', 'secondary', or null (unspecified)
|
|
50
|
+
* @property {string} raw - The original `@cap …` text for error reporting
|
|
51
|
+
* @property {string[]} warnings - Soft warnings (unknown keys, malformed AC ids, …)
|
|
52
|
+
*/
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Parse a single `@cap key:value …` body (the content inside the comment, already
|
|
56
|
+
* stripped of delimiters like `/*`, `*` /, `#`, `<!--`, `-->`).
|
|
57
|
+
*
|
|
58
|
+
* Returns null when no `@cap` token is present or the line is completely malformed.
|
|
59
|
+
* When the token is present but some keys are unrecognised or values are malformed,
|
|
60
|
+
* still returns a ParsedAnchor with the recognised subset plus a `warnings` array so
|
|
61
|
+
* callers can surface soft failures without losing usable information.
|
|
62
|
+
*
|
|
63
|
+
* @param {string} line
|
|
64
|
+
* @returns {ParsedAnchor|null}
|
|
65
|
+
*/
|
|
66
|
+
function parseAnchorLine(line) {
|
|
67
|
+
if (typeof line !== 'string') return null;
|
|
68
|
+
const m = line.match(ANCHOR_RE);
|
|
69
|
+
if (!m) return null;
|
|
70
|
+
|
|
71
|
+
const body = m[1].trim();
|
|
72
|
+
// Strip trailing comment delimiters that may have leaked through (e.g. `-->` or `*/`)
|
|
73
|
+
const cleaned = body
|
|
74
|
+
.replace(/\s*-->\s*$/, '')
|
|
75
|
+
.replace(/\s*\*\/\s*$/, '')
|
|
76
|
+
.trim();
|
|
77
|
+
|
|
78
|
+
/** @type {ParsedAnchor} */
|
|
79
|
+
const out = { feature: '', acs: [], role: null, raw: m[0], warnings: [] };
|
|
80
|
+
if (cleaned.length === 0) {
|
|
81
|
+
out.warnings.push('empty anchor body');
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Tokenise by whitespace; each token must match key:value.
|
|
86
|
+
const tokens = cleaned.split(/\s+/);
|
|
87
|
+
for (const tok of tokens) {
|
|
88
|
+
const km = tok.match(KV_TOKEN_RE);
|
|
89
|
+
if (!km) {
|
|
90
|
+
out.warnings.push(`unparseable token: ${tok}`);
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
const key = km[1];
|
|
94
|
+
const value = km[2];
|
|
95
|
+
switch (key) {
|
|
96
|
+
case 'feature':
|
|
97
|
+
if (!/^F-\d{3,}$/.test(value)) {
|
|
98
|
+
out.warnings.push(`feature value must match /^F-\\d{3,}$/ (got ${value})`);
|
|
99
|
+
}
|
|
100
|
+
out.feature = value;
|
|
101
|
+
break;
|
|
102
|
+
case 'acs': {
|
|
103
|
+
if (!value.startsWith('[') || !value.endsWith(']')) {
|
|
104
|
+
out.warnings.push(`acs must be [bracketed,list] (got ${value})`);
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
const inner = value.slice(1, -1).trim();
|
|
108
|
+
const items = inner.length === 0 ? [] : inner.split(',').map((s) => s.trim()).filter(Boolean);
|
|
109
|
+
for (const ac of items) {
|
|
110
|
+
if (!/^AC-\d+$/.test(ac)) {
|
|
111
|
+
out.warnings.push(`acs item must match /^AC-\\d+$/ (got ${ac})`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
out.acs = items;
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
case 'role':
|
|
118
|
+
if (value !== 'primary' && value !== 'secondary') {
|
|
119
|
+
out.warnings.push(`role must be 'primary' or 'secondary' (got ${value})`);
|
|
120
|
+
}
|
|
121
|
+
out.role = value;
|
|
122
|
+
break;
|
|
123
|
+
default:
|
|
124
|
+
out.warnings.push(`unknown key: ${key}`);
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return out;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Expand a parsed anchor into the CapTag[] shape used elsewhere in CAP.
|
|
134
|
+
* Emits:
|
|
135
|
+
* - one @cap-feature tag (primary:true flag when role === 'primary')
|
|
136
|
+
* - one @cap-todo tag per AC listed in `anchor.acs`, with `ac: F-XXX/AC-N`
|
|
137
|
+
*
|
|
138
|
+
* When anchor.feature is empty (parse error), returns [] — the caller can still
|
|
139
|
+
* inspect anchor.warnings for diagnostics.
|
|
140
|
+
*
|
|
141
|
+
* @param {ParsedAnchor} anchor
|
|
142
|
+
* @param {string} filePath - Relative file path (for tag.file)
|
|
143
|
+
* @param {number} lineNumber - 1-based line number of the anchor in the source
|
|
144
|
+
* @returns {CapTag[]}
|
|
145
|
+
*/
|
|
146
|
+
function expandAnchorToTags(anchor, filePath, lineNumber) {
|
|
147
|
+
if (!anchor || !anchor.feature) return [];
|
|
148
|
+
/** @type {CapTag[]} */
|
|
149
|
+
const tags = [];
|
|
150
|
+
const metadata = { feature: anchor.feature };
|
|
151
|
+
if (anchor.role === 'primary') metadata.primary = true;
|
|
152
|
+
tags.push({
|
|
153
|
+
type: 'feature',
|
|
154
|
+
file: filePath,
|
|
155
|
+
line: lineNumber,
|
|
156
|
+
metadata,
|
|
157
|
+
description: `unified anchor for ${anchor.feature}`,
|
|
158
|
+
raw: anchor.raw,
|
|
159
|
+
});
|
|
160
|
+
for (const ac of anchor.acs || []) {
|
|
161
|
+
tags.push({
|
|
162
|
+
type: 'todo',
|
|
163
|
+
file: filePath,
|
|
164
|
+
line: lineNumber,
|
|
165
|
+
metadata: { ac: `${anchor.feature}/${ac}` },
|
|
166
|
+
description: `AC reference expanded from unified anchor`,
|
|
167
|
+
raw: anchor.raw,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
return tags;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Serialize a structured anchor into the canonical block string, using the
|
|
175
|
+
* requested comment style. Used by the migration tool to write the unified
|
|
176
|
+
* block back to source.
|
|
177
|
+
*
|
|
178
|
+
* @param {{feature:string, acs?:string[], role?:string}} anchor
|
|
179
|
+
* @param {('block'|'line'|'html')} [style='block'] - comment family
|
|
180
|
+
* @returns {string} Single-line block, no trailing newline
|
|
181
|
+
*/
|
|
182
|
+
function emitAnchorBlock(anchor, style = 'block') {
|
|
183
|
+
const parts = [];
|
|
184
|
+
parts.push(`feature:${anchor.feature}`);
|
|
185
|
+
if (Array.isArray(anchor.acs) && anchor.acs.length > 0) {
|
|
186
|
+
parts.push(`acs:[${anchor.acs.join(',')}]`);
|
|
187
|
+
}
|
|
188
|
+
if (anchor.role === 'primary' || anchor.role === 'secondary') {
|
|
189
|
+
parts.push(`role:${anchor.role}`);
|
|
190
|
+
}
|
|
191
|
+
const body = `@cap ${parts.join(' ')}`;
|
|
192
|
+
if (style === 'line') return `# ${body}`;
|
|
193
|
+
if (style === 'html') return `<!-- ${body} -->`;
|
|
194
|
+
// default: block comment
|
|
195
|
+
return `/* ${body} */`;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Convenience: scan a full file content for unified anchor blocks and expand each.
|
|
200
|
+
* Internal use by cap-tag-scanner when unifiedAnchors.enabled is true.
|
|
201
|
+
*
|
|
202
|
+
* @param {string} content - Full file content
|
|
203
|
+
* @param {string} filePath - Relative path for tag.file
|
|
204
|
+
* @returns {CapTag[]} All tags expanded from every anchor in the file
|
|
205
|
+
*/
|
|
206
|
+
function scanAnchorsInContent(content, filePath) {
|
|
207
|
+
if (typeof content !== 'string' || content.length === 0) return [];
|
|
208
|
+
const tags = [];
|
|
209
|
+
const lines = content.split('\n');
|
|
210
|
+
for (let i = 0; i < lines.length; i++) {
|
|
211
|
+
const line = lines[i];
|
|
212
|
+
if (!line.includes('@cap ')) continue; // fast-path filter; space distinguishes from `@cap-feature`
|
|
213
|
+
const parsed = parseAnchorLine(line);
|
|
214
|
+
if (!parsed || !parsed.feature) continue;
|
|
215
|
+
tags.push(...expandAnchorToTags(parsed, filePath, i + 1));
|
|
216
|
+
}
|
|
217
|
+
return tags;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
module.exports = {
|
|
221
|
+
parseAnchorLine,
|
|
222
|
+
expandAnchorToTags,
|
|
223
|
+
emitAnchorBlock,
|
|
224
|
+
scanAnchorsInContent,
|
|
225
|
+
// constants (exported for tests)
|
|
226
|
+
ANCHOR_RE,
|
|
227
|
+
KV_TOKEN_RE,
|
|
228
|
+
};
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
// @cap-feature(feature:F-028) Code Annotation Writer — write @cap-history, @cap-pitfall, @cap-pattern annotations into source files
|
|
2
|
+
// @cap-decision Annotations placed at file-top block, after shebang/'use strict', alongside existing @cap-feature tags.
|
|
3
|
+
// @cap-decision Comment syntax detected per file extension — language-agnostic, same approach as tag scanner.
|
|
4
|
+
// @cap-constraint Zero external dependencies — uses only Node.js built-ins.
|
|
5
|
+
|
|
6
|
+
'use strict';
|
|
7
|
+
|
|
8
|
+
// @cap-history(sessions:2, edits:7, since:2026-04-03, learned:2026-04-03) Frequently modified — 2 sessions, 7 edits
|
|
9
|
+
const fs = require('node:fs');
|
|
10
|
+
const path = require('node:path');
|
|
11
|
+
|
|
12
|
+
// --- Comment Syntax Detection (AC-2) ---
|
|
13
|
+
|
|
14
|
+
/** @type {Object<string, string>} Extension to single-line comment prefix mapping */
|
|
15
|
+
const COMMENT_PREFIX_MAP = {
|
|
16
|
+
// // style
|
|
17
|
+
'.js': '//', '.cjs': '//', '.mjs': '//', '.ts': '//', '.tsx': '//', '.jsx': '//',
|
|
18
|
+
'.go': '//', '.rs': '//', '.c': '//', '.cpp': '//', '.h': '//', '.java': '//',
|
|
19
|
+
'.swift': '//', '.kt': '//', '.scala': '//', '.cs': '//', '.dart': '//', '.zig': '//',
|
|
20
|
+
// # style
|
|
21
|
+
'.py': '#', '.rb': '#', '.sh': '#', '.bash': '#', '.zsh': '#', '.fish': '#',
|
|
22
|
+
'.yml': '#', '.yaml': '#', '.toml': '#', '.pl': '#', '.pm': '#', '.r': '#',
|
|
23
|
+
'.tf': '#', '.hcl': '#', '.dockerfile': '#', '.conf': '#', '.ini': '#',
|
|
24
|
+
// -- style
|
|
25
|
+
'.sql': '--', '.lua': '--', '.hs': '--', '.elm': '--',
|
|
26
|
+
// ; style
|
|
27
|
+
'.lisp': ';', '.clj': ';', '.el': ';', '.scm': ';',
|
|
28
|
+
// % style
|
|
29
|
+
'.erl': '%', '.tex': '%', '.m': '%',
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// @cap-todo(ref:F-028:AC-2) Detect correct comment syntax for target file based on extension
|
|
33
|
+
|
|
34
|
+
/** File extensions that must never receive annotations (no valid comment syntax or structured format). */
|
|
35
|
+
const ANNOTATION_BLOCKLIST = new Set([
|
|
36
|
+
'.md', '.markdown', '.json', '.jsonl', '.lock', '.svg', '.xml', '.html', '.htm',
|
|
37
|
+
'.css', '.scss', '.less', '.png', '.jpg', '.jpeg', '.gif', '.ico', '.woff', '.woff2',
|
|
38
|
+
'.ttf', '.eot', '.map', '.min.js', '.min.css', '.patch', '.diff',
|
|
39
|
+
]);
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Check if a file can receive annotations.
|
|
43
|
+
* @param {string} filePath
|
|
44
|
+
* @returns {boolean}
|
|
45
|
+
*/
|
|
46
|
+
function canAnnotate(filePath) {
|
|
47
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
48
|
+
const basename = path.basename(filePath).toLowerCase();
|
|
49
|
+
if (ANNOTATION_BLOCKLIST.has(ext)) return false;
|
|
50
|
+
if (basename === 'package-lock.json' || basename === 'yarn.lock' || basename === 'pnpm-lock.yaml') return false;
|
|
51
|
+
if (basename.endsWith('.md')) return false;
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Get the single-line comment prefix for a file.
|
|
57
|
+
* @param {string} filePath
|
|
58
|
+
* @returns {string} Comment prefix (defaults to '//')
|
|
59
|
+
*/
|
|
60
|
+
function getCommentPrefix(filePath) {
|
|
61
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
62
|
+
// Dockerfile has no extension but starts with FROM
|
|
63
|
+
if (path.basename(filePath).toLowerCase() === 'dockerfile') return '#';
|
|
64
|
+
return COMMENT_PREFIX_MAP[ext] || '//';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// --- Annotation Parsing ---
|
|
68
|
+
|
|
69
|
+
/** Regex to match existing memory annotations in a line */
|
|
70
|
+
const MEMORY_TAG_RE = /^(\s*(?:\/\/|#|--|;|%)\s*)@(cap-history|cap-pitfall|cap-pattern|cap-decision)\b/;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* @typedef {Object} ParsedAnnotation
|
|
74
|
+
* @property {number} lineIndex - 0-based line index in file
|
|
75
|
+
* @property {string} tag - Tag name (e.g., 'cap-history')
|
|
76
|
+
* @property {string} fullLine - Complete line text
|
|
77
|
+
* @property {string} prefix - Comment prefix with whitespace
|
|
78
|
+
*/
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Parse existing memory annotations from file lines.
|
|
82
|
+
* @param {string[]} lines
|
|
83
|
+
* @returns {ParsedAnnotation[]}
|
|
84
|
+
*/
|
|
85
|
+
function parseExistingAnnotations(lines) {
|
|
86
|
+
const annotations = [];
|
|
87
|
+
for (let i = 0; i < lines.length; i++) {
|
|
88
|
+
const match = lines[i].match(MEMORY_TAG_RE);
|
|
89
|
+
if (match) {
|
|
90
|
+
annotations.push({
|
|
91
|
+
lineIndex: i,
|
|
92
|
+
tag: match[2],
|
|
93
|
+
fullLine: lines[i],
|
|
94
|
+
prefix: match[1],
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return annotations;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// --- Insertion Point Detection ---
|
|
102
|
+
|
|
103
|
+
// @cap-todo(ref:F-028:AC-1) Insert annotations at file-top block alongside existing @cap-feature tags
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Find the line index where memory annotations should be inserted.
|
|
107
|
+
* After shebang, 'use strict', and existing @cap-* annotation block.
|
|
108
|
+
* @param {string[]} lines
|
|
109
|
+
* @returns {number} Line index for insertion
|
|
110
|
+
*/
|
|
111
|
+
function findInsertionPoint(lines) {
|
|
112
|
+
let insertAt = 0;
|
|
113
|
+
|
|
114
|
+
for (let i = 0; i < lines.length; i++) {
|
|
115
|
+
const line = lines[i].trim();
|
|
116
|
+
|
|
117
|
+
// Skip shebang
|
|
118
|
+
if (i === 0 && line.startsWith('#!')) {
|
|
119
|
+
insertAt = i + 1;
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Skip 'use strict'
|
|
124
|
+
if (line === "'use strict';" || line === '"use strict";') {
|
|
125
|
+
insertAt = i + 1;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Skip empty lines at top
|
|
130
|
+
if (line === '' && i <= insertAt) {
|
|
131
|
+
insertAt = i + 1;
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Skip existing @cap-* annotation lines
|
|
136
|
+
if (/^\s*(?:\/\/|#|--|;|%)\s*@cap-/.test(line)) {
|
|
137
|
+
insertAt = i + 1;
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Stop at first non-annotation, non-header line
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return insertAt;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// --- Write Operations ---
|
|
149
|
+
|
|
150
|
+
// @cap-todo(ref:F-028:AC-3) Update existing annotations in-place without creating duplicates
|
|
151
|
+
// @cap-todo(ref:F-028:AC-4) Remove annotations marked as stale
|
|
152
|
+
// @cap-todo(ref:F-028:AC-7) Support dry-run mode
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* @typedef {Object} AnnotationChange
|
|
156
|
+
* @property {'add'|'update'|'remove'} action
|
|
157
|
+
* @property {string} file
|
|
158
|
+
* @property {string} annotation - Formatted annotation text
|
|
159
|
+
* @property {number} [lineIndex] - For update/remove: existing line
|
|
160
|
+
*/
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Plan annotation changes for a single file.
|
|
164
|
+
* @param {string} filePath
|
|
165
|
+
* @param {string} fileContent - Current file content
|
|
166
|
+
* @param {import('./cap-memory-engine.cjs').MemoryEntry[]} entries - New entries for this file
|
|
167
|
+
* @param {string[]} [staleContentPrefixes] - Content prefixes of stale entries to remove
|
|
168
|
+
* @returns {AnnotationChange[]}
|
|
169
|
+
*/
|
|
170
|
+
function planFileChanges(filePath, fileContent, entries, staleContentPrefixes = []) {
|
|
171
|
+
const commentPrefix = getCommentPrefix(filePath);
|
|
172
|
+
const lines = fileContent.split('\n');
|
|
173
|
+
const existing = parseExistingAnnotations(lines);
|
|
174
|
+
const changes = [];
|
|
175
|
+
|
|
176
|
+
const { formatAnnotation } = require('./cap-memory-engine.cjs');
|
|
177
|
+
|
|
178
|
+
// Plan removals for stale entries (AC-4)
|
|
179
|
+
for (const ann of existing) {
|
|
180
|
+
const lineContent = ann.fullLine.replace(ann.prefix, '').trim();
|
|
181
|
+
if (staleContentPrefixes.some(prefix => lineContent.startsWith(prefix))) {
|
|
182
|
+
changes.push({ action: 'remove', file: filePath, annotation: ann.fullLine, lineIndex: ann.lineIndex });
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Plan adds/updates for new entries
|
|
187
|
+
for (const entry of entries) {
|
|
188
|
+
const formatted = formatAnnotation(entry);
|
|
189
|
+
// @cap-todo(ac:F-086/AC-1) Dedup matcher: aggregate annotations (one-per-file) like
|
|
190
|
+
// @cap-history must match by TAG NAME alone, since their content carries live edit
|
|
191
|
+
// counts that change between runs. Matching by content-prefix caused a duplicate
|
|
192
|
+
// line every time the stats changed (observed on GoetzeInvest hub-types.ts).
|
|
193
|
+
// Per-occurrence annotations (@cap-pitfall, @cap-pattern, @cap-decision) keep the
|
|
194
|
+
// content-prefix match because multiple distinct ones can legitimately coexist.
|
|
195
|
+
const tagName = formatted.split('(')[0].split(' ')[0]; // e.g., @cap-history
|
|
196
|
+
const contentKey = entry.content.substring(0, 60).toLowerCase();
|
|
197
|
+
const isAggregateTag = tagName === '@cap-history';
|
|
198
|
+
|
|
199
|
+
const existingMatch = existing.find(ann => {
|
|
200
|
+
const annContent = ann.fullLine.replace(ann.prefix, '').trim();
|
|
201
|
+
if (!annContent.startsWith(tagName)) return false;
|
|
202
|
+
if (isAggregateTag) return true; // any existing @cap-history on this file is "the" one
|
|
203
|
+
return annContent.toLowerCase().includes(contentKey);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
if (existingMatch) {
|
|
207
|
+
// Update in-place (AC-3)
|
|
208
|
+
const newLine = `${commentPrefix} ${formatted}`;
|
|
209
|
+
if (existingMatch.fullLine.trim() !== newLine.trim()) {
|
|
210
|
+
changes.push({ action: 'update', file: filePath, annotation: newLine, lineIndex: existingMatch.lineIndex });
|
|
211
|
+
}
|
|
212
|
+
} else {
|
|
213
|
+
// Add new
|
|
214
|
+
changes.push({ action: 'add', file: filePath, annotation: `${commentPrefix} ${formatted}` });
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return changes;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Apply planned changes to a file's content.
|
|
223
|
+
* @param {string} fileContent
|
|
224
|
+
* @param {AnnotationChange[]} changes
|
|
225
|
+
* @returns {string} Updated file content
|
|
226
|
+
*/
|
|
227
|
+
function applyChanges(fileContent, changes) {
|
|
228
|
+
const lines = fileContent.split('\n');
|
|
229
|
+
|
|
230
|
+
// Apply removals and updates (by line index, process from bottom to preserve indices)
|
|
231
|
+
const lineChanges = changes
|
|
232
|
+
.filter(c => c.action === 'remove' || c.action === 'update')
|
|
233
|
+
.sort((a, b) => b.lineIndex - a.lineIndex);
|
|
234
|
+
|
|
235
|
+
for (const change of lineChanges) {
|
|
236
|
+
if (change.action === 'remove') {
|
|
237
|
+
lines.splice(change.lineIndex, 1);
|
|
238
|
+
} else if (change.action === 'update') {
|
|
239
|
+
lines[change.lineIndex] = change.annotation;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Apply additions at insertion point
|
|
244
|
+
const additions = changes.filter(c => c.action === 'add');
|
|
245
|
+
if (additions.length > 0) {
|
|
246
|
+
const insertAt = findInsertionPoint(lines);
|
|
247
|
+
const newLines = additions.map(a => a.annotation);
|
|
248
|
+
lines.splice(insertAt, 0, ...newLines);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return lines.join('\n');
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// @cap-todo(ref:F-028:AC-5) Format annotations with parenthesized metadata matching existing tag conventions
|
|
255
|
+
// @cap-todo(ref:F-028:AC-6) Be parseable by existing tag scanner without modifications
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Write memory annotations to files.
|
|
259
|
+
* @param {Object<string, import('./cap-memory-engine.cjs').MemoryEntry[]>} fileEntries - Map of filePath -> entries
|
|
260
|
+
* @param {Object} [options]
|
|
261
|
+
* @param {boolean} [options.dryRun] - If true, return changes without writing
|
|
262
|
+
* @param {Object<string, string[]>} [options.staleByFile] - Map of filePath -> stale content prefixes to remove
|
|
263
|
+
* @returns {{changes: AnnotationChange[], filesModified: number}}
|
|
264
|
+
*/
|
|
265
|
+
function writeAnnotations(fileEntries, options = {}) {
|
|
266
|
+
const allChanges = [];
|
|
267
|
+
let filesModified = 0;
|
|
268
|
+
|
|
269
|
+
for (const [filePath, entries] of Object.entries(fileEntries)) {
|
|
270
|
+
if (!fs.existsSync(filePath)) continue;
|
|
271
|
+
if (!canAnnotate(filePath)) continue;
|
|
272
|
+
|
|
273
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
274
|
+
const stale = options.staleByFile?.[filePath] || [];
|
|
275
|
+
const changes = planFileChanges(filePath, content, entries, stale);
|
|
276
|
+
|
|
277
|
+
if (changes.length === 0) continue;
|
|
278
|
+
|
|
279
|
+
allChanges.push(...changes);
|
|
280
|
+
|
|
281
|
+
if (!options.dryRun) {
|
|
282
|
+
const updated = applyChanges(content, changes);
|
|
283
|
+
fs.writeFileSync(filePath, updated, 'utf8');
|
|
284
|
+
}
|
|
285
|
+
filesModified++;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return { changes: allChanges, filesModified };
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Remove stale annotations from files.
|
|
293
|
+
* @param {Array<{file: string, content: string}>} staleEntries
|
|
294
|
+
* @param {Object} [options]
|
|
295
|
+
* @param {boolean} [options.dryRun]
|
|
296
|
+
* @returns {{removed: number, filesModified: number}}
|
|
297
|
+
*/
|
|
298
|
+
function removeStaleAnnotations(staleEntries, options = {}) {
|
|
299
|
+
const byFile = {};
|
|
300
|
+
for (const entry of staleEntries) {
|
|
301
|
+
if (!byFile[entry.file]) byFile[entry.file] = [];
|
|
302
|
+
byFile[entry.file].push(entry.content.substring(0, 60));
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
let removed = 0;
|
|
306
|
+
let filesModified = 0;
|
|
307
|
+
|
|
308
|
+
for (const [filePath, prefixes] of Object.entries(byFile)) {
|
|
309
|
+
if (!fs.existsSync(filePath)) continue;
|
|
310
|
+
|
|
311
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
312
|
+
const changes = planFileChanges(filePath, content, [], prefixes);
|
|
313
|
+
const removals = changes.filter(c => c.action === 'remove');
|
|
314
|
+
|
|
315
|
+
if (removals.length === 0) continue;
|
|
316
|
+
|
|
317
|
+
if (!options.dryRun) {
|
|
318
|
+
const updated = applyChanges(content, removals);
|
|
319
|
+
fs.writeFileSync(filePath, updated, 'utf8');
|
|
320
|
+
}
|
|
321
|
+
removed += removals.length;
|
|
322
|
+
filesModified++;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return { removed, filesModified };
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
module.exports = {
|
|
329
|
+
canAnnotate,
|
|
330
|
+
getCommentPrefix,
|
|
331
|
+
parseExistingAnnotations,
|
|
332
|
+
findInsertionPoint,
|
|
333
|
+
planFileChanges,
|
|
334
|
+
applyChanges,
|
|
335
|
+
writeAnnotations,
|
|
336
|
+
removeStaleAnnotations,
|
|
337
|
+
COMMENT_PREFIX_MAP,
|
|
338
|
+
ANNOTATION_BLOCKLIST,
|
|
339
|
+
MEMORY_TAG_RE,
|
|
340
|
+
};
|