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,627 @@
|
|
|
1
|
+
// @cap-context CAP F-070 Collect Learning Signals — observability foundation for the V5 Self-Learning pivot.
|
|
2
|
+
// Three collectors emit different signal types into .cap/learning/signals/<type>.jsonl, plus a
|
|
3
|
+
// getSignals(type, range) query API that F-071 (pattern extraction) and F-072 (fitness score)
|
|
4
|
+
// will consume. Mirrors F-061's privacy boundary: no raw text, hash-only context.
|
|
5
|
+
// @cap-decision(F-070/D1) JSONL append-only format (same as F-061 telemetry). One record per line, O(1)
|
|
6
|
+
// append, O(n) streaming read. Reading is reserved for the cold path (getSignals), never the
|
|
7
|
+
// hot path (recordX). Steals the writeJsonlLine pattern from cap-telemetry.cjs#writeJsonlLine.
|
|
8
|
+
// @cap-decision(F-070/D2) Hot-path collectors (recordOverride / recordMemoryRef) are SYNCHRONOUS and never
|
|
9
|
+
// read **signal** JSONLs. AC-5 caps hook overhead at <50ms; the only way to keep that bound
|
|
10
|
+
// under a growing signal volume is to never read overrides.jsonl / memory-refs.jsonl during a
|
|
11
|
+
// hook. Regret detection is the deliberate exception (AC-3) and runs from /cap:scan.
|
|
12
|
+
// The state ledger (written-files.jsonl) is a separate concern from signals: hooks DO read
|
|
13
|
+
// it, but the file stays per-session-small (<100 entries typical) so the read is bounded.
|
|
14
|
+
// @cap-decision(F-070/D11) `subType` (not `kind`) discriminates override flavours. F-061 uses `kind` for a
|
|
15
|
+
// single discriminator; F-070 needs TWO nested discriminators (`signalType` for the broad
|
|
16
|
+
// type — override / memory-ref / regret — and `subType` for override-internal flavours
|
|
17
|
+
// — editAfterWrite / rejectApproval). `kind=override` would be redundant to signalType.
|
|
18
|
+
// F-071/F-072 readers will see both discriminators and route accordingly.
|
|
19
|
+
// @cap-decision(F-070/D3) Record schema is fixed: { id, ts, sessionId, featureId, signalType, subType?,
|
|
20
|
+
// contextHash, ...typeSpecific }. AC-4 forbids raw text on disk — every free-text field must
|
|
21
|
+
// be hashed via cap-telemetry.cjs#hashContext (re-used, not duplicated). New keys added in
|
|
22
|
+
// the future must be structured metadata only.
|
|
23
|
+
// @cap-decision(F-070/D4) Trigger split: hooks fire recordOverride / recordMemoryRef from PostToolUse,
|
|
24
|
+
// recordRegret runs from the tag-scanner (cold path) via recordRegretsFromScan. A regret hook
|
|
25
|
+
// on every Stop would scan all source files and blow AC-5's 50ms budget on any non-trivial
|
|
26
|
+
// codebase. Retrospective tagging is a scan-time concern, not a per-tool-call concern.
|
|
27
|
+
// @cap-constraint Zero external dependencies: node:fs, node:path, node:crypto only — and we re-use
|
|
28
|
+
// cap-telemetry.cjs#hashContext for the SHA256 path so the privacy gate has a single source.
|
|
29
|
+
// @cap-risk(F-070/AC-4) PRIVACY BOUNDARY — this module must never accept, log, or persist raw user-typed
|
|
30
|
+
// prompts, edit diffs, or file contents. Free-text inputs (e.g. file paths, decision text)
|
|
31
|
+
// must pass through hashContext before they reach disk. Any future contributor adding a
|
|
32
|
+
// `diff`, `prompt`, `body`, or `text` field violates AC-4.
|
|
33
|
+
// @cap-risk(F-070/AC-5) HOT-PATH OVERHEAD — recordOverride and recordMemoryRef MUST NOT read JSONL files,
|
|
34
|
+
// spawn processes, or do any work that scales with prior signal volume. The performance
|
|
35
|
+
// budget is <50ms per hook invocation; tests bracket this with performance.now().
|
|
36
|
+
|
|
37
|
+
'use strict';
|
|
38
|
+
|
|
39
|
+
// @cap-feature(feature:F-070, primary:true) Collect Learning Signals — three collectors + getSignals query API.
|
|
40
|
+
|
|
41
|
+
const fs = require('node:fs');
|
|
42
|
+
const path = require('node:path');
|
|
43
|
+
const crypto = require('node:crypto');
|
|
44
|
+
|
|
45
|
+
// Re-use the hashContext primitive from F-061. Single source of truth for the SHA256 privacy gate.
|
|
46
|
+
// @cap-risk(F-070/AC-4) Direct require avoids duplicating the sha256[:16] code path. If F-061's helper
|
|
47
|
+
// changes shape (e.g. digest length), this module follows automatically — there is
|
|
48
|
+
// only one privacy primitive, and it lives in cap-telemetry.cjs.
|
|
49
|
+
const telemetry = require('./cap-telemetry.cjs');
|
|
50
|
+
|
|
51
|
+
// -----------------------------------------------------------------------------
|
|
52
|
+
// Constants
|
|
53
|
+
// -----------------------------------------------------------------------------
|
|
54
|
+
|
|
55
|
+
const CAP_DIR = '.cap';
|
|
56
|
+
const LEARNING_DIR = 'learning';
|
|
57
|
+
const SIGNALS_DIR = 'signals';
|
|
58
|
+
const STATE_DIR = 'state';
|
|
59
|
+
|
|
60
|
+
// File names per signal type. Kept as constants so tests and consumers (F-071) reference one place.
|
|
61
|
+
const OVERRIDES_FILE = 'overrides.jsonl';
|
|
62
|
+
const MEMORY_REFS_FILE = 'memory-refs.jsonl';
|
|
63
|
+
const REGRETS_FILE = 'regrets.jsonl';
|
|
64
|
+
|
|
65
|
+
// Per-session ledger: which files the agent wrote in this session. Read by the editAfterWrite
|
|
66
|
+
// hook to determine whether an Edit follows a Write of the same file. Hooks fire as fresh
|
|
67
|
+
// subprocesses, so an in-memory Set cannot persist across events — the ledger is the bridge.
|
|
68
|
+
const WRITTEN_FILES_LEDGER = 'written-files.jsonl';
|
|
69
|
+
|
|
70
|
+
// Cap on the persisted file path so a hostile or accidental long path doesn't bloat the ledger.
|
|
71
|
+
// Real paths in this codebase are <500 chars; 1024 is a generous cap.
|
|
72
|
+
const PATH_MAX = 1024;
|
|
73
|
+
|
|
74
|
+
// Length cap for any string field that lands on disk. Matches cap-telemetry.cjs#ID_MAX so a hostile
|
|
75
|
+
// caller can't use sessionId / featureId as a smuggle channel even if the privacy gate above slips.
|
|
76
|
+
const ID_MAX = 200;
|
|
77
|
+
|
|
78
|
+
// Allowed signal types for the public getSignals API. Order matches FEATURE-MAP.md AC-6 phrasing.
|
|
79
|
+
// @cap-decision(F-070/D5) Public type names are 'override' | 'memory-ref' | 'regret' (singular, hyphenated)
|
|
80
|
+
// to match the AC-6 contract. Internally the file names use plural ('overrides.jsonl' etc.)
|
|
81
|
+
// to mirror cap-telemetry.cjs's file-naming convention; the mapping is centralised in
|
|
82
|
+
// typeToFile() so consumers never see the difference.
|
|
83
|
+
const VALID_TYPES = new Set(['override', 'memory-ref', 'regret']);
|
|
84
|
+
|
|
85
|
+
// Allowed override subTypes. AC-1 distinguishes Edit-after-Write from explicit Reject-Approval events.
|
|
86
|
+
const VALID_OVERRIDE_SUBTYPES = new Set(['editAfterWrite', 'rejectApproval']);
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* @typedef {Object} OverrideRecord
|
|
90
|
+
* @property {string} id - Unique record id (timestamp + random).
|
|
91
|
+
* @property {string} ts - ISO timestamp.
|
|
92
|
+
* @property {string|null} sessionId
|
|
93
|
+
* @property {string|null} featureId
|
|
94
|
+
* @property {'override'} signalType
|
|
95
|
+
* @property {'editAfterWrite'|'rejectApproval'} subType
|
|
96
|
+
* @property {string} contextHash - 16-char sha256 hex of the structured context (path or decision id).
|
|
97
|
+
* @property {string} [targetFileHash] - 16-char sha256 of the targeted file path (path-string-only, never the contents).
|
|
98
|
+
*/
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* @typedef {Object} MemoryRefRecord
|
|
102
|
+
* @property {string} id
|
|
103
|
+
* @property {string} ts
|
|
104
|
+
* @property {string|null} sessionId
|
|
105
|
+
* @property {string|null} featureId
|
|
106
|
+
* @property {'memory-ref'} signalType
|
|
107
|
+
* @property {string} contextHash - 16-char sha256 of the memory-file path (path-string-only — AC-2 forbids reading the file).
|
|
108
|
+
* @property {string} [memoryFileHash] - 16-char sha256 of the memory-file path (alias of contextHash for query convenience).
|
|
109
|
+
*/
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* @typedef {Object} RegretRecord
|
|
113
|
+
* @property {string} id
|
|
114
|
+
* @property {string} ts
|
|
115
|
+
* @property {string|null} sessionId
|
|
116
|
+
* @property {string|null} featureId
|
|
117
|
+
* @property {'regret'} signalType
|
|
118
|
+
* @property {string} decisionId - Stable identifier for the @cap-decision tag (file:line is the default; consumers may pass the decision-id from metadata).
|
|
119
|
+
* @property {string} contextHash - 16-char sha256 of decisionId. Used for dedup keys in F-071.
|
|
120
|
+
*/
|
|
121
|
+
|
|
122
|
+
// -----------------------------------------------------------------------------
|
|
123
|
+
// Internal helpers (lazy-create dir, atomic-ish append, id generation)
|
|
124
|
+
// -----------------------------------------------------------------------------
|
|
125
|
+
|
|
126
|
+
// @cap-todo(ac:F-070/AC-7) Lazy-create on first append: ensure .cap/learning/signals/ exists before writing
|
|
127
|
+
// the first JSONL line. Idempotent; mkdir { recursive: true } is safe to call repeatedly.
|
|
128
|
+
function ensureDir(dir) {
|
|
129
|
+
try {
|
|
130
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
131
|
+
} catch (_e) {
|
|
132
|
+
// Swallow — AC-7 demands no exception escapes a collector. The append below will surface
|
|
133
|
+
// any persistent IO problem via its own try/catch and silently no-op.
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// @cap-decision(F-070/D6) JSONL append uses O_APPEND like cap-telemetry.cjs. Atomic for short single-line
|
|
138
|
+
// writes on Linux/macOS (PIPE_BUF >= 4 KiB). The record + newline always fits in one write.
|
|
139
|
+
// Stealing the proven pattern from F-061 keeps both modules' on-disk format consistent so
|
|
140
|
+
// F-071 / F-072 can share a single JSONL reader if they want to.
|
|
141
|
+
/**
|
|
142
|
+
* Append one JSON record as a single line to the given file. Lazy-creates the parent directory.
|
|
143
|
+
* Never throws — AC-7 requires that a collector failure is silent.
|
|
144
|
+
* @param {string} filePath
|
|
145
|
+
* @param {object} record
|
|
146
|
+
*/
|
|
147
|
+
function appendJsonlLine(filePath, record) {
|
|
148
|
+
try {
|
|
149
|
+
ensureDir(path.dirname(filePath));
|
|
150
|
+
const line = JSON.stringify(record) + '\n';
|
|
151
|
+
const fd = fs.openSync(filePath, 'a');
|
|
152
|
+
try {
|
|
153
|
+
fs.writeSync(fd, line);
|
|
154
|
+
} finally {
|
|
155
|
+
fs.closeSync(fd);
|
|
156
|
+
}
|
|
157
|
+
} catch (_e) {
|
|
158
|
+
// @cap-risk(F-070/AC-7) Swallow IO errors so a transient EACCES / ENOSPC doesn't crash a hook.
|
|
159
|
+
// The signal is lost, but the user's command continues. F-074 (Pattern Unlearn)
|
|
160
|
+
// will surface signal-loss diagnostics later — not our concern here.
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Generate a short unique record id. Same shape as cap-telemetry.cjs#generateCallId so consumers
|
|
166
|
+
* can use one regex if they ever cross-reference IDs.
|
|
167
|
+
*/
|
|
168
|
+
function generateSignalId() {
|
|
169
|
+
const ts = Date.now().toString(36);
|
|
170
|
+
const rnd = crypto.randomBytes(4).toString('hex');
|
|
171
|
+
return `${ts}-${rnd}`;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Length-cap a string id (sessionId, featureId, decisionId) and reject non-strings.
|
|
176
|
+
* Mirrors cap-telemetry.cjs's capId helper.
|
|
177
|
+
* @param {any} v
|
|
178
|
+
* @returns {string|null}
|
|
179
|
+
*/
|
|
180
|
+
function capId(v) {
|
|
181
|
+
if (typeof v !== 'string' || v.length === 0) return null;
|
|
182
|
+
return v.slice(0, ID_MAX);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Map a public signal type to its on-disk file name. Centralised so AC-1/AC-2/AC-3 file paths
|
|
187
|
+
* are defined in exactly one place.
|
|
188
|
+
* @param {string} type - 'override' | 'memory-ref' | 'regret'
|
|
189
|
+
* @returns {string|null}
|
|
190
|
+
*/
|
|
191
|
+
function typeToFile(type) {
|
|
192
|
+
if (type === 'override') return OVERRIDES_FILE;
|
|
193
|
+
if (type === 'memory-ref') return MEMORY_REFS_FILE;
|
|
194
|
+
if (type === 'regret') return REGRETS_FILE;
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Resolve the absolute path for a given signal type's JSONL file.
|
|
200
|
+
* @param {string} projectRoot
|
|
201
|
+
* @param {string} type
|
|
202
|
+
* @returns {string|null}
|
|
203
|
+
*/
|
|
204
|
+
function signalsFilePath(projectRoot, type) {
|
|
205
|
+
const file = typeToFile(type);
|
|
206
|
+
if (!file) return null;
|
|
207
|
+
return path.join(projectRoot, CAP_DIR, LEARNING_DIR, SIGNALS_DIR, file);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Resolve the absolute path for the per-session written-files ledger.
|
|
212
|
+
* @param {string} projectRoot
|
|
213
|
+
* @returns {string}
|
|
214
|
+
*/
|
|
215
|
+
function writtenFilesLedgerPath(projectRoot) {
|
|
216
|
+
return path.join(projectRoot, CAP_DIR, LEARNING_DIR, STATE_DIR, WRITTEN_FILES_LEDGER);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// -----------------------------------------------------------------------------
|
|
220
|
+
// State ledger — bridges Write→Edit events across subprocess hook invocations.
|
|
221
|
+
// -----------------------------------------------------------------------------
|
|
222
|
+
|
|
223
|
+
// @cap-todo(ac:F-070/AC-1) Persistent ledger replaces the broken in-memory Set in
|
|
224
|
+
// hooks/cap-learning-hook.js. Subprocess hooks cannot share a Set;
|
|
225
|
+
// they share the file system. Append on Write, check on Edit.
|
|
226
|
+
/**
|
|
227
|
+
* Record that the agent wrote `targetFile` in `sessionId`. Called from PostToolUse on
|
|
228
|
+
* Write / MultiEdit / NotebookEdit. Lazy-creates `.cap/learning/state/written-files.jsonl`.
|
|
229
|
+
* Never throws — AC-7 contract.
|
|
230
|
+
* @param {string} projectRoot
|
|
231
|
+
* @param {string} sessionId
|
|
232
|
+
* @param {string} targetFile
|
|
233
|
+
* @returns {{sessionId:string, targetFile:string, ts:string}|null}
|
|
234
|
+
*/
|
|
235
|
+
function recordWriteIntoLedger(projectRoot, sessionId, targetFile) {
|
|
236
|
+
try {
|
|
237
|
+
if (!projectRoot || typeof projectRoot !== 'string') return null;
|
|
238
|
+
const sid = capId(sessionId);
|
|
239
|
+
if (!sid) return null;
|
|
240
|
+
if (typeof targetFile !== 'string' || targetFile.length === 0) return null;
|
|
241
|
+
const record = {
|
|
242
|
+
sessionId: sid,
|
|
243
|
+
targetFile: targetFile.slice(0, PATH_MAX),
|
|
244
|
+
ts: new Date().toISOString(),
|
|
245
|
+
};
|
|
246
|
+
appendJsonlLine(writtenFilesLedgerPath(projectRoot), record);
|
|
247
|
+
return record;
|
|
248
|
+
} catch (_e) {
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Check whether `targetFile` was previously recorded as written by `sessionId`.
|
|
255
|
+
* Reads the ledger filtered by sessionId. Returns false on any error or missing file.
|
|
256
|
+
*
|
|
257
|
+
* Hot-path read, but bounded: ledger entries are scoped per session and the file is
|
|
258
|
+
* cold-started fresh each project. AC-5's <50ms budget is preserved because (a) we only
|
|
259
|
+
* read this single small file, never the signal JSONLs, and (b) typical sessions have
|
|
260
|
+
* <100 writes. Adversarial test bracket-confirms the bound.
|
|
261
|
+
*
|
|
262
|
+
* @param {string} projectRoot
|
|
263
|
+
* @param {string} sessionId
|
|
264
|
+
* @param {string} targetFile
|
|
265
|
+
* @returns {boolean}
|
|
266
|
+
*/
|
|
267
|
+
function wasWrittenInSession(projectRoot, sessionId, targetFile) {
|
|
268
|
+
try {
|
|
269
|
+
if (!projectRoot || typeof projectRoot !== 'string') return false;
|
|
270
|
+
const sid = capId(sessionId);
|
|
271
|
+
if (!sid) return false;
|
|
272
|
+
if (typeof targetFile !== 'string' || targetFile.length === 0) return false;
|
|
273
|
+
const fp = writtenFilesLedgerPath(projectRoot);
|
|
274
|
+
if (!fs.existsSync(fp)) return false;
|
|
275
|
+
const raw = fs.readFileSync(fp, 'utf8');
|
|
276
|
+
const target = targetFile.slice(0, PATH_MAX);
|
|
277
|
+
for (const line of raw.split('\n')) {
|
|
278
|
+
if (!line) continue;
|
|
279
|
+
try {
|
|
280
|
+
const r = JSON.parse(line);
|
|
281
|
+
if (r && r.sessionId === sid && r.targetFile === target) return true;
|
|
282
|
+
} catch (_e) {
|
|
283
|
+
// Malformed line — skip, keep scanning. AC-7 forbids throwing.
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return false;
|
|
287
|
+
} catch (_e) {
|
|
288
|
+
return false;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// -----------------------------------------------------------------------------
|
|
293
|
+
// Public API — collectors
|
|
294
|
+
// -----------------------------------------------------------------------------
|
|
295
|
+
|
|
296
|
+
// @cap-todo(ac:F-070/AC-1) Override collector: persists Edit-after-Write and Reject-Approval events to
|
|
297
|
+
// .cap/learning/signals/overrides.jsonl. subType discriminates the two sources.
|
|
298
|
+
// @cap-todo(ac:F-070/AC-4) Record schema enforced here: { id, ts, sessionId, featureId, signalType,
|
|
299
|
+
// subType, contextHash, targetFileHash? } — never raw paths or text.
|
|
300
|
+
/**
|
|
301
|
+
* Record an override event. Two flavours: 'editAfterWrite' (the agent wrote a file, the user edited it
|
|
302
|
+
* during the same session) and 'rejectApproval' (the user explicitly rejected an approval prompt).
|
|
303
|
+
*
|
|
304
|
+
* Never throws — AC-7 contract.
|
|
305
|
+
*
|
|
306
|
+
* @param {Object} input
|
|
307
|
+
* @param {string} input.projectRoot
|
|
308
|
+
* @param {'editAfterWrite'|'rejectApproval'} input.subType
|
|
309
|
+
* @param {string|null} [input.sessionId]
|
|
310
|
+
* @param {string|null} [input.featureId]
|
|
311
|
+
* @param {string} [input.contextHash] - Optional pre-computed hash. If omitted and `targetFile` is given,
|
|
312
|
+
* the hash is derived from the target file path (path-string-only — never reads the file).
|
|
313
|
+
* @param {string} [input.targetFile] - Optional structured context (e.g. the edited file path). Hashed
|
|
314
|
+
* before persistence — the raw string never reaches disk.
|
|
315
|
+
* @param {string} [input.ts] - Override timestamp (mostly for tests).
|
|
316
|
+
* @returns {OverrideRecord|null} The persisted record, or null when the input is invalid (no throw).
|
|
317
|
+
*/
|
|
318
|
+
function recordOverride(input) {
|
|
319
|
+
try {
|
|
320
|
+
const safe = input || {};
|
|
321
|
+
if (!safe.projectRoot || typeof safe.projectRoot !== 'string') return null;
|
|
322
|
+
if (!VALID_OVERRIDE_SUBTYPES.has(safe.subType)) return null;
|
|
323
|
+
|
|
324
|
+
// @cap-risk(F-070/AC-4) Derive contextHash from the structured target file path, never from file
|
|
325
|
+
// contents. If the caller passes a pre-computed hash we accept it (consumer
|
|
326
|
+
// knows their dedup key), but we still cap its length defensively.
|
|
327
|
+
const fallbackContext = safe.targetFile
|
|
328
|
+
? telemetry.hashContext(safe.targetFile)
|
|
329
|
+
: telemetry.hashContext(`${safe.subType}:${safe.sessionId || ''}`);
|
|
330
|
+
const contextHash = (typeof safe.contextHash === 'string' && safe.contextHash.length > 0)
|
|
331
|
+
? safe.contextHash.slice(0, 64)
|
|
332
|
+
: fallbackContext;
|
|
333
|
+
|
|
334
|
+
/** @type {OverrideRecord} */
|
|
335
|
+
const record = {
|
|
336
|
+
id: generateSignalId(),
|
|
337
|
+
ts: safe.ts || new Date().toISOString(),
|
|
338
|
+
sessionId: capId(safe.sessionId),
|
|
339
|
+
featureId: capId(safe.featureId),
|
|
340
|
+
signalType: 'override',
|
|
341
|
+
subType: safe.subType,
|
|
342
|
+
contextHash,
|
|
343
|
+
};
|
|
344
|
+
if (safe.targetFile) {
|
|
345
|
+
// Hash-only — path string is privacy-sensitive (could include a username under /Users/<name>/...).
|
|
346
|
+
record.targetFileHash = telemetry.hashContext(safe.targetFile);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
appendJsonlLine(signalsFilePath(safe.projectRoot, 'override'), record);
|
|
350
|
+
return record;
|
|
351
|
+
} catch (_e) {
|
|
352
|
+
return null;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// @cap-todo(ac:F-070/AC-2) Memory-Reference collector: increments a per-session count whenever any file
|
|
357
|
+
// under .cap/memory/*.md is read. Writes one record per read to memory-refs.jsonl.
|
|
358
|
+
// The "count" is reconstructed by query (getSignals) — we don't aggregate at write.
|
|
359
|
+
/**
|
|
360
|
+
* Record a memory-reference event. Called when the agent (via PostToolUse hook on Read) touches any file
|
|
361
|
+
* under `.cap/memory/`. The file path is hashed; the file contents are NEVER read here.
|
|
362
|
+
*
|
|
363
|
+
* Never throws — AC-7 contract.
|
|
364
|
+
*
|
|
365
|
+
* @param {Object} input
|
|
366
|
+
* @param {string} input.projectRoot
|
|
367
|
+
* @param {string|null} [input.sessionId]
|
|
368
|
+
* @param {string|null} [input.featureId]
|
|
369
|
+
* @param {string} input.memoryFile - Path of the touched memory file (relative or absolute). Hashed before
|
|
370
|
+
* persistence — the raw path never lands on disk.
|
|
371
|
+
* @param {string} [input.ts]
|
|
372
|
+
* @returns {MemoryRefRecord|null}
|
|
373
|
+
*/
|
|
374
|
+
function recordMemoryRef(input) {
|
|
375
|
+
try {
|
|
376
|
+
const safe = input || {};
|
|
377
|
+
if (!safe.projectRoot || typeof safe.projectRoot !== 'string') return null;
|
|
378
|
+
if (typeof safe.memoryFile !== 'string' || safe.memoryFile.length === 0) return null;
|
|
379
|
+
|
|
380
|
+
// @cap-risk(F-070/AC-4) memoryFile is hashed, never persisted as a raw path. The privacy boundary is
|
|
381
|
+
// symmetric with recordOverride — same hash function, same 16-char digest.
|
|
382
|
+
const memoryFileHash = telemetry.hashContext(safe.memoryFile);
|
|
383
|
+
|
|
384
|
+
/** @type {MemoryRefRecord} */
|
|
385
|
+
const record = {
|
|
386
|
+
id: generateSignalId(),
|
|
387
|
+
ts: safe.ts || new Date().toISOString(),
|
|
388
|
+
sessionId: capId(safe.sessionId),
|
|
389
|
+
featureId: capId(safe.featureId),
|
|
390
|
+
signalType: 'memory-ref',
|
|
391
|
+
contextHash: memoryFileHash,
|
|
392
|
+
memoryFileHash,
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
appendJsonlLine(signalsFilePath(safe.projectRoot, 'memory-ref'), record);
|
|
396
|
+
return record;
|
|
397
|
+
} catch (_e) {
|
|
398
|
+
return null;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// @cap-todo(ac:F-070/AC-3) Decision-Regret collector: emits one record per @cap-decision tag carrying
|
|
403
|
+
// regret:true. Triggered from /cap:scan (the cold path) — see recordRegretsFromScan
|
|
404
|
+
// below for the integration point.
|
|
405
|
+
/**
|
|
406
|
+
* Record a single regret. Lower-level than recordRegretsFromScan — useful when a caller already has the
|
|
407
|
+
* decision id in hand (e.g. the F-073 review board's manual "mark regret" action).
|
|
408
|
+
*
|
|
409
|
+
* Never throws — AC-7 contract.
|
|
410
|
+
*
|
|
411
|
+
* @param {Object} input
|
|
412
|
+
* @param {string} input.projectRoot
|
|
413
|
+
* @param {string|null} [input.sessionId]
|
|
414
|
+
* @param {string|null} [input.featureId]
|
|
415
|
+
* @param {string} input.decisionId - Stable identifier for the @cap-decision (file:line by default).
|
|
416
|
+
* @param {string} [input.contextHash]
|
|
417
|
+
* @param {string} [input.ts]
|
|
418
|
+
* @returns {RegretRecord|null}
|
|
419
|
+
*/
|
|
420
|
+
function recordRegret(input) {
|
|
421
|
+
try {
|
|
422
|
+
const safe = input || {};
|
|
423
|
+
if (!safe.projectRoot || typeof safe.projectRoot !== 'string') return null;
|
|
424
|
+
if (typeof safe.decisionId !== 'string' || safe.decisionId.length === 0) return null;
|
|
425
|
+
|
|
426
|
+
const decisionId = safe.decisionId.slice(0, ID_MAX);
|
|
427
|
+
const contextHash = (typeof safe.contextHash === 'string' && safe.contextHash.length > 0)
|
|
428
|
+
? safe.contextHash.slice(0, 64)
|
|
429
|
+
: telemetry.hashContext(decisionId);
|
|
430
|
+
|
|
431
|
+
/** @type {RegretRecord} */
|
|
432
|
+
const record = {
|
|
433
|
+
id: generateSignalId(),
|
|
434
|
+
ts: safe.ts || new Date().toISOString(),
|
|
435
|
+
sessionId: capId(safe.sessionId),
|
|
436
|
+
featureId: capId(safe.featureId),
|
|
437
|
+
signalType: 'regret',
|
|
438
|
+
decisionId,
|
|
439
|
+
contextHash,
|
|
440
|
+
};
|
|
441
|
+
|
|
442
|
+
appendJsonlLine(signalsFilePath(safe.projectRoot, 'regret'), record);
|
|
443
|
+
return record;
|
|
444
|
+
} catch (_e) {
|
|
445
|
+
return null;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// -----------------------------------------------------------------------------
|
|
450
|
+
// Public API — query
|
|
451
|
+
// -----------------------------------------------------------------------------
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Read all records from a signal-type JSONL. Tolerant to missing file and malformed lines.
|
|
455
|
+
* Internal helper for getSignals.
|
|
456
|
+
* @param {string} projectRoot
|
|
457
|
+
* @param {string} type
|
|
458
|
+
* @returns {Array<object>}
|
|
459
|
+
*/
|
|
460
|
+
function readAllSignals(projectRoot, type) {
|
|
461
|
+
if (typeof projectRoot !== 'string' || projectRoot.length === 0) return [];
|
|
462
|
+
const filePath = signalsFilePath(projectRoot, type);
|
|
463
|
+
if (!filePath || !fs.existsSync(filePath)) return [];
|
|
464
|
+
let raw;
|
|
465
|
+
try {
|
|
466
|
+
raw = fs.readFileSync(filePath, 'utf8');
|
|
467
|
+
} catch (_e) {
|
|
468
|
+
return [];
|
|
469
|
+
}
|
|
470
|
+
const records = [];
|
|
471
|
+
for (const line of raw.split('\n')) {
|
|
472
|
+
if (!line) continue;
|
|
473
|
+
try {
|
|
474
|
+
records.push(JSON.parse(line));
|
|
475
|
+
} catch (_e) {
|
|
476
|
+
// Skip malformed lines — query must never crash a command (mirrors F-061 behaviour).
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
return records;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// @cap-todo(ac:F-070/AC-6) Query API consumed by F-071 (pattern extraction) and F-072 (fitness score).
|
|
483
|
+
// Contract intentionally minimal: type + range. No byFeature, no recentSignals —
|
|
484
|
+
// add them later only when F-071/F-072 actually need them.
|
|
485
|
+
/**
|
|
486
|
+
* Query persisted signals by type and range.
|
|
487
|
+
*
|
|
488
|
+
* @param {string} projectRoot - Absolute path to project root.
|
|
489
|
+
* @param {'override'|'memory-ref'|'regret'} type - Signal type.
|
|
490
|
+
* @param {{from?: string|Date, to?: string|Date, sessionId?: string}} [range] - Time range OR sessionId.
|
|
491
|
+
* Pass `{from, to}` for a time slice (ISO strings or Date objects, inclusive).
|
|
492
|
+
* Pass `{sessionId}` to filter by session. Both keys may be combined.
|
|
493
|
+
* When `range` is omitted, ALL records of the given type are returned.
|
|
494
|
+
* @returns {Array<object>} Matching records, or [] if the type is invalid or no file exists.
|
|
495
|
+
*/
|
|
496
|
+
function getSignals(projectRoot, type, range) {
|
|
497
|
+
if (!VALID_TYPES.has(type)) return [];
|
|
498
|
+
const all = readAllSignals(projectRoot, type);
|
|
499
|
+
if (!range) return all;
|
|
500
|
+
|
|
501
|
+
const fromTs = range.from ? new Date(range.from).getTime() : null;
|
|
502
|
+
const toTs = range.to ? new Date(range.to).getTime() : null;
|
|
503
|
+
const sessionId = typeof range.sessionId === 'string' && range.sessionId.length > 0
|
|
504
|
+
? range.sessionId
|
|
505
|
+
: null;
|
|
506
|
+
|
|
507
|
+
return all.filter((r) => {
|
|
508
|
+
if (sessionId && r.sessionId !== sessionId) return false;
|
|
509
|
+
if (fromTs !== null || toTs !== null) {
|
|
510
|
+
const recordTs = new Date(r.ts).getTime();
|
|
511
|
+
if (Number.isNaN(recordTs)) return false;
|
|
512
|
+
if (fromTs !== null && recordTs < fromTs) return false;
|
|
513
|
+
if (toTs !== null && recordTs > toTs) return false;
|
|
514
|
+
}
|
|
515
|
+
return true;
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// -----------------------------------------------------------------------------
|
|
520
|
+
// Tag-scanner integration — regret detection (cold path)
|
|
521
|
+
// -----------------------------------------------------------------------------
|
|
522
|
+
|
|
523
|
+
// @cap-todo(ac:F-070/AC-3) Walk @cap-decision tags carrying regret:true and emit a RegretRecord per tag.
|
|
524
|
+
// Called from /cap:scan after enrichFromTags. Reads existing regrets to dedup —
|
|
525
|
+
// this is fine because the scan path is cold (AC-5 governs hooks, not scan).
|
|
526
|
+
// @cap-decision(F-070/D7) Dedup key is decisionId. The tag scanner's CapTag carries (file, line, metadata)
|
|
527
|
+
// so we synthesise a stable id when the tag has no explicit `id:` metadata: `<file>:<line>`.
|
|
528
|
+
// If multiple regret tags share a decisionId across runs (e.g. the same line), only the first
|
|
529
|
+
// is recorded — F-074's audit trail will track lifecycle from there.
|
|
530
|
+
/**
|
|
531
|
+
* Scan a tag list for `@cap-decision` tags carrying `regret:true` and emit a RegretRecord for each one
|
|
532
|
+
* not already persisted. Idempotent across repeated /cap:scan invocations.
|
|
533
|
+
*
|
|
534
|
+
* Never throws — wraps individual tag failures so a single malformed tag doesn't break the batch.
|
|
535
|
+
*
|
|
536
|
+
* @param {string} projectRoot
|
|
537
|
+
* @param {Array<{type: string, file: string, line: number, metadata: object, description: string}>} tags
|
|
538
|
+
* Tags from cap-tag-scanner.cjs#scanDirectory.
|
|
539
|
+
* @param {Object} [options]
|
|
540
|
+
* @param {string|null} [options.sessionId] - Optional session id to attach to emitted records.
|
|
541
|
+
* @param {string|null} [options.featureId] - Optional active feature id (default falls back to tag.metadata.feature).
|
|
542
|
+
* @returns {{recorded: number, skipped: number}} Counts for /cap:scan reporting.
|
|
543
|
+
*/
|
|
544
|
+
function recordRegretsFromScan(projectRoot, tags, options) {
|
|
545
|
+
const opts = options || {};
|
|
546
|
+
let recorded = 0;
|
|
547
|
+
let skipped = 0;
|
|
548
|
+
|
|
549
|
+
if (!Array.isArray(tags) || tags.length === 0) {
|
|
550
|
+
return { recorded, skipped };
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// Read existing regrets once to build the dedup set. Cold-path read — fine per D2.
|
|
554
|
+
const existing = readAllSignals(projectRoot, 'regret');
|
|
555
|
+
const seenDecisionIds = new Set(existing.map((r) => r.decisionId).filter(Boolean));
|
|
556
|
+
|
|
557
|
+
for (const tag of tags) {
|
|
558
|
+
try {
|
|
559
|
+
if (!tag || tag.type !== 'decision') continue;
|
|
560
|
+
// Match the regret marker. Tag metadata is parsed by cap-tag-scanner.cjs#parseMetadata which
|
|
561
|
+
// stores `regret:true` as the string 'true' (boolean-flag convention). We accept both string
|
|
562
|
+
// and boolean defensively — future scanner refactors mustn't silently break this integration.
|
|
563
|
+
const md = tag.metadata || {};
|
|
564
|
+
const isRegret = md.regret === 'true' || md.regret === true;
|
|
565
|
+
if (!isRegret) continue;
|
|
566
|
+
|
|
567
|
+
// Derive a stable decisionId. Prefer explicit metadata.id, else metadata.decision (e.g. "F-070/D1"),
|
|
568
|
+
// else fall back to file:line as a per-tag-position anchor.
|
|
569
|
+
const explicitId = md.id || md.decision;
|
|
570
|
+
const decisionId = (typeof explicitId === 'string' && explicitId.length > 0)
|
|
571
|
+
? explicitId
|
|
572
|
+
: `${tag.file}:${tag.line}`;
|
|
573
|
+
|
|
574
|
+
if (seenDecisionIds.has(decisionId)) {
|
|
575
|
+
skipped += 1;
|
|
576
|
+
continue;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
const featureId = opts.featureId != null ? opts.featureId : (md.feature || null);
|
|
580
|
+
|
|
581
|
+
const result = recordRegret({
|
|
582
|
+
projectRoot,
|
|
583
|
+
sessionId: opts.sessionId || null,
|
|
584
|
+
featureId,
|
|
585
|
+
decisionId,
|
|
586
|
+
});
|
|
587
|
+
if (result) {
|
|
588
|
+
recorded += 1;
|
|
589
|
+
seenDecisionIds.add(decisionId);
|
|
590
|
+
}
|
|
591
|
+
} catch (_e) {
|
|
592
|
+
// Per-tag failure must not break the batch.
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
return { recorded, skipped };
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// -----------------------------------------------------------------------------
|
|
600
|
+
// Exports
|
|
601
|
+
// -----------------------------------------------------------------------------
|
|
602
|
+
|
|
603
|
+
module.exports = {
|
|
604
|
+
// constants — exported for tests and consumers (F-071/F-072)
|
|
605
|
+
CAP_DIR,
|
|
606
|
+
LEARNING_DIR,
|
|
607
|
+
SIGNALS_DIR,
|
|
608
|
+
STATE_DIR,
|
|
609
|
+
OVERRIDES_FILE,
|
|
610
|
+
MEMORY_REFS_FILE,
|
|
611
|
+
REGRETS_FILE,
|
|
612
|
+
WRITTEN_FILES_LEDGER,
|
|
613
|
+
VALID_TYPES,
|
|
614
|
+
VALID_OVERRIDE_SUBTYPES,
|
|
615
|
+
// public API — collectors
|
|
616
|
+
recordOverride,
|
|
617
|
+
recordMemoryRef,
|
|
618
|
+
recordRegret,
|
|
619
|
+
// public API — query
|
|
620
|
+
getSignals,
|
|
621
|
+
// tag-scanner integration
|
|
622
|
+
recordRegretsFromScan,
|
|
623
|
+
// hook integration — persistent state ledger for editAfterWrite detection
|
|
624
|
+
recordWriteIntoLedger,
|
|
625
|
+
wasWrittenInSession,
|
|
626
|
+
writtenFilesLedgerPath,
|
|
627
|
+
};
|