@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.
Files changed (363) hide show
  1. package/README.md +36 -45
  2. package/dist/cli.js +37443 -36022
  3. package/package.json +2 -1
  4. package/dist/behaviors/agent-llm-bridge.js +0 -166
  5. package/dist/behaviors/architecture-guard.js +0 -256
  6. package/dist/behaviors/auto-doc.js +0 -247
  7. package/dist/behaviors/cascade-guard.js +0 -289
  8. package/dist/behaviors/change-narrative.js +0 -270
  9. package/dist/behaviors/convention-drift.js +0 -290
  10. package/dist/behaviors/framework.js +0 -235
  11. package/dist/behaviors/guard-formatter.js +0 -44
  12. package/dist/behaviors/incomplete-work.js +0 -270
  13. package/dist/behaviors/loop-breaker.js +0 -300
  14. package/dist/behaviors/session-continuity.js +0 -208
  15. package/dist/commands/branches.js +0 -97
  16. package/dist/commands/check-commit.js +0 -225
  17. package/dist/commands/compress-output.js +0 -64
  18. package/dist/commands/config-verify.js +0 -243
  19. package/dist/commands/daemon.js +0 -905
  20. package/dist/commands/dashboard.js +0 -52
  21. package/dist/commands/debug.js +0 -200
  22. package/dist/commands/enrich.js +0 -184
  23. package/dist/commands/exec.js +0 -233
  24. package/dist/commands/gain.js +0 -156
  25. package/dist/commands/hook.js +0 -88
  26. package/dist/commands/index.js +0 -88
  27. package/dist/commands/init.js +0 -74
  28. package/dist/commands/install.js +0 -505
  29. package/dist/commands/learn.js +0 -116
  30. package/dist/commands/manifest.js +0 -193
  31. package/dist/commands/rewind.js +0 -103
  32. package/dist/commands/serve.js +0 -19
  33. package/dist/commands/setup-wizard.js +0 -414
  34. package/dist/commands/skills.js +0 -64
  35. package/dist/commands/stats.js +0 -20
  36. package/dist/commands/status.js +0 -654
  37. package/dist/commands/timeline.js +0 -139
  38. package/dist/commands/uninstall.js +0 -230
  39. package/dist/components/App.js +0 -109
  40. package/dist/components/Banner.js +0 -12
  41. package/dist/components/ConfirmPrompt.js +0 -25
  42. package/dist/components/DriftSummary.js +0 -23
  43. package/dist/components/GradeBadge.js +0 -15
  44. package/dist/components/HealthCard.js +0 -18
  45. package/dist/components/InkSpinner.js +0 -22
  46. package/dist/components/InputBox.js +0 -17
  47. package/dist/components/KeyValue.js +0 -13
  48. package/dist/components/MessageList.js +0 -14
  49. package/dist/components/ProgressBar.js +0 -26
  50. package/dist/components/Section.js +0 -16
  51. package/dist/components/SessionSummaryCard.js +0 -73
  52. package/dist/components/StartupDisplay.js +0 -24
  53. package/dist/components/StatusDashboard.js +0 -57
  54. package/dist/components/StatusLine.js +0 -8
  55. package/dist/components/StepLine.js +0 -22
  56. package/dist/components/Theme.js +0 -20
  57. package/dist/components/ToolProgress.js +0 -8
  58. package/dist/components/ViolationList.js +0 -21
  59. package/dist/components/render.js +0 -13
  60. package/dist/config/agent-registry.js +0 -237
  61. package/dist/config/claude-settings-hooks.js +0 -304
  62. package/dist/config/hook-installer.js +0 -65
  63. package/dist/config/instruction-writer.js +0 -388
  64. package/dist/config/mcp-config-writer.js +0 -266
  65. package/dist/config/settings.js +0 -174
  66. package/dist/config/tool-detector.js +0 -42
  67. package/dist/config/value-surfacing.js +0 -119
  68. package/dist/core/context-assembly.js +0 -108
  69. package/dist/core/conversation.js +0 -33
  70. package/dist/core/local-chat-provider.js +0 -475
  71. package/dist/core/provider-factory.js +0 -55
  72. package/dist/core/providers.js +0 -90
  73. package/dist/core/query-engine.js +0 -174
  74. package/dist/daemon/api.js +0 -312
  75. package/dist/daemon/autostart.js +0 -119
  76. package/dist/daemon/bootstrap.js +0 -39
  77. package/dist/daemon/client.js +0 -164
  78. package/dist/daemon/detect-ci.js +0 -81
  79. package/dist/daemon/platform-linux.js +0 -146
  80. package/dist/daemon/platform-macos.js +0 -134
  81. package/dist/daemon/platform-windows.js +0 -116
  82. package/dist/daemon/process-manager.js +0 -299
  83. package/dist/daemon/protocol.js +0 -23
  84. package/dist/daemon/registry.js +0 -270
  85. package/dist/daemon/settings-schema.js +0 -72
  86. package/dist/daemon/system-health.js +0 -134
  87. package/dist/daemon/version-checker.js +0 -262
  88. package/dist/daemon/warm-start.js +0 -223
  89. package/dist/entrypoints/cli.js +0 -1043
  90. package/dist/entrypoints/daemon.js +0 -380
  91. package/dist/entrypoints/repl.js +0 -147
  92. package/dist/hooks/adapters/claude-code.js +0 -90
  93. package/dist/hooks/adapters/cline.js +0 -100
  94. package/dist/hooks/adapters/cursor.js +0 -98
  95. package/dist/hooks/hook-dedup.js +0 -79
  96. package/dist/hooks/hook-runner.js +0 -113
  97. package/dist/hooks/navigation-hooks.js +0 -175
  98. package/dist/hooks/prompt-hooks.js +0 -63
  99. package/dist/hooks/shell-hooks.js +0 -47
  100. package/dist/ignore.js +0 -111
  101. package/dist/intelligence/approach-suggester.js +0 -61
  102. package/dist/intelligence/ast-extractor.js +0 -2615
  103. package/dist/intelligence/ast-worker.js +0 -34
  104. package/dist/intelligence/background-indexer.js +0 -121
  105. package/dist/intelligence/blast-radius.js +0 -200
  106. package/dist/intelligence/community-detection.js +0 -691
  107. package/dist/intelligence/community-detector.js +0 -184
  108. package/dist/intelligence/computation-scheduler.js +0 -75
  109. package/dist/intelligence/confidence-propagation.js +0 -47
  110. package/dist/intelligence/convention-detector.js +0 -242
  111. package/dist/intelligence/convention-learner.js +0 -205
  112. package/dist/intelligence/convention-matcher.js +0 -205
  113. package/dist/intelligence/cozo-schema.js +0 -376
  114. package/dist/intelligence/decision-point-detector.js +0 -90
  115. package/dist/intelligence/deep-dive-tools.js +0 -586
  116. package/dist/intelligence/durability-scorer.js +0 -84
  117. package/dist/intelligence/exploration-cost.js +0 -204
  118. package/dist/intelligence/exploration-pattern-tracker.js +0 -61
  119. package/dist/intelligence/fact-generator.js +0 -322
  120. package/dist/intelligence/facts-schema.js +0 -90
  121. package/dist/intelligence/file-intelligence.js +0 -59
  122. package/dist/intelligence/graph-holder.js +0 -220
  123. package/dist/intelligence/graph-temporal-joiner.js +0 -238
  124. package/dist/intelligence/health-grade.js +0 -423
  125. package/dist/intelligence/health-grader.js +0 -200
  126. package/dist/intelligence/health-map-data.js +0 -259
  127. package/dist/intelligence/import-symbols.js +0 -136
  128. package/dist/intelligence/incremental-indexer.js +0 -658
  129. package/dist/intelligence/indexer/centrality.js +0 -62
  130. package/dist/intelligence/indexer/cfg-context.js +0 -95
  131. package/dist/intelligence/indexer/confidence.js +0 -34
  132. package/dist/intelligence/indexer/cross-file-resolver.js +0 -104
  133. package/dist/intelligence/indexer/edge-repair.js +0 -89
  134. package/dist/intelligence/indexer/entity-key.js +0 -17
  135. package/dist/intelligence/indexer/export-map.js +0 -132
  136. package/dist/intelligence/indexer/git-cochange.js +0 -128
  137. package/dist/intelligence/indexer/graph-patch.js +0 -147
  138. package/dist/intelligence/indexer/incremental.js +0 -78
  139. package/dist/intelligence/indexer/ingest.js +0 -160
  140. package/dist/intelligence/indexer/language-detect.js +0 -226
  141. package/dist/intelligence/indexer/metadata.js +0 -63
  142. package/dist/intelligence/indexer/mutation-tracker.js +0 -79
  143. package/dist/intelligence/indexer/orchestrator.js +0 -155
  144. package/dist/intelligence/indexer/plugin-interface.js +0 -31
  145. package/dist/intelligence/indexer/plugins/csharp.js +0 -440
  146. package/dist/intelligence/indexer/plugins/go.js +0 -335
  147. package/dist/intelligence/indexer/plugins/java.js +0 -370
  148. package/dist/intelligence/indexer/plugins/python.js +0 -358
  149. package/dist/intelligence/indexer/plugins/regex-fallback.js +0 -82
  150. package/dist/intelligence/indexer/plugins/ruby.js +0 -290
  151. package/dist/intelligence/indexer/plugins/rust.js +0 -484
  152. package/dist/intelligence/indexer/plugins/tier2-generic.js +0 -310
  153. package/dist/intelligence/indexer/plugins/typescript.js +0 -456
  154. package/dist/intelligence/indexer/resource-monitor.js +0 -93
  155. package/dist/intelligence/indexer/scip/decoder.js +0 -253
  156. package/dist/intelligence/indexer/scip/detector.js +0 -232
  157. package/dist/intelligence/indexer/scip/downloader.js +0 -427
  158. package/dist/intelligence/indexer/scip/fallback.js +0 -34
  159. package/dist/intelligence/indexer/scip/merger.js +0 -109
  160. package/dist/intelligence/indexer/scip/orchestrator.js +0 -433
  161. package/dist/intelligence/indexer/scip/runner.js +0 -98
  162. package/dist/intelligence/indexer/snapshot.js +0 -66
  163. package/dist/intelligence/indexer/test-detector.js +0 -196
  164. package/dist/intelligence/indexer/watch-integration.js +0 -61
  165. package/dist/intelligence/indexer/worker.js +0 -85
  166. package/dist/intelligence/local-convention-detector.js +0 -437
  167. package/dist/intelligence/local-embeddings.js +0 -190
  168. package/dist/intelligence/local-graph.js +0 -1946
  169. package/dist/intelligence/local-indexer.js +0 -1575
  170. package/dist/intelligence/local-llm.js +0 -163
  171. package/dist/intelligence/local-rule-generator.js +0 -154
  172. package/dist/intelligence/local-snapshot.js +0 -213
  173. package/dist/intelligence/negative-knowledge.js +0 -103
  174. package/dist/intelligence/persistent-db.js +0 -85
  175. package/dist/intelligence/query-router.js +0 -2556
  176. package/dist/intelligence/risk-classifier.js +0 -116
  177. package/dist/intelligence/rule-evaluator.js +0 -380
  178. package/dist/intelligence/rule-generator.js +0 -49
  179. package/dist/intelligence/search-index.js +0 -173
  180. package/dist/intelligence/semantic/docstring-extractor.js +0 -67
  181. package/dist/intelligence/semantic/embedding-store.js +0 -52
  182. package/dist/intelligence/semantic/enrichment-orchestrator.js +0 -48
  183. package/dist/intelligence/semantic/git-message-miner.js +0 -114
  184. package/dist/intelligence/semantic/identifier-tokenizer.js +0 -51
  185. package/dist/intelligence/semantic/node2vec-embeddings.js +0 -71
  186. package/dist/intelligence/semantic/node2vec-walks.js +0 -103
  187. package/dist/intelligence/semantic/path-domain-inference.js +0 -112
  188. package/dist/intelligence/semantic/similarity-engine.js +0 -60
  189. package/dist/intelligence/semantic/tfidf-vectors.js +0 -88
  190. package/dist/intelligence/session-brief-builder.js +0 -159
  191. package/dist/intelligence/session-context.js +0 -221
  192. package/dist/intelligence/session-health-monitor.js +0 -211
  193. package/dist/intelligence/session-narrative.js +0 -197
  194. package/dist/intelligence/session-pattern-analyzer.js +0 -218
  195. package/dist/intelligence/signal-scorer.js +0 -390
  196. package/dist/intelligence/signal-show-store.js +0 -182
  197. package/dist/intelligence/smart-truncate.js +0 -158
  198. package/dist/intelligence/subgraph-cache.js +0 -88
  199. package/dist/intelligence/temporal-facts.js +0 -494
  200. package/dist/intelligence/token-estimator.js +0 -100
  201. package/dist/intelligence/tool-injector.js +0 -87
  202. package/dist/intelligence/tree-sitter-loader.js +0 -71
  203. package/dist/intelligence/worker-pool.js +0 -116
  204. package/dist/proxy/arg-validator.js +0 -79
  205. package/dist/proxy/auto-bootstrap.js +0 -167
  206. package/dist/proxy/bridge.js +0 -147
  207. package/dist/proxy/budget-enforcer.js +0 -70
  208. package/dist/proxy/compression-quality-monitor.js +0 -160
  209. package/dist/proxy/compression-stats.js +0 -51
  210. package/dist/proxy/context-rot-detector.js +0 -137
  211. package/dist/proxy/drift-detector.js +0 -139
  212. package/dist/proxy/efficiency-tracker.js +0 -79
  213. package/dist/proxy/fact-ranking.js +0 -154
  214. package/dist/proxy/format-encoder.js +0 -266
  215. package/dist/proxy/http-transport.js +0 -90
  216. package/dist/proxy/lifecycle-actor.js +0 -55
  217. package/dist/proxy/lifecycle-machine.js +0 -187
  218. package/dist/proxy/log-tailer.js +0 -265
  219. package/dist/proxy/model-pricing.js +0 -98
  220. package/dist/proxy/network-firewall.js +0 -141
  221. package/dist/proxy/nudge-state.js +0 -93
  222. package/dist/proxy/output-compressor.js +0 -185
  223. package/dist/proxy/pid-lock.js +0 -291
  224. package/dist/proxy/proxy-context.js +0 -11
  225. package/dist/proxy/proxy.js +0 -2633
  226. package/dist/proxy/response-enrichment.js +0 -32
  227. package/dist/proxy/response-envelope.js +0 -313
  228. package/dist/proxy/session-dedup.js +0 -82
  229. package/dist/proxy/session-legend.js +0 -30
  230. package/dist/proxy/session-persistence.js +0 -210
  231. package/dist/proxy/session-resume.js +0 -94
  232. package/dist/proxy/session-stats.js +0 -513
  233. package/dist/proxy/shell-classifier.js +0 -1346
  234. package/dist/proxy/shell-compression-log.js +0 -93
  235. package/dist/proxy/shell-compressor.js +0 -390
  236. package/dist/proxy/shell-graph-boost.js +0 -202
  237. package/dist/proxy/shell-monitor-map.js +0 -18
  238. package/dist/proxy/shell-stats.js +0 -54
  239. package/dist/proxy/shell-strategies/cloud.js +0 -215
  240. package/dist/proxy/shell-strategies/diff.js +0 -159
  241. package/dist/proxy/shell-strategies/error-diagnostic.js +0 -796
  242. package/dist/proxy/shell-strategies/filter-dsl.js +0 -358
  243. package/dist/proxy/shell-strategies/git-status.js +0 -177
  244. package/dist/proxy/shell-strategies/key-value.js +0 -193
  245. package/dist/proxy/shell-strategies/log-text.js +0 -154
  246. package/dist/proxy/shell-strategies/omni.js +0 -188
  247. package/dist/proxy/shell-strategies/progress.js +0 -55
  248. package/dist/proxy/shell-strategies/redact.js +0 -76
  249. package/dist/proxy/shell-strategies/structured.js +0 -241
  250. package/dist/proxy/shell-strategies/tabular.js +0 -243
  251. package/dist/proxy/shell-strategies/test-results-types.js +0 -13
  252. package/dist/proxy/shell-strategies/test-results.js +0 -784
  253. package/dist/proxy/shell-strategies/tree-paths.js +0 -144
  254. package/dist/proxy/shell-strategies/yaml.js +0 -182
  255. package/dist/proxy/shell-tee.js +0 -111
  256. package/dist/proxy/signal-dedup.js +0 -171
  257. package/dist/proxy/startup-renderer.js +0 -158
  258. package/dist/proxy/task-token-display.js +0 -38
  259. package/dist/proxy/token-counter.js +0 -61
  260. package/dist/proxy/tool-clusters.js +0 -273
  261. package/dist/proxy/tool-definitions.js +0 -525
  262. package/dist/proxy/transport-mux.js +0 -229
  263. package/dist/proxy/wire-cap.js +0 -268
  264. package/dist/rules/developer.mozilla.org.json +0 -9
  265. package/dist/rules/github.com.json +0 -21
  266. package/dist/schemas/api/skills.js +0 -19
  267. package/dist/schemas/common/errors.js +0 -7
  268. package/dist/schemas/common/headers.js +0 -5
  269. package/dist/schemas/entities/edge.js +0 -25
  270. package/dist/schemas/entities/entity.js +0 -22
  271. package/dist/schemas/entities/rule.js +0 -18
  272. package/dist/schemas/index.js +0 -14
  273. package/dist/server/event-bus.js +0 -59
  274. package/dist/server/http.js +0 -156
  275. package/dist/server/middleware.js +0 -70
  276. package/dist/server/routes/drift.js +0 -97
  277. package/dist/server/routes/intelligence.js +0 -1217
  278. package/dist/server/routes/reasoning-quality.js +0 -444
  279. package/dist/server/routes/session.js +0 -86
  280. package/dist/server/routes/stream.js +0 -120
  281. package/dist/server/routes/system.js +0 -73
  282. package/dist/server/routes/temporal.js +0 -170
  283. package/dist/server/routes/timeline.js +0 -232
  284. package/dist/server/routes/token-flow.js +0 -403
  285. package/dist/skills/effectiveness-tracker.js +0 -93
  286. package/dist/skills/local-pack.js +0 -380
  287. package/dist/skills/resolver.js +0 -495
  288. package/dist/state-detector.js +0 -83
  289. package/dist/timeline/intent-detector.js +0 -263
  290. package/dist/timeline/loop-miner.js +0 -140
  291. package/dist/timeline/open-threads.js +0 -49
  292. package/dist/timeline/signal-reinforcer.js +0 -62
  293. package/dist/timeline/timeline-bootstrap.js +0 -151
  294. package/dist/timeline/timeline-store.js +0 -618
  295. package/dist/tools/coding/bash.js +0 -49
  296. package/dist/tools/coding/file-edit.js +0 -72
  297. package/dist/tools/coding/file-outline.js +0 -227
  298. package/dist/tools/coding/file-read-protocol.js +0 -425
  299. package/dist/tools/coding/file-read.js +0 -35
  300. package/dist/tools/coding/file-write.js +0 -43
  301. package/dist/tools/coding/glob-tool.js +0 -109
  302. package/dist/tools/coding/grep.js +0 -162
  303. package/dist/tools/coding/index.js +0 -27
  304. package/dist/tools/intelligence/index.js +0 -269
  305. package/dist/tools/intelligence/record-fact.js +0 -48
  306. package/dist/tools/intelligence/timeline-markers.js +0 -130
  307. package/dist/tools/registry.js +0 -47
  308. package/dist/tools/types.js +0 -8
  309. package/dist/tracking/auto-snapshot-triggers.js +0 -246
  310. package/dist/tracking/branch-context.js +0 -115
  311. package/dist/tracking/branch-snapshot.js +0 -217
  312. package/dist/tracking/causal-bridge.js +0 -317
  313. package/dist/tracking/circuit-breaker.js +0 -147
  314. package/dist/tracking/commit-watcher.js +0 -114
  315. package/dist/tracking/context-ledger.js +0 -119
  316. package/dist/tracking/correction-detector.js +0 -324
  317. package/dist/tracking/drift-tracker.js +0 -874
  318. package/dist/tracking/durability-tracker.js +0 -94
  319. package/dist/tracking/entity-rewind.js +0 -200
  320. package/dist/tracking/file-hash-state.js +0 -114
  321. package/dist/tracking/git-attribution.js +0 -132
  322. package/dist/tracking/git-trailers.js +0 -171
  323. package/dist/tracking/intelligence-counter.js +0 -46
  324. package/dist/tracking/intent-correlator.js +0 -202
  325. package/dist/tracking/intent-encoder.js +0 -52
  326. package/dist/tracking/intent-token-tracker.js +0 -159
  327. package/dist/tracking/ledger-archiver.js +0 -94
  328. package/dist/tracking/ledger-chains.js +0 -245
  329. package/dist/tracking/metrics-store.js +0 -361
  330. package/dist/tracking/native-watcher.js +0 -131
  331. package/dist/tracking/offline-rewind.js +0 -295
  332. package/dist/tracking/pending-violations.js +0 -74
  333. package/dist/tracking/persistence-effectiveness.js +0 -167
  334. package/dist/tracking/prompt-durability.js +0 -202
  335. package/dist/tracking/quality-signals.js +0 -213
  336. package/dist/tracking/redactor.js +0 -73
  337. package/dist/tracking/rewind-engine.js +0 -161
  338. package/dist/tracking/session-history.js +0 -128
  339. package/dist/tracking/session-receipt.js +0 -88
  340. package/dist/tracking/session-summary-writer.js +0 -157
  341. package/dist/tracking/shadow-ledger.js +0 -321
  342. package/dist/tracking/stash-manager.js +0 -258
  343. package/dist/tracking/timeline-fork.js +0 -213
  344. package/dist/tracking/timeline.js +0 -69
  345. package/dist/tracking/token-flow.js +0 -276
  346. package/dist/tracking/turn-segmenter.js +0 -122
  347. package/dist/tracking/weekly-accumulator.js +0 -179
  348. package/dist/tracking/working-snapshots.js +0 -188
  349. package/dist/tracking/workspace-manifest.js +0 -176
  350. package/dist/transport/http.js +0 -102
  351. package/dist/utils/counterfactual.js +0 -65
  352. package/dist/utils/deep-link.js +0 -34
  353. package/dist/utils/detect.js +0 -193
  354. package/dist/utils/exec.js +0 -73
  355. package/dist/utils/file-logger.js +0 -87
  356. package/dist/utils/format-error.js +0 -29
  357. package/dist/utils/git.js +0 -181
  358. package/dist/utils/log.js +0 -57
  359. package/dist/utils/logger.js +0 -35
  360. package/dist/utils/mcp-content-json.js +0 -8
  361. package/dist/utils/session-logger.js +0 -154
  362. package/dist/utils/startup-log.js +0 -512
  363. 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
- ]);