@unerr-ai/unerr 0.2.1 → 0.2.3
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/README.md +36 -45
- package/dist/cli.js +37443 -36022
- package/package.json +2 -1
- package/dist/behaviors/agent-llm-bridge.js +0 -166
- package/dist/behaviors/architecture-guard.js +0 -256
- package/dist/behaviors/auto-doc.js +0 -247
- package/dist/behaviors/cascade-guard.js +0 -289
- package/dist/behaviors/change-narrative.js +0 -270
- package/dist/behaviors/convention-drift.js +0 -290
- package/dist/behaviors/framework.js +0 -235
- package/dist/behaviors/guard-formatter.js +0 -44
- package/dist/behaviors/incomplete-work.js +0 -270
- package/dist/behaviors/loop-breaker.js +0 -300
- package/dist/behaviors/session-continuity.js +0 -208
- package/dist/commands/branches.js +0 -97
- package/dist/commands/check-commit.js +0 -225
- package/dist/commands/compress-output.js +0 -64
- package/dist/commands/config-verify.js +0 -243
- package/dist/commands/daemon.js +0 -905
- package/dist/commands/dashboard.js +0 -52
- package/dist/commands/debug.js +0 -200
- package/dist/commands/enrich.js +0 -184
- package/dist/commands/exec.js +0 -233
- package/dist/commands/gain.js +0 -156
- package/dist/commands/hook.js +0 -88
- package/dist/commands/index.js +0 -88
- package/dist/commands/init.js +0 -74
- package/dist/commands/install.js +0 -505
- package/dist/commands/learn.js +0 -116
- package/dist/commands/manifest.js +0 -193
- package/dist/commands/rewind.js +0 -103
- package/dist/commands/serve.js +0 -19
- package/dist/commands/setup-wizard.js +0 -414
- package/dist/commands/skills.js +0 -64
- package/dist/commands/stats.js +0 -20
- package/dist/commands/status.js +0 -654
- package/dist/commands/timeline.js +0 -139
- package/dist/commands/uninstall.js +0 -230
- package/dist/components/App.js +0 -109
- package/dist/components/Banner.js +0 -12
- package/dist/components/ConfirmPrompt.js +0 -25
- package/dist/components/DriftSummary.js +0 -23
- package/dist/components/GradeBadge.js +0 -15
- package/dist/components/HealthCard.js +0 -18
- package/dist/components/InkSpinner.js +0 -22
- package/dist/components/InputBox.js +0 -17
- package/dist/components/KeyValue.js +0 -13
- package/dist/components/MessageList.js +0 -14
- package/dist/components/ProgressBar.js +0 -26
- package/dist/components/Section.js +0 -16
- package/dist/components/SessionSummaryCard.js +0 -73
- package/dist/components/StartupDisplay.js +0 -24
- package/dist/components/StatusDashboard.js +0 -57
- package/dist/components/StatusLine.js +0 -8
- package/dist/components/StepLine.js +0 -22
- package/dist/components/Theme.js +0 -20
- package/dist/components/ToolProgress.js +0 -8
- package/dist/components/ViolationList.js +0 -21
- package/dist/components/render.js +0 -13
- package/dist/config/agent-registry.js +0 -237
- package/dist/config/claude-settings-hooks.js +0 -304
- package/dist/config/hook-installer.js +0 -65
- package/dist/config/instruction-writer.js +0 -388
- package/dist/config/mcp-config-writer.js +0 -266
- package/dist/config/settings.js +0 -174
- package/dist/config/tool-detector.js +0 -42
- package/dist/config/value-surfacing.js +0 -119
- package/dist/core/context-assembly.js +0 -108
- package/dist/core/conversation.js +0 -33
- package/dist/core/local-chat-provider.js +0 -475
- package/dist/core/provider-factory.js +0 -55
- package/dist/core/providers.js +0 -90
- package/dist/core/query-engine.js +0 -174
- package/dist/daemon/api.js +0 -312
- package/dist/daemon/autostart.js +0 -119
- package/dist/daemon/bootstrap.js +0 -39
- package/dist/daemon/client.js +0 -164
- package/dist/daemon/detect-ci.js +0 -81
- package/dist/daemon/platform-linux.js +0 -146
- package/dist/daemon/platform-macos.js +0 -134
- package/dist/daemon/platform-windows.js +0 -116
- package/dist/daemon/process-manager.js +0 -299
- package/dist/daemon/protocol.js +0 -23
- package/dist/daemon/registry.js +0 -270
- package/dist/daemon/settings-schema.js +0 -72
- package/dist/daemon/system-health.js +0 -134
- package/dist/daemon/version-checker.js +0 -262
- package/dist/daemon/warm-start.js +0 -223
- package/dist/entrypoints/cli.js +0 -1043
- package/dist/entrypoints/daemon.js +0 -380
- package/dist/entrypoints/repl.js +0 -147
- package/dist/hooks/adapters/claude-code.js +0 -90
- package/dist/hooks/adapters/cline.js +0 -100
- package/dist/hooks/adapters/cursor.js +0 -98
- package/dist/hooks/hook-dedup.js +0 -79
- package/dist/hooks/hook-runner.js +0 -113
- package/dist/hooks/navigation-hooks.js +0 -175
- package/dist/hooks/prompt-hooks.js +0 -63
- package/dist/hooks/shell-hooks.js +0 -47
- package/dist/ignore.js +0 -111
- package/dist/intelligence/approach-suggester.js +0 -61
- package/dist/intelligence/ast-extractor.js +0 -2615
- package/dist/intelligence/ast-worker.js +0 -34
- package/dist/intelligence/background-indexer.js +0 -121
- package/dist/intelligence/blast-radius.js +0 -200
- package/dist/intelligence/community-detection.js +0 -691
- package/dist/intelligence/community-detector.js +0 -184
- package/dist/intelligence/computation-scheduler.js +0 -75
- package/dist/intelligence/confidence-propagation.js +0 -47
- package/dist/intelligence/convention-detector.js +0 -242
- package/dist/intelligence/convention-learner.js +0 -205
- package/dist/intelligence/convention-matcher.js +0 -205
- package/dist/intelligence/cozo-schema.js +0 -376
- package/dist/intelligence/decision-point-detector.js +0 -90
- package/dist/intelligence/deep-dive-tools.js +0 -586
- package/dist/intelligence/durability-scorer.js +0 -84
- package/dist/intelligence/exploration-cost.js +0 -204
- package/dist/intelligence/exploration-pattern-tracker.js +0 -61
- package/dist/intelligence/fact-generator.js +0 -322
- package/dist/intelligence/facts-schema.js +0 -90
- package/dist/intelligence/file-intelligence.js +0 -59
- package/dist/intelligence/graph-holder.js +0 -220
- package/dist/intelligence/graph-temporal-joiner.js +0 -238
- package/dist/intelligence/health-grade.js +0 -423
- package/dist/intelligence/health-grader.js +0 -200
- package/dist/intelligence/health-map-data.js +0 -259
- package/dist/intelligence/import-symbols.js +0 -136
- package/dist/intelligence/incremental-indexer.js +0 -658
- package/dist/intelligence/indexer/centrality.js +0 -62
- package/dist/intelligence/indexer/cfg-context.js +0 -95
- package/dist/intelligence/indexer/confidence.js +0 -34
- package/dist/intelligence/indexer/cross-file-resolver.js +0 -104
- package/dist/intelligence/indexer/edge-repair.js +0 -89
- package/dist/intelligence/indexer/entity-key.js +0 -17
- package/dist/intelligence/indexer/export-map.js +0 -132
- package/dist/intelligence/indexer/git-cochange.js +0 -128
- package/dist/intelligence/indexer/graph-patch.js +0 -147
- package/dist/intelligence/indexer/incremental.js +0 -78
- package/dist/intelligence/indexer/ingest.js +0 -160
- package/dist/intelligence/indexer/language-detect.js +0 -226
- package/dist/intelligence/indexer/metadata.js +0 -63
- package/dist/intelligence/indexer/mutation-tracker.js +0 -79
- package/dist/intelligence/indexer/orchestrator.js +0 -155
- package/dist/intelligence/indexer/plugin-interface.js +0 -31
- package/dist/intelligence/indexer/plugins/csharp.js +0 -440
- package/dist/intelligence/indexer/plugins/go.js +0 -335
- package/dist/intelligence/indexer/plugins/java.js +0 -370
- package/dist/intelligence/indexer/plugins/python.js +0 -358
- package/dist/intelligence/indexer/plugins/regex-fallback.js +0 -82
- package/dist/intelligence/indexer/plugins/ruby.js +0 -290
- package/dist/intelligence/indexer/plugins/rust.js +0 -484
- package/dist/intelligence/indexer/plugins/tier2-generic.js +0 -310
- package/dist/intelligence/indexer/plugins/typescript.js +0 -456
- package/dist/intelligence/indexer/resource-monitor.js +0 -93
- package/dist/intelligence/indexer/scip/decoder.js +0 -253
- package/dist/intelligence/indexer/scip/detector.js +0 -232
- package/dist/intelligence/indexer/scip/downloader.js +0 -427
- package/dist/intelligence/indexer/scip/fallback.js +0 -34
- package/dist/intelligence/indexer/scip/merger.js +0 -109
- package/dist/intelligence/indexer/scip/orchestrator.js +0 -433
- package/dist/intelligence/indexer/scip/runner.js +0 -98
- package/dist/intelligence/indexer/snapshot.js +0 -66
- package/dist/intelligence/indexer/test-detector.js +0 -196
- package/dist/intelligence/indexer/watch-integration.js +0 -61
- package/dist/intelligence/indexer/worker.js +0 -85
- package/dist/intelligence/local-convention-detector.js +0 -437
- package/dist/intelligence/local-embeddings.js +0 -190
- package/dist/intelligence/local-graph.js +0 -1946
- package/dist/intelligence/local-indexer.js +0 -1575
- package/dist/intelligence/local-llm.js +0 -163
- package/dist/intelligence/local-rule-generator.js +0 -154
- package/dist/intelligence/local-snapshot.js +0 -213
- package/dist/intelligence/negative-knowledge.js +0 -103
- package/dist/intelligence/persistent-db.js +0 -85
- package/dist/intelligence/query-router.js +0 -2556
- package/dist/intelligence/risk-classifier.js +0 -116
- package/dist/intelligence/rule-evaluator.js +0 -380
- package/dist/intelligence/rule-generator.js +0 -49
- package/dist/intelligence/search-index.js +0 -173
- package/dist/intelligence/semantic/docstring-extractor.js +0 -67
- package/dist/intelligence/semantic/embedding-store.js +0 -52
- package/dist/intelligence/semantic/enrichment-orchestrator.js +0 -48
- package/dist/intelligence/semantic/git-message-miner.js +0 -114
- package/dist/intelligence/semantic/identifier-tokenizer.js +0 -51
- package/dist/intelligence/semantic/node2vec-embeddings.js +0 -71
- package/dist/intelligence/semantic/node2vec-walks.js +0 -103
- package/dist/intelligence/semantic/path-domain-inference.js +0 -112
- package/dist/intelligence/semantic/similarity-engine.js +0 -60
- package/dist/intelligence/semantic/tfidf-vectors.js +0 -88
- package/dist/intelligence/session-brief-builder.js +0 -159
- package/dist/intelligence/session-context.js +0 -221
- package/dist/intelligence/session-health-monitor.js +0 -211
- package/dist/intelligence/session-narrative.js +0 -197
- package/dist/intelligence/session-pattern-analyzer.js +0 -218
- package/dist/intelligence/signal-scorer.js +0 -390
- package/dist/intelligence/signal-show-store.js +0 -182
- package/dist/intelligence/smart-truncate.js +0 -158
- package/dist/intelligence/subgraph-cache.js +0 -88
- package/dist/intelligence/temporal-facts.js +0 -494
- package/dist/intelligence/token-estimator.js +0 -100
- package/dist/intelligence/tool-injector.js +0 -87
- package/dist/intelligence/tree-sitter-loader.js +0 -71
- package/dist/intelligence/worker-pool.js +0 -116
- package/dist/proxy/arg-validator.js +0 -79
- package/dist/proxy/auto-bootstrap.js +0 -167
- package/dist/proxy/bridge.js +0 -147
- package/dist/proxy/budget-enforcer.js +0 -70
- package/dist/proxy/compression-quality-monitor.js +0 -160
- package/dist/proxy/compression-stats.js +0 -51
- package/dist/proxy/context-rot-detector.js +0 -137
- package/dist/proxy/drift-detector.js +0 -139
- package/dist/proxy/efficiency-tracker.js +0 -79
- package/dist/proxy/fact-ranking.js +0 -154
- package/dist/proxy/format-encoder.js +0 -266
- package/dist/proxy/http-transport.js +0 -90
- package/dist/proxy/lifecycle-actor.js +0 -55
- package/dist/proxy/lifecycle-machine.js +0 -187
- package/dist/proxy/log-tailer.js +0 -265
- package/dist/proxy/model-pricing.js +0 -98
- package/dist/proxy/network-firewall.js +0 -141
- package/dist/proxy/nudge-state.js +0 -93
- package/dist/proxy/output-compressor.js +0 -185
- package/dist/proxy/pid-lock.js +0 -291
- package/dist/proxy/proxy-context.js +0 -11
- package/dist/proxy/proxy.js +0 -2633
- package/dist/proxy/response-enrichment.js +0 -32
- package/dist/proxy/response-envelope.js +0 -313
- package/dist/proxy/session-dedup.js +0 -82
- package/dist/proxy/session-legend.js +0 -30
- package/dist/proxy/session-persistence.js +0 -210
- package/dist/proxy/session-resume.js +0 -94
- package/dist/proxy/session-stats.js +0 -513
- package/dist/proxy/shell-classifier.js +0 -1346
- package/dist/proxy/shell-compression-log.js +0 -93
- package/dist/proxy/shell-compressor.js +0 -390
- package/dist/proxy/shell-graph-boost.js +0 -202
- package/dist/proxy/shell-monitor-map.js +0 -18
- package/dist/proxy/shell-stats.js +0 -54
- package/dist/proxy/shell-strategies/cloud.js +0 -215
- package/dist/proxy/shell-strategies/diff.js +0 -159
- package/dist/proxy/shell-strategies/error-diagnostic.js +0 -796
- package/dist/proxy/shell-strategies/filter-dsl.js +0 -358
- package/dist/proxy/shell-strategies/git-status.js +0 -177
- package/dist/proxy/shell-strategies/key-value.js +0 -193
- package/dist/proxy/shell-strategies/log-text.js +0 -154
- package/dist/proxy/shell-strategies/omni.js +0 -188
- package/dist/proxy/shell-strategies/progress.js +0 -55
- package/dist/proxy/shell-strategies/redact.js +0 -76
- package/dist/proxy/shell-strategies/structured.js +0 -241
- package/dist/proxy/shell-strategies/tabular.js +0 -243
- package/dist/proxy/shell-strategies/test-results-types.js +0 -13
- package/dist/proxy/shell-strategies/test-results.js +0 -784
- package/dist/proxy/shell-strategies/tree-paths.js +0 -144
- package/dist/proxy/shell-strategies/yaml.js +0 -182
- package/dist/proxy/shell-tee.js +0 -111
- package/dist/proxy/signal-dedup.js +0 -171
- package/dist/proxy/startup-renderer.js +0 -158
- package/dist/proxy/task-token-display.js +0 -38
- package/dist/proxy/token-counter.js +0 -61
- package/dist/proxy/tool-clusters.js +0 -273
- package/dist/proxy/tool-definitions.js +0 -525
- package/dist/proxy/transport-mux.js +0 -229
- package/dist/proxy/wire-cap.js +0 -268
- package/dist/rules/developer.mozilla.org.json +0 -9
- package/dist/rules/github.com.json +0 -21
- package/dist/schemas/api/skills.js +0 -19
- package/dist/schemas/common/errors.js +0 -7
- package/dist/schemas/common/headers.js +0 -5
- package/dist/schemas/entities/edge.js +0 -25
- package/dist/schemas/entities/entity.js +0 -22
- package/dist/schemas/entities/rule.js +0 -18
- package/dist/schemas/index.js +0 -14
- package/dist/server/event-bus.js +0 -59
- package/dist/server/http.js +0 -156
- package/dist/server/middleware.js +0 -70
- package/dist/server/routes/drift.js +0 -97
- package/dist/server/routes/intelligence.js +0 -1217
- package/dist/server/routes/reasoning-quality.js +0 -444
- package/dist/server/routes/session.js +0 -86
- package/dist/server/routes/stream.js +0 -120
- package/dist/server/routes/system.js +0 -73
- package/dist/server/routes/temporal.js +0 -170
- package/dist/server/routes/timeline.js +0 -232
- package/dist/server/routes/token-flow.js +0 -403
- package/dist/skills/effectiveness-tracker.js +0 -93
- package/dist/skills/local-pack.js +0 -380
- package/dist/skills/resolver.js +0 -495
- package/dist/state-detector.js +0 -83
- package/dist/timeline/intent-detector.js +0 -263
- package/dist/timeline/loop-miner.js +0 -140
- package/dist/timeline/open-threads.js +0 -49
- package/dist/timeline/signal-reinforcer.js +0 -62
- package/dist/timeline/timeline-bootstrap.js +0 -151
- package/dist/timeline/timeline-store.js +0 -618
- package/dist/tools/coding/bash.js +0 -49
- package/dist/tools/coding/file-edit.js +0 -72
- package/dist/tools/coding/file-outline.js +0 -227
- package/dist/tools/coding/file-read-protocol.js +0 -425
- package/dist/tools/coding/file-read.js +0 -35
- package/dist/tools/coding/file-write.js +0 -43
- package/dist/tools/coding/glob-tool.js +0 -109
- package/dist/tools/coding/grep.js +0 -162
- package/dist/tools/coding/index.js +0 -27
- package/dist/tools/intelligence/index.js +0 -269
- package/dist/tools/intelligence/record-fact.js +0 -48
- package/dist/tools/intelligence/timeline-markers.js +0 -130
- package/dist/tools/registry.js +0 -47
- package/dist/tools/types.js +0 -8
- package/dist/tracking/auto-snapshot-triggers.js +0 -246
- package/dist/tracking/branch-context.js +0 -115
- package/dist/tracking/branch-snapshot.js +0 -217
- package/dist/tracking/causal-bridge.js +0 -317
- package/dist/tracking/circuit-breaker.js +0 -147
- package/dist/tracking/commit-watcher.js +0 -114
- package/dist/tracking/context-ledger.js +0 -119
- package/dist/tracking/correction-detector.js +0 -324
- package/dist/tracking/drift-tracker.js +0 -874
- package/dist/tracking/durability-tracker.js +0 -94
- package/dist/tracking/entity-rewind.js +0 -200
- package/dist/tracking/file-hash-state.js +0 -114
- package/dist/tracking/git-attribution.js +0 -132
- package/dist/tracking/git-trailers.js +0 -171
- package/dist/tracking/intelligence-counter.js +0 -46
- package/dist/tracking/intent-correlator.js +0 -202
- package/dist/tracking/intent-encoder.js +0 -52
- package/dist/tracking/intent-token-tracker.js +0 -159
- package/dist/tracking/ledger-archiver.js +0 -94
- package/dist/tracking/ledger-chains.js +0 -245
- package/dist/tracking/metrics-store.js +0 -361
- package/dist/tracking/native-watcher.js +0 -131
- package/dist/tracking/offline-rewind.js +0 -295
- package/dist/tracking/pending-violations.js +0 -74
- package/dist/tracking/persistence-effectiveness.js +0 -167
- package/dist/tracking/prompt-durability.js +0 -202
- package/dist/tracking/quality-signals.js +0 -213
- package/dist/tracking/redactor.js +0 -73
- package/dist/tracking/rewind-engine.js +0 -161
- package/dist/tracking/session-history.js +0 -128
- package/dist/tracking/session-receipt.js +0 -88
- package/dist/tracking/session-summary-writer.js +0 -157
- package/dist/tracking/shadow-ledger.js +0 -321
- package/dist/tracking/stash-manager.js +0 -258
- package/dist/tracking/timeline-fork.js +0 -213
- package/dist/tracking/timeline.js +0 -69
- package/dist/tracking/token-flow.js +0 -276
- package/dist/tracking/turn-segmenter.js +0 -122
- package/dist/tracking/weekly-accumulator.js +0 -179
- package/dist/tracking/working-snapshots.js +0 -188
- package/dist/tracking/workspace-manifest.js +0 -176
- package/dist/transport/http.js +0 -102
- package/dist/utils/counterfactual.js +0 -65
- package/dist/utils/deep-link.js +0 -34
- package/dist/utils/detect.js +0 -193
- package/dist/utils/exec.js +0 -73
- package/dist/utils/file-logger.js +0 -87
- package/dist/utils/format-error.js +0 -29
- package/dist/utils/git.js +0 -181
- package/dist/utils/log.js +0 -57
- package/dist/utils/logger.js +0 -35
- package/dist/utils/mcp-content-json.js +0 -8
- package/dist/utils/session-logger.js +0 -154
- package/dist/utils/startup-log.js +0 -512
- package/dist/utils/ui.js +0 -56
|
@@ -1,317 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Causal Bridge — assembles causal chains linking AI interactions to code survival.
|
|
3
|
-
*
|
|
4
|
-
* Given an entity key, traces all AI interactions (from shadow ledger) through
|
|
5
|
-
* commit associations to survival outcomes. Produces a CausalChain with per-interaction
|
|
6
|
-
* survival classification and aggregate durability.
|
|
7
|
-
*
|
|
8
|
-
* Survival window: 24 hours. If an entity survives 24h without being reverted
|
|
9
|
-
* or modified, the interaction is classified as "survived".
|
|
10
|
-
*
|
|
11
|
-
* Outcome classification:
|
|
12
|
-
* - survived: entity unchanged after 24h
|
|
13
|
-
* - reverted: entity reverted via git revert/checkout within 24h
|
|
14
|
-
* - human_modified: entity changed by human commit within 24h
|
|
15
|
-
* - ai_modified: entity changed by another AI interaction within 24h
|
|
16
|
-
*/
|
|
17
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
18
|
-
import { join } from "node:path";
|
|
19
|
-
import { gitQuery } from "../utils/exec.js";
|
|
20
|
-
import { createModuleLogger } from "../utils/logger.js";
|
|
21
|
-
const log = createModuleLogger("causal-bridge");
|
|
22
|
-
const SURVIVAL_WINDOW_MS = 24 * 60 * 60 * 1000;
|
|
23
|
-
const AI_COMMIT_PATTERNS = [
|
|
24
|
-
/\bgenerated\b/i,
|
|
25
|
-
/\bauto[-\s]?generated\b/i,
|
|
26
|
-
/\bai[-\s]?assisted\b/i,
|
|
27
|
-
/\bcopilot\b/i,
|
|
28
|
-
/\bcursor\b/i,
|
|
29
|
-
/\bclaude\b/i,
|
|
30
|
-
/\bgpt\b/i,
|
|
31
|
-
/\bunerr\b/i,
|
|
32
|
-
];
|
|
33
|
-
export class CausalBridge {
|
|
34
|
-
unerrDir;
|
|
35
|
-
cwd;
|
|
36
|
-
commitCache = new Map();
|
|
37
|
-
constructor(unerrDir, cwd) {
|
|
38
|
-
this.unerrDir = unerrDir;
|
|
39
|
-
this.cwd = cwd;
|
|
40
|
-
}
|
|
41
|
-
async buildCausalChain(entityKey) {
|
|
42
|
-
const entityName = extractEntityName(entityKey);
|
|
43
|
-
const ledgerEntries = this.loadEntityLedgerEntries(entityKey);
|
|
44
|
-
if (ledgerEntries.length === 0) {
|
|
45
|
-
return {
|
|
46
|
-
entityKey,
|
|
47
|
-
entityName,
|
|
48
|
-
interactions: [],
|
|
49
|
-
durability: 1.0,
|
|
50
|
-
failureModes: [],
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
|
-
const commits = await this.loadEntityCommitHistory(entityKey);
|
|
54
|
-
const interactions = [];
|
|
55
|
-
const failureModeSet = new Set();
|
|
56
|
-
for (const entry of ledgerEntries) {
|
|
57
|
-
const interaction = await this.classifyInteraction(entry, commits, entityKey);
|
|
58
|
-
interactions.push(interaction);
|
|
59
|
-
if (!interaction.survived) {
|
|
60
|
-
failureModeSet.add(interaction.outcome);
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
const durability = computeAggregateDurability(interactions);
|
|
64
|
-
return {
|
|
65
|
-
entityKey,
|
|
66
|
-
entityName,
|
|
67
|
-
interactions,
|
|
68
|
-
durability,
|
|
69
|
-
failureModes: Array.from(failureModeSet),
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
async buildCausalChains(entityKeys) {
|
|
73
|
-
const chains = [];
|
|
74
|
-
for (const key of entityKeys) {
|
|
75
|
-
const chain = await this.buildCausalChain(key);
|
|
76
|
-
chains.push(chain);
|
|
77
|
-
}
|
|
78
|
-
return chains;
|
|
79
|
-
}
|
|
80
|
-
loadEntityLedgerEntries(entityKey) {
|
|
81
|
-
const ledgerPath = join(this.unerrDir, "ledger", "shadow.jsonl");
|
|
82
|
-
if (!existsSync(ledgerPath))
|
|
83
|
-
return [];
|
|
84
|
-
const content = readFileSync(ledgerPath, "utf-8");
|
|
85
|
-
const lines = content.split("\n").filter((l) => l.trim().length > 0);
|
|
86
|
-
const entries = [];
|
|
87
|
-
for (const line of lines) {
|
|
88
|
-
try {
|
|
89
|
-
const entry = JSON.parse(line);
|
|
90
|
-
if (entryReferencesEntity(entry, entityKey)) {
|
|
91
|
-
entries.push(entry);
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
catch { }
|
|
95
|
-
}
|
|
96
|
-
return entries.sort((a, b) => new Date(a.ts).getTime() - new Date(b.ts).getTime());
|
|
97
|
-
}
|
|
98
|
-
async loadEntityCommitHistory(entityKey) {
|
|
99
|
-
const filePath = entityKeyToFilePath(entityKey);
|
|
100
|
-
if (!filePath)
|
|
101
|
-
return [];
|
|
102
|
-
const raw = await gitQuery([
|
|
103
|
-
"log",
|
|
104
|
-
"--format=%H|%at|%s|%an",
|
|
105
|
-
"--follow",
|
|
106
|
-
"--diff-filter=ACDMR",
|
|
107
|
-
"--",
|
|
108
|
-
filePath,
|
|
109
|
-
], this.cwd);
|
|
110
|
-
if (!raw)
|
|
111
|
-
return [];
|
|
112
|
-
const commits = [];
|
|
113
|
-
for (const line of raw.split("\n")) {
|
|
114
|
-
const trimmed = line.trim();
|
|
115
|
-
if (!trimmed)
|
|
116
|
-
continue;
|
|
117
|
-
const parts = trimmed.split("|");
|
|
118
|
-
if (parts.length < 4)
|
|
119
|
-
continue;
|
|
120
|
-
const sha = parts[0];
|
|
121
|
-
const timestamp = Number.parseInt(parts[1], 10) * 1000;
|
|
122
|
-
const message = parts[2];
|
|
123
|
-
const author = parts.slice(3).join("|");
|
|
124
|
-
if (this.commitCache.has(sha)) {
|
|
125
|
-
commits.push(this.commitCache.get(sha));
|
|
126
|
-
continue;
|
|
127
|
-
}
|
|
128
|
-
const filesRaw = await gitQuery(["diff-tree", "--no-commit-id", "--name-only", "-r", sha], this.cwd);
|
|
129
|
-
const files = filesRaw
|
|
130
|
-
? filesRaw
|
|
131
|
-
.split("\n")
|
|
132
|
-
.map((f) => f.trim())
|
|
133
|
-
.filter(Boolean)
|
|
134
|
-
: [];
|
|
135
|
-
const info = { sha, timestamp, message, author, files };
|
|
136
|
-
this.commitCache.set(sha, info);
|
|
137
|
-
commits.push(info);
|
|
138
|
-
}
|
|
139
|
-
return commits.sort((a, b) => a.timestamp - b.timestamp);
|
|
140
|
-
}
|
|
141
|
-
async classifyInteraction(entry, commits, entityKey) {
|
|
142
|
-
const entryTs = new Date(entry.ts).getTime();
|
|
143
|
-
const prompt = extractPrompt(entry);
|
|
144
|
-
const commitSha = entry.commit_sha ?? "";
|
|
145
|
-
const sessionId = entry.session_id;
|
|
146
|
-
const subsequentCommits = commits.filter((c) => c.timestamp > entryTs && c.timestamp <= entryTs + SURVIVAL_WINDOW_MS);
|
|
147
|
-
if (subsequentCommits.length === 0) {
|
|
148
|
-
const timeSinceEntry = Date.now() - entryTs;
|
|
149
|
-
const survived = timeSinceEntry >= SURVIVAL_WINDOW_MS;
|
|
150
|
-
return {
|
|
151
|
-
prompt,
|
|
152
|
-
commitSha,
|
|
153
|
-
sessionId,
|
|
154
|
-
timestamp: entry.ts,
|
|
155
|
-
survived,
|
|
156
|
-
outcome: survived ? "survived" : "survived",
|
|
157
|
-
survivalMs: timeSinceEntry,
|
|
158
|
-
};
|
|
159
|
-
}
|
|
160
|
-
const filePath = entityKeyToFilePath(entityKey);
|
|
161
|
-
for (const commit of subsequentCommits) {
|
|
162
|
-
if (!filePath || !commit.files.includes(filePath))
|
|
163
|
-
continue;
|
|
164
|
-
const survivalMs = commit.timestamp - entryTs;
|
|
165
|
-
if (isRevertCommit(commit.message)) {
|
|
166
|
-
return {
|
|
167
|
-
prompt,
|
|
168
|
-
commitSha,
|
|
169
|
-
sessionId,
|
|
170
|
-
timestamp: entry.ts,
|
|
171
|
-
survived: false,
|
|
172
|
-
outcome: "reverted",
|
|
173
|
-
survivalMs,
|
|
174
|
-
};
|
|
175
|
-
}
|
|
176
|
-
if (isAiCommit(commit.message)) {
|
|
177
|
-
return {
|
|
178
|
-
prompt,
|
|
179
|
-
commitSha,
|
|
180
|
-
sessionId,
|
|
181
|
-
timestamp: entry.ts,
|
|
182
|
-
survived: false,
|
|
183
|
-
outcome: "ai_modified",
|
|
184
|
-
survivalMs,
|
|
185
|
-
};
|
|
186
|
-
}
|
|
187
|
-
return {
|
|
188
|
-
prompt,
|
|
189
|
-
commitSha,
|
|
190
|
-
sessionId,
|
|
191
|
-
timestamp: entry.ts,
|
|
192
|
-
survived: false,
|
|
193
|
-
outcome: "human_modified",
|
|
194
|
-
survivalMs,
|
|
195
|
-
};
|
|
196
|
-
}
|
|
197
|
-
return {
|
|
198
|
-
prompt,
|
|
199
|
-
commitSha,
|
|
200
|
-
sessionId,
|
|
201
|
-
timestamp: entry.ts,
|
|
202
|
-
survived: true,
|
|
203
|
-
outcome: "survived",
|
|
204
|
-
survivalMs: Date.now() - entryTs,
|
|
205
|
-
};
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
function computeAggregateDurability(interactions) {
|
|
209
|
-
if (interactions.length === 0)
|
|
210
|
-
return 1.0;
|
|
211
|
-
const survivedCount = interactions.filter((i) => i.survived).length;
|
|
212
|
-
return survivedCount / interactions.length;
|
|
213
|
-
}
|
|
214
|
-
function extractEntityName(entityKey) {
|
|
215
|
-
const parts = entityKey.split("::");
|
|
216
|
-
return parts[parts.length - 1] ?? entityKey;
|
|
217
|
-
}
|
|
218
|
-
function entityKeyToFilePath(entityKey) {
|
|
219
|
-
const fileMatch = entityKey.match(/^([^:]+)/);
|
|
220
|
-
if (!fileMatch)
|
|
221
|
-
return null;
|
|
222
|
-
return fileMatch[1];
|
|
223
|
-
}
|
|
224
|
-
function entryReferencesEntity(entry, entityKey) {
|
|
225
|
-
const argsStr = JSON.stringify(entry.args_summary);
|
|
226
|
-
if (argsStr.includes(entityKey))
|
|
227
|
-
return true;
|
|
228
|
-
const resultStr = JSON.stringify(entry.result_summary);
|
|
229
|
-
if (resultStr.includes(entityKey))
|
|
230
|
-
return true;
|
|
231
|
-
const filePath = entityKeyToFilePath(entityKey);
|
|
232
|
-
if (filePath && argsStr.includes(filePath))
|
|
233
|
-
return true;
|
|
234
|
-
return false;
|
|
235
|
-
}
|
|
236
|
-
function extractPrompt(entry) {
|
|
237
|
-
if (entry.plan_summary)
|
|
238
|
-
return entry.plan_summary;
|
|
239
|
-
const args = entry.args_summary;
|
|
240
|
-
if (typeof args.prompt === "string")
|
|
241
|
-
return args.prompt;
|
|
242
|
-
if (typeof args.message === "string")
|
|
243
|
-
return args.message;
|
|
244
|
-
if (typeof args.query === "string")
|
|
245
|
-
return args.query;
|
|
246
|
-
return `${entry.tool} call`;
|
|
247
|
-
}
|
|
248
|
-
function isRevertCommit(message) {
|
|
249
|
-
const lower = message.toLowerCase();
|
|
250
|
-
return (lower.startsWith("revert ") ||
|
|
251
|
-
lower.includes("revert:") ||
|
|
252
|
-
lower.includes("undo ") ||
|
|
253
|
-
lower.includes("rollback "));
|
|
254
|
-
}
|
|
255
|
-
function isAiCommit(message) {
|
|
256
|
-
return AI_COMMIT_PATTERNS.some((p) => p.test(message));
|
|
257
|
-
}
|
|
258
|
-
/**
|
|
259
|
-
* Standalone causal chain assembly from pre-loaded ledger entries.
|
|
260
|
-
* Does not require git — classifies based on entry data alone.
|
|
261
|
-
*/
|
|
262
|
-
export function assembleCausalChain(entityKey, entries) {
|
|
263
|
-
const entityName = extractEntityName(entityKey);
|
|
264
|
-
const relevant = entries.filter((e) => entryReferencesEntity(e, entityKey));
|
|
265
|
-
const interactions = [];
|
|
266
|
-
const failureModeSet = new Set();
|
|
267
|
-
for (let i = 0; i < relevant.length; i++) {
|
|
268
|
-
const entry = relevant[i];
|
|
269
|
-
const entryTs = new Date(entry.ts).getTime();
|
|
270
|
-
const prompt = extractPrompt(entry);
|
|
271
|
-
const commitSha = entry.commit_sha ??
|
|
272
|
-
(typeof entry.result_summary.commit_sha === "string"
|
|
273
|
-
? entry.result_summary.commit_sha
|
|
274
|
-
: "");
|
|
275
|
-
let survived = true;
|
|
276
|
-
let outcome = "survived";
|
|
277
|
-
let survivalMs = Date.now() - entryTs;
|
|
278
|
-
if (i + 1 < relevant.length) {
|
|
279
|
-
const nextEntry = relevant[i + 1];
|
|
280
|
-
const nextTs = new Date(nextEntry.ts).getTime();
|
|
281
|
-
survivalMs = nextTs - entryTs;
|
|
282
|
-
if (survivalMs < SURVIVAL_WINDOW_MS) {
|
|
283
|
-
survived = false;
|
|
284
|
-
outcome = "ai_modified";
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
interactions.push({
|
|
288
|
-
prompt,
|
|
289
|
-
commitSha,
|
|
290
|
-
sessionId: entry.session_id,
|
|
291
|
-
timestamp: entry.ts,
|
|
292
|
-
survived,
|
|
293
|
-
outcome,
|
|
294
|
-
survivalMs,
|
|
295
|
-
});
|
|
296
|
-
if (!survived) {
|
|
297
|
-
failureModeSet.add(outcome);
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
return {
|
|
301
|
-
entityKey,
|
|
302
|
-
entityName,
|
|
303
|
-
interactions,
|
|
304
|
-
durability: computeDurability(interactions),
|
|
305
|
-
failureModes: Array.from(failureModeSet),
|
|
306
|
-
};
|
|
307
|
-
}
|
|
308
|
-
/**
|
|
309
|
-
* Computes aggregate durability from a set of interactions.
|
|
310
|
-
* Durability = fraction of interactions that survived.
|
|
311
|
-
*/
|
|
312
|
-
export function computeDurability(interactions) {
|
|
313
|
-
if (interactions.length === 0)
|
|
314
|
-
return 1.0;
|
|
315
|
-
const survivedCount = interactions.filter((i) => i.survived).length;
|
|
316
|
-
return survivedCount / interactions.length;
|
|
317
|
-
}
|
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Sprint 10.6: Ledger Circuit Breaker — hallucination loop detection.
|
|
3
|
-
*
|
|
4
|
-
* Detects when the AI enters a fix-break-fix-break cycle:
|
|
5
|
-
* >4 consecutive broken entries on the same entity within 10 minutes.
|
|
6
|
-
* Attaches a circuit_breaker payload to the internal `meta` carrier, which
|
|
7
|
-
* `buildSignalPrefix()` drains into a `ur|hlt` prefix line on the response.
|
|
8
|
-
*
|
|
9
|
-
* Design authority: Phase 5.5 §1.2.8b (Ledger Circuit Breaker)
|
|
10
|
-
*/
|
|
11
|
-
/** stderr logger */
|
|
12
|
-
const _log = {
|
|
13
|
-
info: (msg) => process.stderr.write(`[unerr:circuit-breaker] ${msg}\n`),
|
|
14
|
-
warn: (msg) => process.stderr.write(`[unerr:circuit-breaker] WARN: ${msg}\n`),
|
|
15
|
-
};
|
|
16
|
-
/** Threshold: consecutive failed modifications to trigger halt. */
|
|
17
|
-
const CONSECUTIVE_THRESHOLD = 4;
|
|
18
|
-
/** Window: 10 minutes in milliseconds. */
|
|
19
|
-
const WINDOW_MS = 10 * 60 * 1000;
|
|
20
|
-
export class LedgerCircuitBreaker {
|
|
21
|
-
/**
|
|
22
|
-
* Per-entity tracking of recent modification attempts.
|
|
23
|
-
* Key: entity name or entity key.
|
|
24
|
-
*/
|
|
25
|
-
entityAttempts = new Map();
|
|
26
|
-
/**
|
|
27
|
-
* Entities currently halted. Cleared when entity changes or user resets.
|
|
28
|
-
*/
|
|
29
|
-
haltedEntities = new Set();
|
|
30
|
-
/**
|
|
31
|
-
* Record a sync_local_diff that touched an entity.
|
|
32
|
-
* Call this after rule evaluation on sync.
|
|
33
|
-
*/
|
|
34
|
-
recordAttempt(entity, hadViolations) {
|
|
35
|
-
const attempts = this.entityAttempts.get(entity) ?? [];
|
|
36
|
-
attempts.push({ timestamp: Date.now(), hadViolations });
|
|
37
|
-
this.entityAttempts.set(entity, attempts);
|
|
38
|
-
// Prune old attempts outside the window
|
|
39
|
-
this.pruneOldAttempts(entity);
|
|
40
|
-
}
|
|
41
|
-
/**
|
|
42
|
-
* Check if any entity has tripped the circuit breaker.
|
|
43
|
-
* Returns metadata to inject into _meta, or null if no trip.
|
|
44
|
-
*/
|
|
45
|
-
check(entities) {
|
|
46
|
-
for (const entity of entities) {
|
|
47
|
-
if (this.haltedEntities.has(entity)) {
|
|
48
|
-
const attempts = this.entityAttempts.get(entity) ?? [];
|
|
49
|
-
return {
|
|
50
|
-
triggered: true,
|
|
51
|
-
entity,
|
|
52
|
-
attempts: attempts.length,
|
|
53
|
-
message: formatHaltMessage(entity, attempts.length),
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
const attempts = this.entityAttempts.get(entity) ?? [];
|
|
57
|
-
if (attempts.length < CONSECUTIVE_THRESHOLD)
|
|
58
|
-
continue;
|
|
59
|
-
// Check if last N attempts all had violations within the window
|
|
60
|
-
const now = Date.now();
|
|
61
|
-
const recentAttempts = attempts.filter((a) => now - a.timestamp < WINDOW_MS);
|
|
62
|
-
if (recentAttempts.length < CONSECUTIVE_THRESHOLD)
|
|
63
|
-
continue;
|
|
64
|
-
// Check if the last CONSECUTIVE_THRESHOLD attempts all had violations
|
|
65
|
-
const lastN = recentAttempts.slice(-CONSECUTIVE_THRESHOLD);
|
|
66
|
-
const allHadViolations = lastN.every((a) => a.hadViolations);
|
|
67
|
-
if (allHadViolations) {
|
|
68
|
-
this.haltedEntities.add(entity);
|
|
69
|
-
_log.warn(`Circuit breaker tripped for entity: ${entity} (${lastN.length} consecutive failures)`);
|
|
70
|
-
return {
|
|
71
|
-
triggered: true,
|
|
72
|
-
entity,
|
|
73
|
-
attempts: lastN.length,
|
|
74
|
-
message: formatHaltMessage(entity, lastN.length),
|
|
75
|
-
};
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
return null;
|
|
79
|
-
}
|
|
80
|
-
/**
|
|
81
|
-
* Reset the circuit breaker for a specific entity.
|
|
82
|
-
* Called when the entity changes externally or user provides new instruction.
|
|
83
|
-
*/
|
|
84
|
-
reset(entity) {
|
|
85
|
-
this.haltedEntities.delete(entity);
|
|
86
|
-
this.entityAttempts.delete(entity);
|
|
87
|
-
_log.info(`Circuit breaker reset for: ${entity}`);
|
|
88
|
-
}
|
|
89
|
-
/**
|
|
90
|
-
* Reset all circuit breakers.
|
|
91
|
-
*/
|
|
92
|
-
resetAll() {
|
|
93
|
-
this.haltedEntities.clear();
|
|
94
|
-
this.entityAttempts.clear();
|
|
95
|
-
_log.info("All circuit breakers reset");
|
|
96
|
-
}
|
|
97
|
-
/**
|
|
98
|
-
* Get list of currently halted entities.
|
|
99
|
-
*/
|
|
100
|
-
getHaltedEntities() {
|
|
101
|
-
return [...this.haltedEntities];
|
|
102
|
-
}
|
|
103
|
-
/**
|
|
104
|
-
* Check if a specific entity is halted.
|
|
105
|
-
*/
|
|
106
|
-
isHalted(entity) {
|
|
107
|
-
return this.haltedEntities.has(entity);
|
|
108
|
-
}
|
|
109
|
-
/**
|
|
110
|
-
* Generate _context injection for the response envelope.
|
|
111
|
-
* Returns null if no circuit breaker is triggered for the given entities.
|
|
112
|
-
*/
|
|
113
|
-
toContextInjection(entities) {
|
|
114
|
-
const meta = this.check(entities);
|
|
115
|
-
if (!meta)
|
|
116
|
-
return null;
|
|
117
|
-
return {
|
|
118
|
-
"dev.unerr/circuit_breaker": {
|
|
119
|
-
triggered: meta.triggered,
|
|
120
|
-
entity: meta.entity,
|
|
121
|
-
attempts: meta.attempts,
|
|
122
|
-
message: meta.message,
|
|
123
|
-
action: "Review previous attempts before retrying. Consider a different approach.",
|
|
124
|
-
},
|
|
125
|
-
};
|
|
126
|
-
}
|
|
127
|
-
pruneOldAttempts(entity) {
|
|
128
|
-
const attempts = this.entityAttempts.get(entity);
|
|
129
|
-
if (!attempts)
|
|
130
|
-
return;
|
|
131
|
-
const cutoff = Date.now() - WINDOW_MS;
|
|
132
|
-
const pruned = attempts.filter((a) => a.timestamp > cutoff);
|
|
133
|
-
if (pruned.length === 0) {
|
|
134
|
-
this.entityAttempts.delete(entity);
|
|
135
|
-
this.haltedEntities.delete(entity);
|
|
136
|
-
}
|
|
137
|
-
else {
|
|
138
|
-
this.entityAttempts.set(entity, pruned);
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
/**
|
|
143
|
-
* S9.4: Format halt message — clear, actionable, includes attempt count.
|
|
144
|
-
*/
|
|
145
|
-
export function formatHaltMessage(entity, attempts) {
|
|
146
|
-
return `Stop. You've tried modifying ${entity} ${attempts} times with violations each time. Each attempt produced rule violations within the last 10 minutes. Consider a fundamentally different approach or ask the user for guidance.`;
|
|
147
|
-
}
|
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Commit Watcher — detects git commits by polling HEAD SHA.
|
|
3
|
-
*
|
|
4
|
-
* Polls `git rev-parse HEAD` every 5s (same cadence as branch poller).
|
|
5
|
-
* When HEAD SHA changes:
|
|
6
|
-
* 1. Extracts changed files via `git diff --name-only {old}..{new}`
|
|
7
|
-
* 2. Calls IntentCorrelator.associateCommit(commitSha, files)
|
|
8
|
-
* 3. Optionally triggers a flush callback
|
|
9
|
-
*
|
|
10
|
-
* All logging to stderr. Timer is unref'd so it doesn't keep the process alive.
|
|
11
|
-
*/
|
|
12
|
-
import { getChangedFiles, getHeadSha } from "../utils/git.js";
|
|
13
|
-
import { createModuleLogger } from "../utils/logger.js";
|
|
14
|
-
import { encodeIntentAsNote } from "./intent-encoder.js";
|
|
15
|
-
const log = createModuleLogger("commit");
|
|
16
|
-
const DEFAULT_POLL_MS = 5_000;
|
|
17
|
-
export class CommitWatcher {
|
|
18
|
-
cwd;
|
|
19
|
-
pollIntervalMs;
|
|
20
|
-
correlator;
|
|
21
|
-
onCommit;
|
|
22
|
-
timer = null;
|
|
23
|
-
lastHeadSha = null;
|
|
24
|
-
running = false;
|
|
25
|
-
sessionId;
|
|
26
|
-
branchContext = null;
|
|
27
|
-
driftSummaryFn = null;
|
|
28
|
-
constructor(correlator, opts = {}) {
|
|
29
|
-
this.cwd = opts.cwd ?? process.cwd();
|
|
30
|
-
this.pollIntervalMs = opts.pollIntervalMs ?? DEFAULT_POLL_MS;
|
|
31
|
-
this.correlator = correlator;
|
|
32
|
-
this.onCommit = opts.onCommit;
|
|
33
|
-
this.sessionId = opts.sessionId ?? "";
|
|
34
|
-
}
|
|
35
|
-
/**
|
|
36
|
-
* Set branch context for git note encoding.
|
|
37
|
-
*/
|
|
38
|
-
setBranchContext(ctx) {
|
|
39
|
-
this.branchContext = ctx;
|
|
40
|
-
}
|
|
41
|
-
/**
|
|
42
|
-
* Set drift summary provider for git note encoding.
|
|
43
|
-
*/
|
|
44
|
-
setDriftSummaryFn(fn) {
|
|
45
|
-
this.driftSummaryFn = fn;
|
|
46
|
-
}
|
|
47
|
-
/**
|
|
48
|
-
* Start polling for commits. Captures initial HEAD SHA on first call.
|
|
49
|
-
*/
|
|
50
|
-
async start() {
|
|
51
|
-
if (this.running)
|
|
52
|
-
return;
|
|
53
|
-
this.running = true;
|
|
54
|
-
this.lastHeadSha = await getHeadSha(this.cwd);
|
|
55
|
-
this.timer = setInterval(() => {
|
|
56
|
-
this.poll();
|
|
57
|
-
}, this.pollIntervalMs);
|
|
58
|
-
// Don't keep process alive just for commit watching
|
|
59
|
-
this.timer.unref();
|
|
60
|
-
}
|
|
61
|
-
/**
|
|
62
|
-
* Stop polling.
|
|
63
|
-
*/
|
|
64
|
-
stop() {
|
|
65
|
-
this.running = false;
|
|
66
|
-
if (this.timer) {
|
|
67
|
-
clearInterval(this.timer);
|
|
68
|
-
this.timer = null;
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
/**
|
|
72
|
-
* Get the last known HEAD SHA (for testing/status).
|
|
73
|
-
*/
|
|
74
|
-
getLastHeadSha() {
|
|
75
|
-
return this.lastHeadSha;
|
|
76
|
-
}
|
|
77
|
-
/**
|
|
78
|
-
* Force a poll cycle (useful for testing).
|
|
79
|
-
*/
|
|
80
|
-
async poll() {
|
|
81
|
-
try {
|
|
82
|
-
const currentHead = await getHeadSha(this.cwd);
|
|
83
|
-
if (!currentHead)
|
|
84
|
-
return;
|
|
85
|
-
if (this.lastHeadSha && currentHead !== this.lastHeadSha) {
|
|
86
|
-
const files = await getChangedFiles(this.cwd, this.lastHeadSha, currentHead);
|
|
87
|
-
const associated = this.correlator.associateCommit(currentHead, files);
|
|
88
|
-
if (associated > 0) {
|
|
89
|
-
log.info(`Commit ${currentHead.slice(0, 8)}: associated ${associated} pending correlation(s)`);
|
|
90
|
-
const committed = this.correlator
|
|
91
|
-
.getCommittedUnflushed()
|
|
92
|
-
.filter((c) => c.commitSha === currentHead);
|
|
93
|
-
if (committed.length > 0 && this.sessionId) {
|
|
94
|
-
const drift = this.driftSummaryFn
|
|
95
|
-
? await this.driftSummaryFn()
|
|
96
|
-
: {
|
|
97
|
-
added: 0,
|
|
98
|
-
modified: 0,
|
|
99
|
-
deleted: 0,
|
|
100
|
-
};
|
|
101
|
-
encodeIntentAsNote(currentHead, committed, this.sessionId, this.branchContext, drift, this.cwd).catch(() => {
|
|
102
|
-
/* fire-and-forget */
|
|
103
|
-
});
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
this.onCommit?.(currentHead, files, associated);
|
|
107
|
-
}
|
|
108
|
-
this.lastHeadSha = currentHead;
|
|
109
|
-
}
|
|
110
|
-
catch {
|
|
111
|
-
// Git errors are non-fatal — skip this cycle
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
}
|
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Cross-Session Context Dedup Ledger — persists delivered _context keys
|
|
3
|
-
* across sessions so repeated information isn't re-delivered.
|
|
4
|
-
*
|
|
5
|
-
* Stored in .unerr/state/context-ledger.json.
|
|
6
|
-
* On session start, loads previous deliveries into the in-session dedup (C.4).
|
|
7
|
-
*
|
|
8
|
-
* Temporal intelligence note: this implements the warm tier of the
|
|
9
|
-
* three-tier memory architecture (Section 13.8). Keys older than
|
|
10
|
-
* TTL_DAYS are evicted to prevent unbounded growth.
|
|
11
|
-
*/
|
|
12
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
13
|
-
import { join } from "node:path";
|
|
14
|
-
const TTL_DAYS = 7;
|
|
15
|
-
const MAX_ENTRIES = 50_000;
|
|
16
|
-
export function createContextLedger(unerrDir) {
|
|
17
|
-
const stateDir = join(unerrDir, "state");
|
|
18
|
-
const filePath = join(stateDir, "context-ledger.json");
|
|
19
|
-
let records = [];
|
|
20
|
-
let index = new Map();
|
|
21
|
-
function ensureDir() {
|
|
22
|
-
if (!existsSync(stateDir)) {
|
|
23
|
-
mkdirSync(stateDir, { recursive: true });
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
function compoundKey(entityKey, contextKey) {
|
|
27
|
-
return `${entityKey}::${contextKey}`;
|
|
28
|
-
}
|
|
29
|
-
function load() {
|
|
30
|
-
ensureDir();
|
|
31
|
-
if (!existsSync(filePath)) {
|
|
32
|
-
records = [];
|
|
33
|
-
index = new Map();
|
|
34
|
-
return new Map();
|
|
35
|
-
}
|
|
36
|
-
try {
|
|
37
|
-
const raw = readFileSync(filePath, "utf-8");
|
|
38
|
-
const parsed = JSON.parse(raw);
|
|
39
|
-
records = Array.isArray(parsed) ? parsed : [];
|
|
40
|
-
}
|
|
41
|
-
catch {
|
|
42
|
-
records = [];
|
|
43
|
-
}
|
|
44
|
-
index = new Map();
|
|
45
|
-
for (const r of records) {
|
|
46
|
-
let set = index.get(r.entityKey);
|
|
47
|
-
if (!set) {
|
|
48
|
-
set = new Set();
|
|
49
|
-
index.set(r.entityKey, set);
|
|
50
|
-
}
|
|
51
|
-
set.add(r.contextKey);
|
|
52
|
-
}
|
|
53
|
-
return new Map([...index.entries()].map(([k, v]) => [k, new Set(v)]));
|
|
54
|
-
}
|
|
55
|
-
function save(delivered) {
|
|
56
|
-
ensureDir();
|
|
57
|
-
const newRecords = [];
|
|
58
|
-
const now = new Date().toISOString();
|
|
59
|
-
for (const [entityKey, contextKeys] of delivered) {
|
|
60
|
-
for (const contextKey of contextKeys) {
|
|
61
|
-
const existing = records.find((r) => r.entityKey === entityKey && r.contextKey === contextKey);
|
|
62
|
-
newRecords.push({
|
|
63
|
-
entityKey,
|
|
64
|
-
contextKey,
|
|
65
|
-
deliveredAt: existing?.deliveredAt ?? now,
|
|
66
|
-
});
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
records = newRecords;
|
|
70
|
-
index = delivered;
|
|
71
|
-
try {
|
|
72
|
-
writeFileSync(filePath, JSON.stringify(records, null, 2), "utf-8");
|
|
73
|
-
}
|
|
74
|
-
catch {
|
|
75
|
-
/* best effort */
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
function hasDelivered(entityKey, contextKey) {
|
|
79
|
-
const set = index.get(entityKey);
|
|
80
|
-
return set?.has(contextKey) ?? false;
|
|
81
|
-
}
|
|
82
|
-
function markDelivered(entityKey, contextKeys) {
|
|
83
|
-
let set = index.get(entityKey);
|
|
84
|
-
if (!set) {
|
|
85
|
-
set = new Set();
|
|
86
|
-
index.set(entityKey, set);
|
|
87
|
-
}
|
|
88
|
-
const now = new Date().toISOString();
|
|
89
|
-
for (const ck of contextKeys) {
|
|
90
|
-
if (!set.has(ck)) {
|
|
91
|
-
set.add(ck);
|
|
92
|
-
records.push({ entityKey, contextKey: ck, deliveredAt: now });
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
function getDeliveredCount() {
|
|
97
|
-
return records.length;
|
|
98
|
-
}
|
|
99
|
-
function prune() {
|
|
100
|
-
const cutoff = new Date(Date.now() - TTL_DAYS * 24 * 60 * 60 * 1000).toISOString();
|
|
101
|
-
const before = records.length;
|
|
102
|
-
records = records.filter((r) => r.deliveredAt >= cutoff);
|
|
103
|
-
if (records.length > MAX_ENTRIES) {
|
|
104
|
-
records = records.slice(-MAX_ENTRIES);
|
|
105
|
-
}
|
|
106
|
-
index = new Map();
|
|
107
|
-
for (const r of records) {
|
|
108
|
-
let set = index.get(r.entityKey);
|
|
109
|
-
if (!set) {
|
|
110
|
-
set = new Set();
|
|
111
|
-
index.set(r.entityKey, set);
|
|
112
|
-
}
|
|
113
|
-
set.add(r.contextKey);
|
|
114
|
-
}
|
|
115
|
-
return before - records.length;
|
|
116
|
-
}
|
|
117
|
-
load();
|
|
118
|
-
return { load, save, hasDelivered, markDelivered, getDeliveredCount, prune };
|
|
119
|
-
}
|