@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,874 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Drift Tracker Engine — detects workspace drift between local files and graph.
|
|
3
|
-
*
|
|
4
|
-
* Processing pipeline (per changed file):
|
|
5
|
-
* 1. Content SHA → skip if unchanged (via FileHashManager)
|
|
6
|
-
* 2. Extract entities via AST extractor (regex-based, fast)
|
|
7
|
-
* 3. Diff against CozoDB base entities for same file_path
|
|
8
|
-
* 4. Upsert drift_overlay: added / modified / deleted
|
|
9
|
-
* 5. Update file hash state
|
|
10
|
-
*
|
|
11
|
-
* Runs within the proxy loop, triggered by file watcher or on-demand.
|
|
12
|
-
* All logging to stderr. Never touches stdout.
|
|
13
|
-
*/
|
|
14
|
-
import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync, } from "node:fs";
|
|
15
|
-
import { join } from "node:path";
|
|
16
|
-
import { detectLanguage, entityKey, extractEntitiesAsync, } from "../intelligence/ast-extractor.js";
|
|
17
|
-
import { BranchSnapshotManager } from "./branch-snapshot.js";
|
|
18
|
-
import { contentSha256 } from "./file-hash-state.js";
|
|
19
|
-
import { StashManager } from "./stash-manager.js";
|
|
20
|
-
/** Thresholds for AI attribution heuristic (ms). */
|
|
21
|
-
const AI_THRESHOLD_MS = 10_000; // <10s after sync = AI
|
|
22
|
-
const MIXED_THRESHOLD_MS = 60_000; // 10-60s = mixed, >60s = human
|
|
23
|
-
/**
|
|
24
|
-
* Determine change origin based on time since last sync_local_diff.
|
|
25
|
-
* - <10s after sync → "ai" (agent just wrote code)
|
|
26
|
-
* - 10-60s → "mixed" (ambiguous, agent wrote + human may have edited)
|
|
27
|
-
* - >60s → "human" (no recent agent activity)
|
|
28
|
-
*/
|
|
29
|
-
export function determineOrigin(lastSyncTimestamp) {
|
|
30
|
-
if (lastSyncTimestamp === 0)
|
|
31
|
-
return "human";
|
|
32
|
-
const elapsed = Date.now() - lastSyncTimestamp;
|
|
33
|
-
if (elapsed < AI_THRESHOLD_MS)
|
|
34
|
-
return "ai";
|
|
35
|
-
if (elapsed < MIXED_THRESHOLD_MS)
|
|
36
|
-
return "mixed";
|
|
37
|
-
return "human";
|
|
38
|
-
}
|
|
39
|
-
/**
|
|
40
|
-
* Chokidar `awaitWriteFinish` config for IDE auto-save noise filtering.
|
|
41
|
-
* Use when setting up a file watcher that feeds into DriftTracker.
|
|
42
|
-
*
|
|
43
|
-
* stabilityThreshold: wait 300ms after last write before emitting 'change'
|
|
44
|
-
* pollInterval: check every 100ms during the stability window
|
|
45
|
-
*/
|
|
46
|
-
export const DRIFT_WATCHER_OPTIONS = {
|
|
47
|
-
awaitWriteFinish: {
|
|
48
|
-
stabilityThreshold: 300,
|
|
49
|
-
pollInterval: 100,
|
|
50
|
-
},
|
|
51
|
-
};
|
|
52
|
-
/**
|
|
53
|
-
* In-memory mtime cache for fast rejection of unchanged files.
|
|
54
|
-
* Avoids reading file content + computing SHA-256 when mtime hasn't changed.
|
|
55
|
-
*/
|
|
56
|
-
export class MtimeCache {
|
|
57
|
-
cache = new Map();
|
|
58
|
-
/**
|
|
59
|
-
* Returns true if the file's mtime has changed (or is new).
|
|
60
|
-
* Updates the cache entry on change.
|
|
61
|
-
*/
|
|
62
|
-
check(filePath) {
|
|
63
|
-
try {
|
|
64
|
-
const stat = statSync(filePath);
|
|
65
|
-
const lastMtime = this.cache.get(filePath);
|
|
66
|
-
if (lastMtime !== undefined && stat.mtimeMs === lastMtime) {
|
|
67
|
-
return false; // unchanged
|
|
68
|
-
}
|
|
69
|
-
this.cache.set(filePath, stat.mtimeMs);
|
|
70
|
-
return true; // changed or new
|
|
71
|
-
}
|
|
72
|
-
catch {
|
|
73
|
-
// File doesn't exist or stat failed — evict from cache, let caller handle
|
|
74
|
-
this.cache.delete(filePath);
|
|
75
|
-
return true;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
/** Remove a file from the cache (e.g., on delete). */
|
|
79
|
-
evict(filePath) {
|
|
80
|
-
this.cache.delete(filePath);
|
|
81
|
-
}
|
|
82
|
-
/** Clear entire cache (e.g., on branch switch). */
|
|
83
|
-
clear() {
|
|
84
|
-
this.cache.clear();
|
|
85
|
-
}
|
|
86
|
-
/** Number of cached entries. */
|
|
87
|
-
get size() {
|
|
88
|
-
return this.cache.size;
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
/** stderr logger */
|
|
92
|
-
const _log = {
|
|
93
|
-
info: (msg) => process.stderr.write(`[unerr:drift] ${msg}\n`),
|
|
94
|
-
warn: (msg) => process.stderr.write(`[unerr:drift] WARN: ${msg}\n`),
|
|
95
|
-
};
|
|
96
|
-
export class DriftTracker {
|
|
97
|
-
config;
|
|
98
|
-
localGraph;
|
|
99
|
-
fileHashManager;
|
|
100
|
-
mtimeCache = new MtimeCache();
|
|
101
|
-
/** Timestamp of last sync_local_diff, set by proxy for attribution heuristic. */
|
|
102
|
-
_lastSyncTimestamp = 0;
|
|
103
|
-
/** Optional rule evaluator for push-based violation detection (Task 7.3). */
|
|
104
|
-
ruleEvaluator = null;
|
|
105
|
-
/** Optional violation store — shared with QueryRouter (Task 7.3). */
|
|
106
|
-
violationStore = null;
|
|
107
|
-
/** Local Mode incremental re-index hook (L2.5). Kept but disabled — full reindex preferred. */
|
|
108
|
-
localReindexFn = null;
|
|
109
|
-
/** File change notification callback — wired to GraphHolder.notifyFileChange(). */
|
|
110
|
-
fileChangeNotifier = null;
|
|
111
|
-
/** Stash manager for save/restore on git stash/pop (Task 7.1). */
|
|
112
|
-
stashManager = null;
|
|
113
|
-
/** Branch snapshot manager for save/restore on branch switch (Task 6.2). */
|
|
114
|
-
branchSnapshotManager = null;
|
|
115
|
-
/** Layer 7: Optional sink for dashboard SSE (same-process, no IPC). */
|
|
116
|
-
driftSink = null;
|
|
117
|
-
constructor(config, localGraph, fileHashManager) {
|
|
118
|
-
this.config = config;
|
|
119
|
-
this.localGraph = localGraph;
|
|
120
|
-
this.fileHashManager = fileHashManager;
|
|
121
|
-
}
|
|
122
|
-
/**
|
|
123
|
-
* Enable push-based rule enforcement (Task 7.3).
|
|
124
|
-
* When set, file changes trigger automatic rule evaluation.
|
|
125
|
-
*/
|
|
126
|
-
setRuleEnforcement(evaluator, store) {
|
|
127
|
-
this.ruleEvaluator = evaluator;
|
|
128
|
-
this.violationStore = store;
|
|
129
|
-
}
|
|
130
|
-
/**
|
|
131
|
-
* Enable Local Mode incremental re-indexing (L2.5).
|
|
132
|
-
* DISABLED — kept for reference. Full reindex is used instead (8s for 450 files).
|
|
133
|
-
*/
|
|
134
|
-
setLocalReindex(reindexFn) {
|
|
135
|
-
this.localReindexFn = reindexFn;
|
|
136
|
-
}
|
|
137
|
-
/**
|
|
138
|
-
* Wire the file change notifier (from GraphHolder.notifyFileChange).
|
|
139
|
-
* Called on every drift-processed file change to signal the GraphHolder's idle timer.
|
|
140
|
-
*/
|
|
141
|
-
setFileChangeNotifier(notifier) {
|
|
142
|
-
this.fileChangeNotifier = notifier;
|
|
143
|
-
}
|
|
144
|
-
/**
|
|
145
|
-
* Swap the graph reference atomically (called by GraphHolder on rebuild completion).
|
|
146
|
-
* All subsequent drift detection will use the new graph instance.
|
|
147
|
-
*/
|
|
148
|
-
swapGraph(newGraph) {
|
|
149
|
-
this.localGraph = newGraph;
|
|
150
|
-
}
|
|
151
|
-
/** Update the last sync timestamp (called by proxy on sync_local_diff). */
|
|
152
|
-
setLastSyncTimestamp(ts) {
|
|
153
|
-
this._lastSyncTimestamp = ts;
|
|
154
|
-
}
|
|
155
|
-
/**
|
|
156
|
-
* Layer 7: Wire dashboard event bus — emits when drift counts change for a file.
|
|
157
|
-
*/
|
|
158
|
-
setDriftEventSink(sink) {
|
|
159
|
-
this.driftSink = sink;
|
|
160
|
-
}
|
|
161
|
-
maybeEmitDrift(relPath, result) {
|
|
162
|
-
if (!this.driftSink)
|
|
163
|
-
return;
|
|
164
|
-
const activity = result.entitiesAdded +
|
|
165
|
-
result.entitiesModified +
|
|
166
|
-
result.entitiesDeleted +
|
|
167
|
-
result.crossFileInvalidated +
|
|
168
|
-
result.edgesExtracted;
|
|
169
|
-
if (activity === 0)
|
|
170
|
-
return;
|
|
171
|
-
this.driftSink({ file: relPath, ...result });
|
|
172
|
-
}
|
|
173
|
-
/**
|
|
174
|
-
* Process a single file for drift detection.
|
|
175
|
-
* Returns the drift result for this file.
|
|
176
|
-
*/
|
|
177
|
-
async processFile(filePath, headSha, intentId) {
|
|
178
|
-
const result = {
|
|
179
|
-
filesProcessed: 0,
|
|
180
|
-
filesSkipped: 0,
|
|
181
|
-
entitiesAdded: 0,
|
|
182
|
-
entitiesModified: 0,
|
|
183
|
-
entitiesDeleted: 0,
|
|
184
|
-
crossFileInvalidated: 0,
|
|
185
|
-
edgesExtracted: 0,
|
|
186
|
-
};
|
|
187
|
-
// Resolve absolute path
|
|
188
|
-
const absPath = filePath.startsWith("/")
|
|
189
|
-
? filePath
|
|
190
|
-
: join(this.config.projectRoot, filePath);
|
|
191
|
-
// Relative path for entity storage (from project root)
|
|
192
|
-
const relPath = filePath.startsWith("/")
|
|
193
|
-
? filePath.slice(this.config.projectRoot.length + 1)
|
|
194
|
-
: filePath;
|
|
195
|
-
// Check if language is supported
|
|
196
|
-
const language = detectLanguage(relPath);
|
|
197
|
-
if (!language)
|
|
198
|
-
return result;
|
|
199
|
-
// Fast mtime rejection — avoid reading content + SHA if mtime unchanged
|
|
200
|
-
if (existsSync(absPath) && !this.mtimeCache.check(absPath)) {
|
|
201
|
-
result.filesSkipped = 1;
|
|
202
|
-
return result;
|
|
203
|
-
}
|
|
204
|
-
// Check if file exists
|
|
205
|
-
if (!existsSync(absPath)) {
|
|
206
|
-
// File was deleted — mark all entities from this file as deleted
|
|
207
|
-
const baseEntities = await this.localGraph.getEntitiesByFile(relPath);
|
|
208
|
-
this.markFileDeleted(relPath, intentId);
|
|
209
|
-
result.filesProcessed = 1;
|
|
210
|
-
result.entitiesDeleted = baseEntities.length;
|
|
211
|
-
// Cross-file invalidation: callers of deleted entities need notification
|
|
212
|
-
const now = new Date().toISOString();
|
|
213
|
-
const origin = determineOrigin(this._lastSyncTimestamp);
|
|
214
|
-
for (const entity of baseEntities) {
|
|
215
|
-
const callers = await this.localGraph.getCallersOf(entity.key);
|
|
216
|
-
for (const caller of callers) {
|
|
217
|
-
if (caller.file_path === relPath)
|
|
218
|
-
continue;
|
|
219
|
-
const existing = (await this.localGraph.getDriftEntitiesForFile(caller.file_path)).find((e) => e.key === caller.key);
|
|
220
|
-
if (existing && existing.drift_status !== "dependency_changed")
|
|
221
|
-
continue;
|
|
222
|
-
const drift = {
|
|
223
|
-
key: caller.key,
|
|
224
|
-
name: caller.name,
|
|
225
|
-
kind: caller.kind,
|
|
226
|
-
signature: caller.signature ?? "",
|
|
227
|
-
body: caller.body ?? "",
|
|
228
|
-
file_path: caller.file_path,
|
|
229
|
-
line_start: caller.start_line ?? 0,
|
|
230
|
-
line_end: caller.start_line ?? 0,
|
|
231
|
-
content_hash: "",
|
|
232
|
-
drift_status: "dependency_changed",
|
|
233
|
-
intent_id: intentId ?? "",
|
|
234
|
-
modified_at: now,
|
|
235
|
-
origin,
|
|
236
|
-
previous_body: "",
|
|
237
|
-
previous_signature: "",
|
|
238
|
-
};
|
|
239
|
-
await this.localGraph.upsertDriftEntity(drift);
|
|
240
|
-
result.crossFileInvalidated++;
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
this.maybeEmitDrift(relPath, result);
|
|
244
|
-
return result;
|
|
245
|
-
}
|
|
246
|
-
// Read content and compute hash
|
|
247
|
-
const content = readFileSync(absPath, "utf-8");
|
|
248
|
-
const sha = contentSha256(content);
|
|
249
|
-
// Skip if unchanged
|
|
250
|
-
const decision = this.fileHashManager.shouldProcess(relPath, sha, headSha);
|
|
251
|
-
if (decision === "skip") {
|
|
252
|
-
result.filesSkipped = 1;
|
|
253
|
-
return result;
|
|
254
|
-
}
|
|
255
|
-
result.filesProcessed = 1;
|
|
256
|
-
// Extract entities from local file (tree-sitter WASM with regex fallback)
|
|
257
|
-
const localEntities = await extractEntitiesAsync(content, relPath);
|
|
258
|
-
// Get base entities from CozoDB for this file
|
|
259
|
-
const baseEntities = await this.localGraph.getEntitiesByFile(relPath);
|
|
260
|
-
// Build lookup maps
|
|
261
|
-
const localByKey = new Map();
|
|
262
|
-
for (const entity of localEntities) {
|
|
263
|
-
const key = entityKey(this.config.repoId, relPath, entity.kind, entity.name, entity.signature);
|
|
264
|
-
localByKey.set(key, entity);
|
|
265
|
-
}
|
|
266
|
-
const baseByKey = new Map();
|
|
267
|
-
for (const entity of baseEntities) {
|
|
268
|
-
baseByKey.set(entity.key, entity);
|
|
269
|
-
}
|
|
270
|
-
const now = new Date().toISOString();
|
|
271
|
-
const origin = determineOrigin(this._lastSyncTimestamp);
|
|
272
|
-
// Find added and modified entities
|
|
273
|
-
for (const [key, local] of localByKey) {
|
|
274
|
-
const base = baseByKey.get(key);
|
|
275
|
-
if (!base) {
|
|
276
|
-
// New entity — not in base graph
|
|
277
|
-
const drift = {
|
|
278
|
-
key,
|
|
279
|
-
name: local.name,
|
|
280
|
-
kind: local.kind,
|
|
281
|
-
signature: local.signature,
|
|
282
|
-
body: extractBodyLines(content, local.line_start, local.line_end),
|
|
283
|
-
file_path: relPath,
|
|
284
|
-
line_start: local.line_start,
|
|
285
|
-
line_end: local.line_end,
|
|
286
|
-
content_hash: local.content_hash,
|
|
287
|
-
drift_status: "added",
|
|
288
|
-
intent_id: intentId ?? "",
|
|
289
|
-
modified_at: now,
|
|
290
|
-
origin,
|
|
291
|
-
previous_body: "",
|
|
292
|
-
previous_signature: "",
|
|
293
|
-
};
|
|
294
|
-
await this.localGraph.upsertDriftEntity(drift);
|
|
295
|
-
result.entitiesAdded++;
|
|
296
|
-
}
|
|
297
|
-
else if (local.content_hash !== contentSha256(base.body || "").slice(0, 16)) {
|
|
298
|
-
// Content changed — mark as modified (preserve previous body for rewind)
|
|
299
|
-
const drift = {
|
|
300
|
-
key,
|
|
301
|
-
name: local.name,
|
|
302
|
-
kind: local.kind,
|
|
303
|
-
signature: local.signature,
|
|
304
|
-
body: extractBodyLines(content, local.line_start, local.line_end),
|
|
305
|
-
file_path: relPath,
|
|
306
|
-
line_start: local.line_start,
|
|
307
|
-
line_end: local.line_end,
|
|
308
|
-
content_hash: local.content_hash,
|
|
309
|
-
drift_status: "modified",
|
|
310
|
-
intent_id: intentId ?? "",
|
|
311
|
-
modified_at: now,
|
|
312
|
-
origin,
|
|
313
|
-
previous_body: base.body || "",
|
|
314
|
-
previous_signature: base.signature || "",
|
|
315
|
-
};
|
|
316
|
-
await this.localGraph.upsertDriftEntity(drift);
|
|
317
|
-
result.entitiesModified++;
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
// Find deleted entities (in base but not in local)
|
|
321
|
-
for (const [key, base] of baseByKey) {
|
|
322
|
-
if (!localByKey.has(key)) {
|
|
323
|
-
const drift = {
|
|
324
|
-
key,
|
|
325
|
-
name: base.name,
|
|
326
|
-
kind: base.kind,
|
|
327
|
-
signature: base.signature,
|
|
328
|
-
body: "",
|
|
329
|
-
file_path: relPath,
|
|
330
|
-
line_start: base.start_line,
|
|
331
|
-
line_end: base.start_line,
|
|
332
|
-
content_hash: "",
|
|
333
|
-
drift_status: "deleted",
|
|
334
|
-
intent_id: intentId ?? "",
|
|
335
|
-
modified_at: now,
|
|
336
|
-
origin,
|
|
337
|
-
previous_body: base.body || "",
|
|
338
|
-
previous_signature: base.signature || "",
|
|
339
|
-
};
|
|
340
|
-
await this.localGraph.upsertDriftEntity(drift);
|
|
341
|
-
result.entitiesDeleted++;
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
// Cross-file drift invalidation: notify callers of modified/deleted entities
|
|
345
|
-
result.crossFileInvalidated = await this.invalidateCrossFileCallers(localByKey, baseByKey, relPath, now, origin, intentId);
|
|
346
|
-
// Task 6.4: Extract drift edges (imports + function calls)
|
|
347
|
-
result.edgesExtracted = await this.extractDriftEdges(content, relPath, localByKey, now);
|
|
348
|
-
// Notify GraphHolder of file change — resets idle timer for swap-on-idle rebuild.
|
|
349
|
-
// GraphHolder handles debouncing + full reindex + atomic graph swap.
|
|
350
|
-
this.fileChangeNotifier?.();
|
|
351
|
-
// Task 7.3: Push-based rule enforcement — evaluate rules on changed file
|
|
352
|
-
if (this.ruleEvaluator &&
|
|
353
|
-
this.violationStore &&
|
|
354
|
-
(await this.localGraph.hasRules())) {
|
|
355
|
-
this.runRuleCheck(relPath, content);
|
|
356
|
-
}
|
|
357
|
-
// Update file hash state
|
|
358
|
-
this.fileHashManager.markProcessed(relPath, sha, headSha);
|
|
359
|
-
this.maybeEmitDrift(relPath, result);
|
|
360
|
-
return result;
|
|
361
|
-
}
|
|
362
|
-
/**
|
|
363
|
-
* Process multiple files for drift detection (batch).
|
|
364
|
-
*/
|
|
365
|
-
async processFiles(filePaths, headSha, intentId) {
|
|
366
|
-
const aggregate = {
|
|
367
|
-
filesProcessed: 0,
|
|
368
|
-
filesSkipped: 0,
|
|
369
|
-
entitiesAdded: 0,
|
|
370
|
-
entitiesModified: 0,
|
|
371
|
-
entitiesDeleted: 0,
|
|
372
|
-
crossFileInvalidated: 0,
|
|
373
|
-
edgesExtracted: 0,
|
|
374
|
-
};
|
|
375
|
-
for (const filePath of filePaths) {
|
|
376
|
-
const result = await this.processFile(filePath, headSha, intentId);
|
|
377
|
-
aggregate.filesProcessed += result.filesProcessed;
|
|
378
|
-
aggregate.filesSkipped += result.filesSkipped;
|
|
379
|
-
aggregate.entitiesAdded += result.entitiesAdded;
|
|
380
|
-
aggregate.entitiesModified += result.entitiesModified;
|
|
381
|
-
aggregate.entitiesDeleted += result.entitiesDeleted;
|
|
382
|
-
aggregate.crossFileInvalidated += result.crossFileInvalidated;
|
|
383
|
-
aggregate.edgesExtracted += result.edgesExtracted;
|
|
384
|
-
}
|
|
385
|
-
// Persist file hash state after batch
|
|
386
|
-
this.fileHashManager.save();
|
|
387
|
-
// Update drift summary on disk
|
|
388
|
-
await this.saveDriftSummary();
|
|
389
|
-
return aggregate;
|
|
390
|
-
}
|
|
391
|
-
/**
|
|
392
|
-
* Initialize branch snapshot manager (Task 6.2).
|
|
393
|
-
* Returns the manager for external use (e.g., GC on startup).
|
|
394
|
-
*/
|
|
395
|
-
initBranchSnapshots() {
|
|
396
|
-
this.branchSnapshotManager = new BranchSnapshotManager(this.config.unerrDir, this.config.projectRoot);
|
|
397
|
-
return this.branchSnapshotManager;
|
|
398
|
-
}
|
|
399
|
-
/**
|
|
400
|
-
* Handle branch switch: save outgoing branch overlay, restore incoming.
|
|
401
|
-
*
|
|
402
|
-
* With BranchSnapshotManager (Task 6.2):
|
|
403
|
-
* 1. Save current overlay + file hashes for outgoing branch
|
|
404
|
-
* 2. Clear overlay + hashes + mtime cache
|
|
405
|
-
* 3. Attempt restore from snapshot (fast, <10ms)
|
|
406
|
-
* 4. If no snapshot (first visit), recompute from scratch
|
|
407
|
-
*
|
|
408
|
-
* Without BranchSnapshotManager: falls back to clear + recompute.
|
|
409
|
-
*/
|
|
410
|
-
async onBranchSwitch(changedFiles, headSha, fromBranch, toBranch) {
|
|
411
|
-
// Save outgoing branch snapshot
|
|
412
|
-
if (this.branchSnapshotManager && fromBranch) {
|
|
413
|
-
const fileHashState = this.fileHashManager.getState();
|
|
414
|
-
await this.branchSnapshotManager.saveSnapshot(fromBranch, this.localGraph, {
|
|
415
|
-
...fileHashState,
|
|
416
|
-
});
|
|
417
|
-
}
|
|
418
|
-
// Clear current state
|
|
419
|
-
await this.localGraph.clearDriftOverlay();
|
|
420
|
-
this.fileHashManager.clearAll();
|
|
421
|
-
this.mtimeCache.clear();
|
|
422
|
-
// Attempt restore from snapshot
|
|
423
|
-
if (this.branchSnapshotManager && toBranch) {
|
|
424
|
-
const snapshot = await this.branchSnapshotManager.restoreSnapshot(toBranch, this.localGraph);
|
|
425
|
-
if (snapshot) {
|
|
426
|
-
// Restored from snapshot — skip recompute
|
|
427
|
-
// Restore file hash state so subsequent processFile() calls
|
|
428
|
-
// correctly skip unchanged files
|
|
429
|
-
this.fileHashManager.restoreState(snapshot.fileHashes);
|
|
430
|
-
return {
|
|
431
|
-
filesProcessed: 0,
|
|
432
|
-
filesSkipped: 0,
|
|
433
|
-
entitiesAdded: snapshot.entities.length,
|
|
434
|
-
entitiesModified: 0,
|
|
435
|
-
entitiesDeleted: 0,
|
|
436
|
-
crossFileInvalidated: 0,
|
|
437
|
-
edgesExtracted: snapshot.edges?.length ?? 0,
|
|
438
|
-
};
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
// No snapshot — first visit, recompute from scratch
|
|
442
|
-
return this.processFiles(changedFiles, headSha);
|
|
443
|
-
}
|
|
444
|
-
/**
|
|
445
|
-
* Get the current drift summary from CozoDB.
|
|
446
|
-
*/
|
|
447
|
-
async getDriftSummary() {
|
|
448
|
-
return await this.localGraph.getDriftSummary();
|
|
449
|
-
}
|
|
450
|
-
/**
|
|
451
|
-
* Initialize stash awareness (Task 7.1).
|
|
452
|
-
* Returns the StashManager for use in polling.
|
|
453
|
-
*/
|
|
454
|
-
initStashManager() {
|
|
455
|
-
this.stashManager = new StashManager(this.config.unerrDir, this.config.projectRoot);
|
|
456
|
-
return this.stashManager;
|
|
457
|
-
}
|
|
458
|
-
/**
|
|
459
|
-
* Handle git stash push: save current overlay + file hashes to snapshot.
|
|
460
|
-
*/
|
|
461
|
-
async onStashSave() {
|
|
462
|
-
if (!this.stashManager)
|
|
463
|
-
return null;
|
|
464
|
-
const fileHashState = this.fileHashManager.getState();
|
|
465
|
-
return await this.stashManager.saveSnapshot(this.localGraph, {
|
|
466
|
-
...fileHashState,
|
|
467
|
-
});
|
|
468
|
-
}
|
|
469
|
-
/**
|
|
470
|
-
* Handle git stash pop: restore overlay from most recent snapshot.
|
|
471
|
-
*/
|
|
472
|
-
async onStashPop() {
|
|
473
|
-
if (!this.stashManager)
|
|
474
|
-
return 0;
|
|
475
|
-
return await this.stashManager.restoreSnapshot(this.localGraph);
|
|
476
|
-
}
|
|
477
|
-
async markFileDeleted(filePath, intentId) {
|
|
478
|
-
// Evict from mtime cache — file no longer exists
|
|
479
|
-
const absPath = filePath.startsWith("/")
|
|
480
|
-
? filePath
|
|
481
|
-
: join(this.config.projectRoot, filePath);
|
|
482
|
-
this.mtimeCache.evict(absPath);
|
|
483
|
-
const baseEntities = await this.localGraph.getEntitiesByFile(filePath);
|
|
484
|
-
const now = new Date().toISOString();
|
|
485
|
-
const origin = determineOrigin(this._lastSyncTimestamp);
|
|
486
|
-
for (const entity of baseEntities) {
|
|
487
|
-
const drift = {
|
|
488
|
-
key: entity.key,
|
|
489
|
-
name: entity.name,
|
|
490
|
-
kind: entity.kind,
|
|
491
|
-
signature: entity.signature,
|
|
492
|
-
body: "",
|
|
493
|
-
file_path: filePath,
|
|
494
|
-
line_start: entity.start_line,
|
|
495
|
-
line_end: entity.start_line,
|
|
496
|
-
content_hash: "",
|
|
497
|
-
drift_status: "deleted",
|
|
498
|
-
intent_id: intentId ?? "",
|
|
499
|
-
modified_at: now,
|
|
500
|
-
origin,
|
|
501
|
-
previous_body: entity.body || "",
|
|
502
|
-
previous_signature: entity.signature || "",
|
|
503
|
-
};
|
|
504
|
-
await this.localGraph.upsertDriftEntity(drift);
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
/**
|
|
508
|
-
* For modified/deleted entities in this file, find callers in OTHER files
|
|
509
|
-
* and mark them as "dependency_changed" in the drift overlay.
|
|
510
|
-
*/
|
|
511
|
-
async invalidateCrossFileCallers(localByKey, baseByKey, filePath, now, origin, intentId) {
|
|
512
|
-
// Collect keys of entities that were modified or deleted
|
|
513
|
-
const changedKeys = [];
|
|
514
|
-
for (const [key, local] of localByKey) {
|
|
515
|
-
const base = baseByKey.get(key);
|
|
516
|
-
if (!base) {
|
|
517
|
-
// Added entity — no callers to invalidate yet
|
|
518
|
-
continue;
|
|
519
|
-
}
|
|
520
|
-
if (local.content_hash !== contentSha256(base.body || "").slice(0, 16)) {
|
|
521
|
-
changedKeys.push(key);
|
|
522
|
-
}
|
|
523
|
-
}
|
|
524
|
-
for (const [key] of baseByKey) {
|
|
525
|
-
if (!localByKey.has(key)) {
|
|
526
|
-
changedKeys.push(key); // deleted
|
|
527
|
-
}
|
|
528
|
-
}
|
|
529
|
-
if (changedKeys.length === 0)
|
|
530
|
-
return 0;
|
|
531
|
-
let invalidated = 0;
|
|
532
|
-
for (const key of changedKeys) {
|
|
533
|
-
const callers = await this.localGraph.getCallersOf(key);
|
|
534
|
-
for (const caller of callers) {
|
|
535
|
-
// Skip callers in the same file — already handled by normal diff
|
|
536
|
-
if (caller.file_path === filePath)
|
|
537
|
-
continue;
|
|
538
|
-
// Don't overwrite a stronger drift status (added/modified/deleted)
|
|
539
|
-
const existingEntities = await this.localGraph.getDriftEntitiesForFile(caller.file_path);
|
|
540
|
-
const existing = existingEntities.find((e) => e.key === caller.key);
|
|
541
|
-
if (existing && existing.drift_status !== "dependency_changed") {
|
|
542
|
-
continue;
|
|
543
|
-
}
|
|
544
|
-
const drift = {
|
|
545
|
-
key: caller.key,
|
|
546
|
-
name: caller.name,
|
|
547
|
-
kind: caller.kind,
|
|
548
|
-
signature: caller.signature ?? "",
|
|
549
|
-
body: caller.body ?? "",
|
|
550
|
-
file_path: caller.file_path,
|
|
551
|
-
line_start: caller.start_line ?? 0,
|
|
552
|
-
line_end: caller.start_line ?? 0,
|
|
553
|
-
content_hash: "",
|
|
554
|
-
drift_status: "dependency_changed",
|
|
555
|
-
intent_id: intentId ?? "",
|
|
556
|
-
modified_at: now,
|
|
557
|
-
origin,
|
|
558
|
-
previous_body: "",
|
|
559
|
-
previous_signature: "",
|
|
560
|
-
};
|
|
561
|
-
await this.localGraph.upsertDriftEntity(drift);
|
|
562
|
-
invalidated++;
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
return invalidated;
|
|
566
|
-
}
|
|
567
|
-
async saveDriftSummary() {
|
|
568
|
-
const driftDir = join(this.config.unerrDir, "drift");
|
|
569
|
-
if (!existsSync(driftDir)) {
|
|
570
|
-
mkdirSync(driftDir, { recursive: true });
|
|
571
|
-
}
|
|
572
|
-
const summary = await this.getDriftSummary();
|
|
573
|
-
writeFileSync(join(driftDir, "drift_summary.json"), JSON.stringify(summary, null, 2), "utf-8");
|
|
574
|
-
}
|
|
575
|
-
/**
|
|
576
|
-
* Task 7.3: Run rule evaluation on a changed file and store violations.
|
|
577
|
-
* Non-blocking: fires and forgets. If evaluation takes >10ms, times out silently.
|
|
578
|
-
*/
|
|
579
|
-
/**
|
|
580
|
-
* Task 6.4: Extract import and function call edges from file content.
|
|
581
|
-
* Upserts them into drift_edges. Approximate — no full scope resolution.
|
|
582
|
-
*/
|
|
583
|
-
async extractDriftEdges(content, filePath, localByKey, now) {
|
|
584
|
-
let count = 0;
|
|
585
|
-
// Extract import edges: import { X } from './module'
|
|
586
|
-
const importEdges = extractImportEdges(content, filePath);
|
|
587
|
-
for (const imp of importEdges) {
|
|
588
|
-
// Resolve target: find entity key matching imported name in target file
|
|
589
|
-
const targetKey = await this.resolveImportTarget(imp.importedName, imp.targetPath);
|
|
590
|
-
if (!targetKey)
|
|
591
|
-
continue;
|
|
592
|
-
// Find a source entity that is the "file module" or first entity in this file
|
|
593
|
-
const sourceKey = this.resolveImportSource(filePath, localByKey);
|
|
594
|
-
if (!sourceKey)
|
|
595
|
-
continue;
|
|
596
|
-
await this.localGraph.upsertDriftEdge({
|
|
597
|
-
from_key: sourceKey,
|
|
598
|
-
to_key: targetKey,
|
|
599
|
-
type: "imports",
|
|
600
|
-
drift_status: "added",
|
|
601
|
-
modified_at: now,
|
|
602
|
-
});
|
|
603
|
-
count++;
|
|
604
|
-
}
|
|
605
|
-
// Extract function call edges within entities in this file.
|
|
606
|
-
// Only callable kinds can emit "calls" edges — variables, interfaces, types
|
|
607
|
-
// are not call sites, and scanning the full file body for them would attribute
|
|
608
|
-
// unrelated calls in the same file to non-callable entities (false positives in
|
|
609
|
-
// get_references). Class is included because class bodies can contain static
|
|
610
|
-
// initializers and field initializers that perform calls.
|
|
611
|
-
const CALLABLE_KINDS = new Set(["function", "method", "class"]);
|
|
612
|
-
for (const [callerKey, entity] of localByKey) {
|
|
613
|
-
if (!CALLABLE_KINDS.has(entity.kind))
|
|
614
|
-
continue;
|
|
615
|
-
const callEdges = extractCallEdges(content, entity.name, callerKey);
|
|
616
|
-
for (const call of callEdges) {
|
|
617
|
-
// Find target entity by name in any file
|
|
618
|
-
const targetKey = await this.resolveCallTarget(call.calledName);
|
|
619
|
-
if (!targetKey || targetKey === callerKey)
|
|
620
|
-
continue;
|
|
621
|
-
await this.localGraph.upsertDriftEdge({
|
|
622
|
-
from_key: callerKey,
|
|
623
|
-
to_key: targetKey,
|
|
624
|
-
type: "calls",
|
|
625
|
-
drift_status: "added",
|
|
626
|
-
modified_at: now,
|
|
627
|
-
});
|
|
628
|
-
count++;
|
|
629
|
-
}
|
|
630
|
-
}
|
|
631
|
-
return count;
|
|
632
|
-
}
|
|
633
|
-
/**
|
|
634
|
-
* Resolve an imported name to an entity key in the target file.
|
|
635
|
-
*/
|
|
636
|
-
async resolveImportTarget(name, targetPath) {
|
|
637
|
-
// Look in base entities first
|
|
638
|
-
const baseEntities = await this.localGraph.getEntitiesByFile(targetPath);
|
|
639
|
-
for (const entity of baseEntities) {
|
|
640
|
-
if (entity.name === name)
|
|
641
|
-
return entity.key;
|
|
642
|
-
}
|
|
643
|
-
// Check drift overlay
|
|
644
|
-
const driftEntities = await this.localGraph.getDriftEntitiesForFile(targetPath);
|
|
645
|
-
for (const entity of driftEntities) {
|
|
646
|
-
if (entity.name === name && entity.drift_status !== "deleted")
|
|
647
|
-
return entity.key;
|
|
648
|
-
}
|
|
649
|
-
return null;
|
|
650
|
-
}
|
|
651
|
-
/**
|
|
652
|
-
* Find the first entity in this file to use as import source.
|
|
653
|
-
*/
|
|
654
|
-
resolveImportSource(filePath, localByKey) {
|
|
655
|
-
// Use first entity from local extraction as the module representative
|
|
656
|
-
for (const [key] of localByKey) {
|
|
657
|
-
return key;
|
|
658
|
-
}
|
|
659
|
-
return null;
|
|
660
|
-
}
|
|
661
|
-
/**
|
|
662
|
-
* Resolve a called function name to an entity key.
|
|
663
|
-
* Searches base entities + drift overlay across all files.
|
|
664
|
-
*/
|
|
665
|
-
async resolveCallTarget(name) {
|
|
666
|
-
// Search base entities by name
|
|
667
|
-
const entity = await this.localGraph.findEntityByName(name);
|
|
668
|
-
if (entity)
|
|
669
|
-
return entity.key;
|
|
670
|
-
return null;
|
|
671
|
-
}
|
|
672
|
-
async runRuleCheck(filePath, content) {
|
|
673
|
-
if (!this.ruleEvaluator || !this.violationStore)
|
|
674
|
-
return;
|
|
675
|
-
const rules = await this.localGraph.getRules();
|
|
676
|
-
if (rules.length === 0)
|
|
677
|
-
return;
|
|
678
|
-
const evaluator = this.ruleEvaluator;
|
|
679
|
-
const store = this.violationStore;
|
|
680
|
-
// Run async rule evaluation — fire and forget, don't block drift processing
|
|
681
|
-
const t0 = performance.now();
|
|
682
|
-
evaluator(rules, filePath, content, this.localGraph)
|
|
683
|
-
.then((result) => {
|
|
684
|
-
const elapsed = performance.now() - t0;
|
|
685
|
-
if (elapsed > 10) {
|
|
686
|
-
_log.warn(`Rule evaluation for ${filePath} took ${elapsed.toFixed(1)}ms (>10ms budget)`);
|
|
687
|
-
}
|
|
688
|
-
store.addViolations(filePath, result.violations);
|
|
689
|
-
})
|
|
690
|
-
.catch(() => {
|
|
691
|
-
// Rule evaluation failed — skip silently, don't break drift processing
|
|
692
|
-
});
|
|
693
|
-
}
|
|
694
|
-
}
|
|
695
|
-
function extractBodyLines(content, lineStart, lineEnd) {
|
|
696
|
-
const lines = content.split("\n");
|
|
697
|
-
return lines.slice(lineStart - 1, lineEnd).join("\n");
|
|
698
|
-
}
|
|
699
|
-
/** Regex for ES import statements: import { X, Y } from './path' */
|
|
700
|
-
const IMPORT_REGEX = /import\s+(?:\{([^}]+)\}|(\w+))\s+from\s+['"]([^'"]+)['"]/g;
|
|
701
|
-
/**
|
|
702
|
-
* Extract import edges from file content.
|
|
703
|
-
* Resolves relative import paths to project-relative paths.
|
|
704
|
-
*/
|
|
705
|
-
function extractImportEdges(content, filePath) {
|
|
706
|
-
const edges = [];
|
|
707
|
-
// Reset lastIndex for global regex
|
|
708
|
-
IMPORT_REGEX.lastIndex = 0;
|
|
709
|
-
for (let match = IMPORT_REGEX.exec(content); match !== null; match = IMPORT_REGEX.exec(content)) {
|
|
710
|
-
const namedImports = match[1]; // { X, Y }
|
|
711
|
-
const defaultImport = match[2]; // default import
|
|
712
|
-
const importPath = match[3] ?? "";
|
|
713
|
-
// Only handle relative imports (local project files)
|
|
714
|
-
if (!importPath.startsWith("."))
|
|
715
|
-
continue;
|
|
716
|
-
// Resolve target path relative to current file
|
|
717
|
-
const targetPath = resolveImportPath(filePath, importPath);
|
|
718
|
-
if (!targetPath)
|
|
719
|
-
continue;
|
|
720
|
-
if (namedImports) {
|
|
721
|
-
// Split named imports: { X, Y as Z } → ["X", "Y"]
|
|
722
|
-
for (const name of namedImports.split(",")) {
|
|
723
|
-
const trimmed = name
|
|
724
|
-
.trim()
|
|
725
|
-
.split(/\s+as\s+/)[0]
|
|
726
|
-
?.trim();
|
|
727
|
-
if (trimmed) {
|
|
728
|
-
edges.push({ importedName: trimmed, targetPath });
|
|
729
|
-
}
|
|
730
|
-
}
|
|
731
|
-
}
|
|
732
|
-
if (defaultImport) {
|
|
733
|
-
edges.push({ importedName: defaultImport, targetPath });
|
|
734
|
-
}
|
|
735
|
-
}
|
|
736
|
-
return edges;
|
|
737
|
-
}
|
|
738
|
-
/**
|
|
739
|
-
* Resolve a relative import path to a project-relative file path.
|
|
740
|
-
* './service' from 'src/auth/handler.ts' → 'src/auth/service.ts'
|
|
741
|
-
*/
|
|
742
|
-
function resolveImportPath(fromFile, importPath) {
|
|
743
|
-
// Remove file extension from current file to get directory
|
|
744
|
-
const dir = fromFile.replace(/\/[^/]+$/, "");
|
|
745
|
-
// Normalize the import path
|
|
746
|
-
let resolved = importPath;
|
|
747
|
-
if (resolved.startsWith("./")) {
|
|
748
|
-
resolved = `${dir}/${resolved.slice(2)}`;
|
|
749
|
-
}
|
|
750
|
-
else if (resolved.startsWith("../")) {
|
|
751
|
-
const parts = dir.split("/");
|
|
752
|
-
let rel = resolved;
|
|
753
|
-
while (rel.startsWith("../")) {
|
|
754
|
-
parts.pop();
|
|
755
|
-
rel = rel.slice(3);
|
|
756
|
-
}
|
|
757
|
-
resolved = [...parts, rel].join("/");
|
|
758
|
-
}
|
|
759
|
-
// Remove .js extension (NodeNext resolution: imports use .js but files are .ts)
|
|
760
|
-
resolved = resolved.replace(/\.js$/, "");
|
|
761
|
-
// Try common extensions
|
|
762
|
-
for (const ext of [".ts", ".tsx", ".js", ".jsx"]) {
|
|
763
|
-
const candidate = resolved + ext;
|
|
764
|
-
// Return project-relative path (we don't check existence here —
|
|
765
|
-
// the caller will look up entities in that file path)
|
|
766
|
-
return candidate;
|
|
767
|
-
}
|
|
768
|
-
return null;
|
|
769
|
-
}
|
|
770
|
-
/**
|
|
771
|
-
* Extract function call edges from within an entity's body.
|
|
772
|
-
* Simple regex: matches `name(` patterns that look like function calls.
|
|
773
|
-
*/
|
|
774
|
-
function extractCallEdges(content, _entityName, callerKey) {
|
|
775
|
-
const edges = [];
|
|
776
|
-
const seen = new Set();
|
|
777
|
-
// Match function/method calls: identifier( or this.identifier( or obj.identifier(
|
|
778
|
-
const CALL_REGEX = /(?:this\.|[\w]+\.)?(\w+)\s*\(/g;
|
|
779
|
-
for (let match = CALL_REGEX.exec(content); match !== null; match = CALL_REGEX.exec(content)) {
|
|
780
|
-
const name = match[1];
|
|
781
|
-
if (!name || name.length < 2)
|
|
782
|
-
continue;
|
|
783
|
-
// Skip common built-ins and keywords
|
|
784
|
-
if (BUILTIN_NAMES.has(name))
|
|
785
|
-
continue;
|
|
786
|
-
if (seen.has(name))
|
|
787
|
-
continue;
|
|
788
|
-
seen.add(name);
|
|
789
|
-
edges.push({ callerKey, calledName: name });
|
|
790
|
-
}
|
|
791
|
-
return edges;
|
|
792
|
-
}
|
|
793
|
-
/** Names to skip during call extraction (language builtins, common patterns). */
|
|
794
|
-
const BUILTIN_NAMES = new Set([
|
|
795
|
-
"if",
|
|
796
|
-
"for",
|
|
797
|
-
"while",
|
|
798
|
-
"switch",
|
|
799
|
-
"catch",
|
|
800
|
-
"return",
|
|
801
|
-
"throw",
|
|
802
|
-
"new",
|
|
803
|
-
"typeof",
|
|
804
|
-
"instanceof",
|
|
805
|
-
"delete",
|
|
806
|
-
"void",
|
|
807
|
-
"require",
|
|
808
|
-
"import",
|
|
809
|
-
"console",
|
|
810
|
-
"log",
|
|
811
|
-
"warn",
|
|
812
|
-
"error",
|
|
813
|
-
"info",
|
|
814
|
-
"debug",
|
|
815
|
-
"parseInt",
|
|
816
|
-
"parseFloat",
|
|
817
|
-
"isNaN",
|
|
818
|
-
"isFinite",
|
|
819
|
-
"setTimeout",
|
|
820
|
-
"setInterval",
|
|
821
|
-
"clearTimeout",
|
|
822
|
-
"clearInterval",
|
|
823
|
-
"Promise",
|
|
824
|
-
"resolve",
|
|
825
|
-
"reject",
|
|
826
|
-
"then",
|
|
827
|
-
"catch",
|
|
828
|
-
"finally",
|
|
829
|
-
"Array",
|
|
830
|
-
"Object",
|
|
831
|
-
"String",
|
|
832
|
-
"Number",
|
|
833
|
-
"Boolean",
|
|
834
|
-
"Map",
|
|
835
|
-
"Set",
|
|
836
|
-
"JSON",
|
|
837
|
-
"parse",
|
|
838
|
-
"stringify",
|
|
839
|
-
"Math",
|
|
840
|
-
"Date",
|
|
841
|
-
"push",
|
|
842
|
-
"pop",
|
|
843
|
-
"shift",
|
|
844
|
-
"unshift",
|
|
845
|
-
"splice",
|
|
846
|
-
"slice",
|
|
847
|
-
"concat",
|
|
848
|
-
"map",
|
|
849
|
-
"filter",
|
|
850
|
-
"reduce",
|
|
851
|
-
"forEach",
|
|
852
|
-
"find",
|
|
853
|
-
"some",
|
|
854
|
-
"every",
|
|
855
|
-
"join",
|
|
856
|
-
"split",
|
|
857
|
-
"replace",
|
|
858
|
-
"match",
|
|
859
|
-
"test",
|
|
860
|
-
"exec",
|
|
861
|
-
"keys",
|
|
862
|
-
"values",
|
|
863
|
-
"entries",
|
|
864
|
-
"from",
|
|
865
|
-
"of",
|
|
866
|
-
"trim",
|
|
867
|
-
"includes",
|
|
868
|
-
"startsWith",
|
|
869
|
-
"endsWith",
|
|
870
|
-
"indexOf",
|
|
871
|
-
"length",
|
|
872
|
-
"toString",
|
|
873
|
-
"valueOf",
|
|
874
|
-
]);
|