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,371 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// @cap-feature(feature:F-054) Hook-Based Tag Event Observation — pure logic module (extractTags, diffTags, snapshot I/O, event append).
|
|
4
|
+
// @cap-decision Snapshot-basiert statt PreToolUse — ein einheitlicher Code-Pfad für alle vier Tools (Edit/Write/MultiEdit/NotebookEdit), robust gegen Tool-Input-Schema-Änderungen, weil wir immer den aktuellen Datei-Inhalt neu lesen und gegen einen persistenten Snapshot diffen.
|
|
5
|
+
// @cap-constraint Zero external dependencies — nur node:-prefixed Built-ins (fs, path, crypto).
|
|
6
|
+
|
|
7
|
+
// @cap-history(sessions:2, edits:5, since:2026-04-20, learned:2026-04-21) Frequently modified — 2 sessions, 5 edits
|
|
8
|
+
const fs = require('node:fs');
|
|
9
|
+
const path = require('node:path');
|
|
10
|
+
const crypto = require('node:crypto');
|
|
11
|
+
|
|
12
|
+
// @cap-decision Tag-Regex bewusst eng an cap-tag-scanner.cjs gehalten (dort: `/^[ \t]*(?:\/\/|\/\*|\*|#|--|"""|''')[ \t]*@cap-(feature|todo|risk|decision)(?:\(([^)]*)\))?[ \t]*(.*)/`). Für F-054 beschränken wir uns explizit auf `feature|todo` (Scope des Memory-Events) und matchen Comment-Tokens zeilengenau, damit @cap-Strings innerhalb von String-Literalen oder Prosa nicht gezählt werden.
|
|
13
|
+
const TAG_LINE_RE = /^[ \t]*(?:\/\/|\/\*|\*|#|--|"""|''')[ \t]*@cap-(?:feature|todo)(?:[ \t]*\([^)]*\))?[ \t]*.*$/;
|
|
14
|
+
|
|
15
|
+
// Matches the tag token (type + optional metadata in parens). Global flag enables matchAll so multiple tokens on the same comment line are captured; `[ \t]*` before the paren tolerates `@cap-todo ( ac:F-1/AC-1 )`-style whitespace which is then stripped by the caller's normalisation step.
|
|
16
|
+
const TAG_TOKEN_RE = /@cap-(?:feature|todo)(?:[ \t]*\([^)]*\))?/g;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Extract @cap-feature and @cap-todo tags from file content.
|
|
20
|
+
*
|
|
21
|
+
* Returns normalized tag identities as strings (e.g. `@cap-todo(ac:F-054/AC-1)`).
|
|
22
|
+
* Metadata inside parens is part of the identity, so
|
|
23
|
+
* `@cap-todo(ac:F-054/AC-1)` and `@cap-todo(ac:F-054/AC-2)` are distinct tags.
|
|
24
|
+
* Duplicates within the same file are deduplicated (Set-based).
|
|
25
|
+
*
|
|
26
|
+
* @cap-todo(ac:F-054/AC-2) extractTags ist die linke Hand des Diff — jede
|
|
27
|
+
* Datei wird zu einer dedup'd Menge von @cap-feature/@cap-todo Tags normalisiert,
|
|
28
|
+
* damit added/removed gegen den Snapshot ein reines Set-Delta bleibt.
|
|
29
|
+
*
|
|
30
|
+
* @param {string} content - File content to scan
|
|
31
|
+
* @returns {string[]} Sorted unique tag identities
|
|
32
|
+
*/
|
|
33
|
+
function extractTags(content) {
|
|
34
|
+
if (typeof content !== 'string' || content.length === 0) return [];
|
|
35
|
+
const seen = new Set();
|
|
36
|
+
const lines = content.split('\n');
|
|
37
|
+
for (const line of lines) {
|
|
38
|
+
if (!TAG_LINE_RE.test(line)) continue;
|
|
39
|
+
for (const match of line.matchAll(TAG_TOKEN_RE)) {
|
|
40
|
+
// Normalise internal whitespace: `@cap-todo ( ac:F-1/AC-1 )` -> `@cap-todo(ac:F-1/AC-1)`.
|
|
41
|
+
const tag = match[0].replace(/\s+/g, '');
|
|
42
|
+
seen.add(tag);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return Array.from(seen).sort();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Compute added/removed tag sets between two normalized tag arrays.
|
|
50
|
+
*
|
|
51
|
+
* @cap-todo(ac:F-054/AC-2) diffTags erzeugt das Delta, das später als JSONL-Event
|
|
52
|
+
* persistiert wird. Dedupe geschieht per Set, Inputs müssen nicht sortiert sein.
|
|
53
|
+
*
|
|
54
|
+
* @param {string[]} before - Previous tag set
|
|
55
|
+
* @param {string[]} after - Current tag set
|
|
56
|
+
* @returns {{added: string[], removed: string[]}}
|
|
57
|
+
*/
|
|
58
|
+
function diffTags(before, after) {
|
|
59
|
+
const beforeSet = new Set(Array.isArray(before) ? before : []);
|
|
60
|
+
const afterSet = new Set(Array.isArray(after) ? after : []);
|
|
61
|
+
const added = [];
|
|
62
|
+
const removed = [];
|
|
63
|
+
for (const t of afterSet) if (!beforeSet.has(t)) added.push(t);
|
|
64
|
+
for (const t of beforeSet) if (!afterSet.has(t)) removed.push(t);
|
|
65
|
+
added.sort();
|
|
66
|
+
removed.sort();
|
|
67
|
+
return { added, removed };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Compute the absolute path of a snapshot file for a given observed source file.
|
|
72
|
+
* Uses SHA-1 of the absolute source path, stored under `.snapshots/`.
|
|
73
|
+
*
|
|
74
|
+
* @param {string} rawDir - Absolute path to `.cap/memory/raw`
|
|
75
|
+
* @param {string} filePath - Path to the observed file (relative or absolute)
|
|
76
|
+
* @returns {string} Absolute path to the snapshot JSON file
|
|
77
|
+
*/
|
|
78
|
+
function snapshotPath(rawDir, filePath) {
|
|
79
|
+
const abs = path.resolve(filePath);
|
|
80
|
+
const hash = crypto.createHash('sha1').update(abs).digest('hex');
|
|
81
|
+
return path.join(rawDir, '.snapshots', `${hash}.json`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Ensure a directory exists (recursive mkdir, idempotent).
|
|
86
|
+
* @param {string} dir
|
|
87
|
+
*/
|
|
88
|
+
function ensureDir(dir) {
|
|
89
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Load a previously persisted tag snapshot.
|
|
94
|
+
*
|
|
95
|
+
* A missing snapshot is an expected "first observation" state — silent null.
|
|
96
|
+
* A corrupt snapshot (non-JSON, wrong shape) is an anomaly worth a breadcrumb
|
|
97
|
+
* so we can later correlate spurious `removed`-events with snapshot damage.
|
|
98
|
+
*
|
|
99
|
+
* @param {string} rawDir
|
|
100
|
+
* @param {string} filePath
|
|
101
|
+
* @returns {{file:string, tags:string[], mtime:(number|null), updatedAt:string}|null}
|
|
102
|
+
*/
|
|
103
|
+
function loadSnapshot(rawDir, filePath) {
|
|
104
|
+
const snap = snapshotPath(rawDir, filePath);
|
|
105
|
+
let raw;
|
|
106
|
+
try {
|
|
107
|
+
raw = fs.readFileSync(snap, 'utf8');
|
|
108
|
+
} catch {
|
|
109
|
+
// ENOENT / EACCES / first-ever observation — no breadcrumb, caller treats as fresh.
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
try {
|
|
113
|
+
const parsed = JSON.parse(raw);
|
|
114
|
+
if (!parsed || !Array.isArray(parsed.tags)) {
|
|
115
|
+
logError(rawDir, new Error(`corrupt snapshot (missing tags array): ${snap}`));
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
return parsed;
|
|
119
|
+
} catch (err) {
|
|
120
|
+
logError(rawDir, new Error(`corrupt snapshot (${err.message}): ${snap}`));
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Write a tag snapshot atomically (tmp + rename).
|
|
127
|
+
*
|
|
128
|
+
* @cap-decision Atomic tmp+rename statt direktem Write, damit ein abgebrochener
|
|
129
|
+
* Hook (z.B. durch SIGTERM des Parent-Claude-Prozesses) nie einen halb-
|
|
130
|
+
* geschriebenen Snapshot hinterlässt, der beim nächsten Lauf als `tags:[]`
|
|
131
|
+
* interpretiert würde und einen falschen `removed`-Event produziert.
|
|
132
|
+
*
|
|
133
|
+
* @param {string} rawDir
|
|
134
|
+
* @param {string} filePath
|
|
135
|
+
* @param {{file:string, tags:string[], mtime:(number|null), updatedAt:string}} data
|
|
136
|
+
*/
|
|
137
|
+
function writeSnapshot(rawDir, filePath, data) {
|
|
138
|
+
const snap = snapshotPath(rawDir, filePath);
|
|
139
|
+
ensureDir(path.dirname(snap));
|
|
140
|
+
// 8 random bytes disambiguate two hook invocations that land in the same
|
|
141
|
+
// millisecond from the same pid (e.g. parallel Edit calls under CI) — Date.now()
|
|
142
|
+
// alone is insufficient because rename(2) on an existing tmp is a data-race.
|
|
143
|
+
const tmp = `${snap}.${process.pid}.${Date.now()}.${crypto.randomBytes(8).toString('hex')}.tmp`;
|
|
144
|
+
fs.writeFileSync(tmp, JSON.stringify(data), 'utf8');
|
|
145
|
+
fs.renameSync(tmp, snap);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Format a date into the daily log filename suffix (UTC-stable `YYYY-MM-DD`).
|
|
150
|
+
*
|
|
151
|
+
* @cap-decision UTC statt Lokalzeit, damit Log-Rotation zwischen Maschinen /
|
|
152
|
+
* CI-Runnern mit unterschiedlichen TZ deterministisch bleibt und
|
|
153
|
+
* /cap:memory prune (F-056) den Tagesstempel einfach vergleichen kann.
|
|
154
|
+
*
|
|
155
|
+
* @param {Date} date
|
|
156
|
+
* @returns {string}
|
|
157
|
+
*/
|
|
158
|
+
function dayStamp(date) {
|
|
159
|
+
const y = date.getUTCFullYear();
|
|
160
|
+
const m = String(date.getUTCMonth() + 1).padStart(2, '0');
|
|
161
|
+
const d = String(date.getUTCDate()).padStart(2, '0');
|
|
162
|
+
return `${y}-${m}-${d}`;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Append a JSONL event line to the daily tag-events log.
|
|
167
|
+
*
|
|
168
|
+
* @cap-todo(ac:F-054/AC-3) appendEvent schreibt eine Zeile
|
|
169
|
+
* {timestamp, tool, file, added[], removed[]} nach
|
|
170
|
+
* `.cap/memory/raw/tag-events-{YYYY-MM-DD}.jsonl`.
|
|
171
|
+
* @cap-todo(ac:F-054/AC-7) Tages-Rotation: Der Dateiname trägt das Datum, der
|
|
172
|
+
* Cleanup von >30 Tage alten Files ist F-056's Job.
|
|
173
|
+
*
|
|
174
|
+
* @cap-risk `fs.appendFileSync` ist auf POSIX atomar für ≤ PIPE_BUF-sized writes
|
|
175
|
+
* (≥4 KiB, typischer JSONL-Event ist weit darunter). Auf Windows gibt es
|
|
176
|
+
* keine formale Garantie; die Payloads bleiben bewusst klein, damit der Hook
|
|
177
|
+
* bei parallelen Tool-Calls nicht interleavt.
|
|
178
|
+
*
|
|
179
|
+
* @param {string} rawDir
|
|
180
|
+
* @param {{timestamp:string, tool:string, file:string, added:string[], removed:string[]}} event
|
|
181
|
+
*/
|
|
182
|
+
function appendEvent(rawDir, event) {
|
|
183
|
+
ensureDir(rawDir);
|
|
184
|
+
const when = event && event.timestamp ? new Date(event.timestamp) : new Date();
|
|
185
|
+
const safeDay = Number.isNaN(when.getTime()) ? dayStamp(new Date()) : dayStamp(when);
|
|
186
|
+
const file = path.join(rawDir, `tag-events-${safeDay}.jsonl`);
|
|
187
|
+
fs.appendFileSync(file, JSON.stringify(event) + '\n', 'utf8');
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Append an error record to `.cap/memory/raw/errors.log`.
|
|
192
|
+
*
|
|
193
|
+
* @cap-todo(ac:F-054/AC-6) Hook-Fehler landen hier und blockieren den Edit-Tool
|
|
194
|
+
* nie — der Aufrufer (Hook-Entry) ruft logError im catch und exit'ed mit 0.
|
|
195
|
+
*
|
|
196
|
+
* @param {string} rawDir
|
|
197
|
+
* @param {Error|{message:string, stack?:string}} err
|
|
198
|
+
*/
|
|
199
|
+
function logError(rawDir, err) {
|
|
200
|
+
try {
|
|
201
|
+
ensureDir(rawDir);
|
|
202
|
+
const entry = {
|
|
203
|
+
timestamp: new Date().toISOString(),
|
|
204
|
+
message: (err && err.message) || String(err),
|
|
205
|
+
stack: (err && err.stack) || null,
|
|
206
|
+
};
|
|
207
|
+
fs.appendFileSync(path.join(rawDir, 'errors.log'), JSON.stringify(entry) + '\n', 'utf8');
|
|
208
|
+
} catch {
|
|
209
|
+
// Best-effort — if even logging fails, we must stay silent (AC-6).
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// @cap-decision Hard cap for readFile — any file > MAX_OBSERVE_BYTES is skipped
|
|
214
|
+
// with a breadcrumb. Rationale: the hook runs synchronously on every Edit/Write
|
|
215
|
+
// and must stay <100 ms (AC-5). A pathological 200 MB generated fixture would
|
|
216
|
+
// not only blow the budget but also push the entire file into memory for a
|
|
217
|
+
// regex scan. 5 MiB comfortably holds any hand-authored source while bounding
|
|
218
|
+
// worst-case latency.
|
|
219
|
+
const MAX_OBSERVE_BYTES = 5 * 1024 * 1024;
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Read the observed file's bytes within the size cap. Returns null (and logs)
|
|
223
|
+
* on I/O failure or size-overflow; never throws.
|
|
224
|
+
*
|
|
225
|
+
* @param {string} filePath
|
|
226
|
+
* @param {string} rawDir
|
|
227
|
+
* @param {(p:string)=>string} readFile
|
|
228
|
+
* @returns {{content:string, mtime:(number|null)}|null}
|
|
229
|
+
*/
|
|
230
|
+
function readObservedFile(filePath, rawDir, readFile) {
|
|
231
|
+
let size = null;
|
|
232
|
+
let mtime = null;
|
|
233
|
+
try {
|
|
234
|
+
const st = fs.statSync(filePath);
|
|
235
|
+
size = st.size;
|
|
236
|
+
mtime = st.mtimeMs;
|
|
237
|
+
} catch {
|
|
238
|
+
// statSync failure is non-fatal — readFile may still succeed (e.g. injected
|
|
239
|
+
// reader in tests). Proceed without the size-guard short-circuit.
|
|
240
|
+
}
|
|
241
|
+
if (size !== null && size > MAX_OBSERVE_BYTES) {
|
|
242
|
+
logError(rawDir, new Error(`skipping oversized file (${size} bytes > ${MAX_OBSERVE_BYTES}): ${filePath}`));
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
try {
|
|
246
|
+
return { content: readFile(filePath), mtime };
|
|
247
|
+
} catch (err) {
|
|
248
|
+
logError(rawDir, err);
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Compute the tag-diff between the current file content and the last snapshot.
|
|
255
|
+
* Pure function modulo the snapshot-read side-effect inside loadSnapshot.
|
|
256
|
+
*
|
|
257
|
+
* @param {string} content
|
|
258
|
+
* @param {string} rawDir
|
|
259
|
+
* @param {string} filePath
|
|
260
|
+
* @returns {{currentTags:string[], snapshot:object|null, added:string[], removed:string[]}}
|
|
261
|
+
*/
|
|
262
|
+
function computeDelta(content, rawDir, filePath) {
|
|
263
|
+
const currentTags = extractTags(content);
|
|
264
|
+
const snapshot = loadSnapshot(rawDir, filePath);
|
|
265
|
+
const previousTags = snapshot ? snapshot.tags : [];
|
|
266
|
+
const { added, removed } = diffTags(previousTags, currentTags);
|
|
267
|
+
return { currentTags, snapshot, added, removed };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Persist the JSONL event and update the snapshot. Event-write failure is fatal
|
|
272
|
+
* for this observation (returns eventWritten=false); snapshot-write failure is
|
|
273
|
+
* non-fatal (next run re-diffs against the stale snapshot).
|
|
274
|
+
*/
|
|
275
|
+
function persistObservation(rawDir, filePath, tool, now, currentTags, mtime, added, removed) {
|
|
276
|
+
try {
|
|
277
|
+
appendEvent(rawDir, {
|
|
278
|
+
timestamp: now.toISOString(),
|
|
279
|
+
tool,
|
|
280
|
+
file: path.resolve(filePath),
|
|
281
|
+
added,
|
|
282
|
+
removed,
|
|
283
|
+
});
|
|
284
|
+
} catch (err) {
|
|
285
|
+
logError(rawDir, err);
|
|
286
|
+
return { eventWritten: false };
|
|
287
|
+
}
|
|
288
|
+
try {
|
|
289
|
+
writeSnapshot(rawDir, filePath, {
|
|
290
|
+
file: path.resolve(filePath),
|
|
291
|
+
tags: currentTags,
|
|
292
|
+
mtime,
|
|
293
|
+
updatedAt: now.toISOString(),
|
|
294
|
+
});
|
|
295
|
+
} catch (err) {
|
|
296
|
+
logError(rawDir, err);
|
|
297
|
+
}
|
|
298
|
+
return { eventWritten: true };
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Main entry: observe a file after a tool invocation, compute tag diff against
|
|
303
|
+
* the last snapshot, persist an event on change.
|
|
304
|
+
*
|
|
305
|
+
* @cap-todo(ac:F-054/AC-2) observe liest die aktuelle Datei, lädt den Snapshot,
|
|
306
|
+
* ruft diffTags auf und persistiert sowohl Event als auch aktualisierten
|
|
307
|
+
* Snapshot.
|
|
308
|
+
* @cap-todo(ac:F-054/AC-4) Kein Diff (added.length === 0 && removed.length === 0)
|
|
309
|
+
* → kein Write. Keine leere JSONL-Zeile, kein Noise.
|
|
310
|
+
* @cap-todo(ac:F-054/AC-5) observe ist synchron und vermeidet zweite Reads/
|
|
311
|
+
* Regex-Passes: ein einziger split+Regex-Scan, dann Set-Diff. Performance-Test
|
|
312
|
+
* in cap-tag-observer.test.cjs erzwingt <100 ms für 10 000-Zeilen-Files.
|
|
313
|
+
*
|
|
314
|
+
* @param {Object} opts
|
|
315
|
+
* @param {string} opts.filePath - Absolute or cwd-relative path to the file that was edited.
|
|
316
|
+
* @param {string} opts.tool - Tool name (Edit/Write/MultiEdit/NotebookEdit).
|
|
317
|
+
* @param {string} [opts.rawDir] - Override raw memory directory (defaults to `<cwd>/.cap/memory/raw`).
|
|
318
|
+
* @param {Date} [opts.now] - Injected clock for testing.
|
|
319
|
+
* @param {(p:string)=>string} [opts.readFile] - Injected reader for testing.
|
|
320
|
+
* @returns {{eventWritten:boolean, added:string[], removed:string[]}}
|
|
321
|
+
*/
|
|
322
|
+
function observe(opts) {
|
|
323
|
+
const filePath = opts && opts.filePath;
|
|
324
|
+
const tool = (opts && opts.tool) || 'unknown';
|
|
325
|
+
const now = (opts && opts.now) || new Date();
|
|
326
|
+
const readFile = (opts && opts.readFile) || ((p) => fs.readFileSync(p, 'utf8'));
|
|
327
|
+
const rawDir = (opts && opts.rawDir) || path.join(process.cwd(), '.cap', 'memory', 'raw');
|
|
328
|
+
|
|
329
|
+
if (!filePath) return { eventWritten: false, added: [], removed: [] };
|
|
330
|
+
|
|
331
|
+
const read = readObservedFile(filePath, rawDir, readFile);
|
|
332
|
+
if (!read) return { eventWritten: false, added: [], removed: [] };
|
|
333
|
+
|
|
334
|
+
const { currentTags, snapshot, added, removed } = computeDelta(read.content, rawDir, filePath);
|
|
335
|
+
|
|
336
|
+
if (added.length === 0 && removed.length === 0) {
|
|
337
|
+
// AC-4: nothing to report. Still seed a snapshot on first-ever observation
|
|
338
|
+
// of a tagless file so we don't re-diff the same empty set forever.
|
|
339
|
+
if (!snapshot) {
|
|
340
|
+
try {
|
|
341
|
+
writeSnapshot(rawDir, filePath, {
|
|
342
|
+
file: path.resolve(filePath),
|
|
343
|
+
tags: currentTags,
|
|
344
|
+
mtime: read.mtime,
|
|
345
|
+
updatedAt: now.toISOString(),
|
|
346
|
+
});
|
|
347
|
+
} catch (err) {
|
|
348
|
+
logError(rawDir, err);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
return { eventWritten: false, added, removed };
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const { eventWritten } = persistObservation(
|
|
355
|
+
rawDir, filePath, tool, now, currentTags, read.mtime, added, removed,
|
|
356
|
+
);
|
|
357
|
+
return { eventWritten, added, removed };
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
module.exports = {
|
|
361
|
+
extractTags,
|
|
362
|
+
diffTags,
|
|
363
|
+
snapshotPath,
|
|
364
|
+
loadSnapshot,
|
|
365
|
+
writeSnapshot,
|
|
366
|
+
appendEvent,
|
|
367
|
+
logError,
|
|
368
|
+
observe,
|
|
369
|
+
// exposed for tests
|
|
370
|
+
_dayStamp: dayStamp,
|
|
371
|
+
};
|