@unerr-ai/unerr 0.1.6 → 0.1.7

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 (218) hide show
  1. package/README.md +70 -194
  2. package/dist/cli.js +39149 -36991
  3. package/package.json +9 -2
  4. package/dist/__tests__/architecture-guard.test.js +0 -122
  5. package/dist/__tests__/arg-validator.test.js +0 -205
  6. package/dist/__tests__/ast-extractor.test.js +0 -203
  7. package/dist/__tests__/auto-bootstrap.test.js +0 -280
  8. package/dist/__tests__/background-indexer.test.js +0 -228
  9. package/dist/__tests__/blast-radius-engine.test.js +0 -200
  10. package/dist/__tests__/bridge-isolation.test.js +0 -37
  11. package/dist/__tests__/budget-enforcer.test.js +0 -53
  12. package/dist/__tests__/cfg-test-detection-perf.test.js +0 -82
  13. package/dist/__tests__/change-narrative.test.js +0 -190
  14. package/dist/__tests__/check-commit.test.js +0 -258
  15. package/dist/__tests__/checksum.test.js +0 -34
  16. package/dist/__tests__/commit-watcher.test.js +0 -154
  17. package/dist/__tests__/community-detection.test.js +0 -179
  18. package/dist/__tests__/community-tools.test.js +0 -299
  19. package/dist/__tests__/components.test.js +0 -449
  20. package/dist/__tests__/compression-log.test.js +0 -174
  21. package/dist/__tests__/compression-quality-monitor.test.js +0 -40
  22. package/dist/__tests__/config-healer.test.js +0 -165
  23. package/dist/__tests__/context-ledger.test.js +0 -58
  24. package/dist/__tests__/convention-detector.test.js +0 -99
  25. package/dist/__tests__/convention-learner.test.js +0 -86
  26. package/dist/__tests__/correction-detector.test.js +0 -330
  27. package/dist/__tests__/daemon-autostart-install.test.js +0 -283
  28. package/dist/__tests__/daemon-bridge.test.js +0 -222
  29. package/dist/__tests__/daemon-dashboard.test.js +0 -202
  30. package/dist/__tests__/daemon-registry.test.js +0 -240
  31. package/dist/__tests__/daemon-supervisor.test.js +0 -318
  32. package/dist/__tests__/daemon-version-check.test.js +0 -275
  33. package/dist/__tests__/decision-point-detector.test.js +0 -98
  34. package/dist/__tests__/deep-link.test.js +0 -143
  35. package/dist/__tests__/disallowed-tools.test.js +0 -115
  36. package/dist/__tests__/drift-tracker.test.js +0 -582
  37. package/dist/__tests__/durability-scorer.test.js +0 -152
  38. package/dist/__tests__/efficiency-tracker.test.js +0 -65
  39. package/dist/__tests__/enrich.test.js +0 -144
  40. package/dist/__tests__/entity-rewind.test.js +0 -248
  41. package/dist/__tests__/ephemeral.test.js +0 -111
  42. package/dist/__tests__/exploration-cost.test.js +0 -93
  43. package/dist/__tests__/fact-generator.test.js +0 -197
  44. package/dist/__tests__/file-l0-graph.test.js +0 -244
  45. package/dist/__tests__/file-logger.test.js +0 -82
  46. package/dist/__tests__/file-outline.test.js +0 -141
  47. package/dist/__tests__/file-read-protocol.test.js +0 -188
  48. package/dist/__tests__/format-encoder.test.js +0 -233
  49. package/dist/__tests__/git-attribution.test.js +0 -259
  50. package/dist/__tests__/graph-temporal-joiner.test.js +0 -219
  51. package/dist/__tests__/health-grade-enhanced.test.js +0 -138
  52. package/dist/__tests__/health-map-data.test.js +0 -173
  53. package/dist/__tests__/helpers/mcp-harness.js +0 -45
  54. package/dist/__tests__/helpers/mcp-harness.test.js +0 -68
  55. package/dist/__tests__/hook-dedup.test.js +0 -112
  56. package/dist/__tests__/hook-runner.test.js +0 -253
  57. package/dist/__tests__/indexer-cfg.test.js +0 -185
  58. package/dist/__tests__/indexer-cross-file.test.js +0 -172
  59. package/dist/__tests__/indexer-extraction.test.js +0 -245
  60. package/dist/__tests__/indexer-incremental.test.js +0 -232
  61. package/dist/__tests__/indexer-language-expansion.test.js +0 -165
  62. package/dist/__tests__/init-push.test.js +0 -131
  63. package/dist/__tests__/instruction-writer.test.js +0 -179
  64. package/dist/__tests__/intelligence-integration.test.js +0 -217
  65. package/dist/__tests__/intent-correlator.test.js +0 -175
  66. package/dist/__tests__/intent-detector.test.js +0 -235
  67. package/dist/__tests__/intent-encoder.test.js +0 -167
  68. package/dist/__tests__/java-build-tool-detection.test.js +0 -174
  69. package/dist/__tests__/layer3-sprint-q.test.js +0 -160
  70. package/dist/__tests__/layer3-sprint-r.test.js +0 -91
  71. package/dist/__tests__/layer3-sprint-s.test.js +0 -183
  72. package/dist/__tests__/layer3-sprint-t.test.js +0 -201
  73. package/dist/__tests__/layer3-sprint-u.test.js +0 -174
  74. package/dist/__tests__/layer4-sprint-ba2.test.js +0 -354
  75. package/dist/__tests__/layer4-sprint-ba4.test.js +0 -84
  76. package/dist/__tests__/layer4-sprint-vs.test.js +0 -105
  77. package/dist/__tests__/ledger-chains.test.js +0 -162
  78. package/dist/__tests__/lifecycle-machine.test.js +0 -226
  79. package/dist/__tests__/local-chat-provider.test.js +0 -170
  80. package/dist/__tests__/local-convention-detector.test.js +0 -308
  81. package/dist/__tests__/local-embeddings.test.js +0 -422
  82. package/dist/__tests__/local-graph.test.js +0 -540
  83. package/dist/__tests__/local-indexer.test.js +0 -228
  84. package/dist/__tests__/local-intelligence-l3.test.js +0 -332
  85. package/dist/__tests__/local-llm.test.js +0 -253
  86. package/dist/__tests__/local-mode-offline.test.js +0 -187
  87. package/dist/__tests__/local-mode-stats.test.js +0 -273
  88. package/dist/__tests__/local-mode-tui.test.js +0 -343
  89. package/dist/__tests__/local-parse.test.js +0 -199
  90. package/dist/__tests__/log-tailer.test.js +0 -208
  91. package/dist/__tests__/loop-breaker.test.js +0 -276
  92. package/dist/__tests__/loop-miner.test.js +0 -226
  93. package/dist/__tests__/mcp-config.test.js +0 -126
  94. package/dist/__tests__/mcp-content-json.test.js +0 -10
  95. package/dist/__tests__/mcp-envelope.test.js +0 -124
  96. package/dist/__tests__/metrics-store.test.js +0 -223
  97. package/dist/__tests__/native-watcher.test.js +0 -191
  98. package/dist/__tests__/navigation-hooks-agent-aware.test.js +0 -145
  99. package/dist/__tests__/negative-knowledge.test.js +0 -116
  100. package/dist/__tests__/network-boundary.test.js +0 -190
  101. package/dist/__tests__/network-firewall.test.js +0 -112
  102. package/dist/__tests__/nudge-invariants.test.js +0 -160
  103. package/dist/__tests__/nudge-v2.test.js +0 -225
  104. package/dist/__tests__/offline-rewind.test.js +0 -251
  105. package/dist/__tests__/open-threads.test.js +0 -89
  106. package/dist/__tests__/output-compressor.test.js +0 -93
  107. package/dist/__tests__/pending-violations.test.js +0 -112
  108. package/dist/__tests__/persistence-effectiveness.test.js +0 -143
  109. package/dist/__tests__/provider-factory.test.js +0 -42
  110. package/dist/__tests__/providers.test.js +0 -24
  111. package/dist/__tests__/proxy.test.js +0 -314
  112. package/dist/__tests__/query-router.test.js +0 -1018
  113. package/dist/__tests__/reasoning-quality-route.test.js +0 -138
  114. package/dist/__tests__/redactor.test.js +0 -120
  115. package/dist/__tests__/resource-monitor.test.js +0 -57
  116. package/dist/__tests__/response-envelope.test.js +0 -100
  117. package/dist/__tests__/risk-classifier.test.js +0 -101
  118. package/dist/__tests__/risk-signal-scope.test.js +0 -75
  119. package/dist/__tests__/rule-evaluator.test.js +0 -280
  120. package/dist/__tests__/scip-decoder.test.js +0 -49
  121. package/dist/__tests__/scip-downloader.test.js +0 -201
  122. package/dist/__tests__/scip-merger.test.js +0 -103
  123. package/dist/__tests__/search-index.test.js +0 -422
  124. package/dist/__tests__/semantic-enrichment.test.js +0 -360
  125. package/dist/__tests__/session-brief-builder.test.js +0 -187
  126. package/dist/__tests__/session-context.test.js +0 -221
  127. package/dist/__tests__/session-continuity.test.js +0 -144
  128. package/dist/__tests__/session-dedup.test.js +0 -74
  129. package/dist/__tests__/session-event-wiring.test.js +0 -206
  130. package/dist/__tests__/session-events.test.js +0 -149
  131. package/dist/__tests__/session-legend.test.js +0 -20
  132. package/dist/__tests__/session-persistence.test.js +0 -131
  133. package/dist/__tests__/session-resume-block.test.js +0 -107
  134. package/dist/__tests__/session-resume.test.js +0 -97
  135. package/dist/__tests__/session-summary-writer.test.js +0 -134
  136. package/dist/__tests__/shadow-ledger.test.js +0 -203
  137. package/dist/__tests__/shell-classifier.test.js +0 -151
  138. package/dist/__tests__/shell-compression-floor.test.js +0 -189
  139. package/dist/__tests__/shell-compression-v2.test.js +0 -339
  140. package/dist/__tests__/shell-compressor.test.js +0 -35
  141. package/dist/__tests__/shell-hooks.test.js +0 -128
  142. package/dist/__tests__/shell-strategies.test.js +0 -644
  143. package/dist/__tests__/shell-tee.test.js +0 -133
  144. package/dist/__tests__/signal-dedup.test.js +0 -158
  145. package/dist/__tests__/signal-reinforcer.test.js +0 -77
  146. package/dist/__tests__/signal-scorer.test.js +0 -251
  147. package/dist/__tests__/signal-show-store.test.js +0 -108
  148. package/dist/__tests__/smart-truncate.test.js +0 -215
  149. package/dist/__tests__/snapshot-v2.test.js +0 -113
  150. package/dist/__tests__/sprint-l1-local-mode.test.js +0 -130
  151. package/dist/__tests__/sprint-l10-boot.test.js +0 -220
  152. package/dist/__tests__/sprint-l9-offline-commands.test.js +0 -189
  153. package/dist/__tests__/sprint-q-persistent-context.test.js +0 -198
  154. package/dist/__tests__/sprint-s1-wiring.test.js +0 -215
  155. package/dist/__tests__/sprint-s2-wiring.test.js +0 -256
  156. package/dist/__tests__/sprint-s3-wiring.test.js +0 -195
  157. package/dist/__tests__/sprint-s4-wiring.test.js +0 -213
  158. package/dist/__tests__/sprint-s6-hooks.test.js +0 -222
  159. package/dist/__tests__/sprint-s7-persistent.test.js +0 -263
  160. package/dist/__tests__/sprint-s8-value.test.js +0 -167
  161. package/dist/__tests__/sprint-s9-behavioral.test.js +0 -179
  162. package/dist/__tests__/sprint3-intelligence.test.js +0 -297
  163. package/dist/__tests__/sprint5-mcp-server.test.js +0 -136
  164. package/dist/__tests__/startup-display.test.js +0 -302
  165. package/dist/__tests__/startup-log-file.test.js +0 -97
  166. package/dist/__tests__/stash-manager.test.js +0 -229
  167. package/dist/__tests__/state-detector.test.js +0 -92
  168. package/dist/__tests__/status-dashboard.test.js +0 -142
  169. package/dist/__tests__/temporal-facts.test.js +0 -292
  170. package/dist/__tests__/temporal-routes.test.js +0 -142
  171. package/dist/__tests__/test-detector.test.js +0 -174
  172. package/dist/__tests__/theme.test.js +0 -72
  173. package/dist/__tests__/timeline-agents.test.js +0 -122
  174. package/dist/__tests__/timeline-bootstrap.test.js +0 -176
  175. package/dist/__tests__/timeline-filters.test.js +0 -193
  176. package/dist/__tests__/timeline-markers.test.js +0 -151
  177. package/dist/__tests__/timeline-routes.test.js +0 -156
  178. package/dist/__tests__/timeline-store.test.js +0 -171
  179. package/dist/__tests__/token-counter.test.js +0 -86
  180. package/dist/__tests__/token-estimator.test.js +0 -96
  181. package/dist/__tests__/token-flow-api.test.js +0 -239
  182. package/dist/__tests__/token-flow-instrumentation.test.js +0 -437
  183. package/dist/__tests__/token-flow-persistence.test.js +0 -356
  184. package/dist/__tests__/token-flow-routes.test.js +0 -199
  185. package/dist/__tests__/token-flow.test.js +0 -695
  186. package/dist/__tests__/tool-clusters.test.js +0 -177
  187. package/dist/__tests__/transport-mux.test.js +0 -283
  188. package/dist/__tests__/turn-segmenter.test.js +0 -166
  189. package/dist/__tests__/uninstall.test.js +0 -141
  190. package/dist/__tests__/warm-start-policy.test.js +0 -271
  191. package/dist/__tests__/wire-cap-nudge.test.js +0 -77
  192. package/dist/__tests__/worker-pool.test.js +0 -101
  193. package/dist/ui/assets/index-7gl3mIuY.css +0 -1
  194. package/dist/ui/assets/index-CX4FCWGT.js +0 -10
  195. package/dist/ui/assets/rolldown-runtime-S-ySWqyJ.js +0 -1
  196. package/dist/ui/assets/vis-network-NIJHUFI3.js +0 -908
  197. package/dist/ui/fonts/jetbrains-mono-latin-400-normal.woff +0 -0
  198. package/dist/ui/icon-wordmark.png +0 -0
  199. package/dist/ui/icon-wordmark.svg +0 -30
  200. package/dist/ui/icon.png +0 -0
  201. package/dist/ui/icon.svg +0 -25
  202. package/dist/ui/index.html +0 -15
  203. package/dist/ui/prototype-sandbox/index.html +0 -257
  204. package/dist/ui/screenshots/activity.png +0 -0
  205. package/dist/ui/screenshots/code-base-intelligence.png +0 -0
  206. package/dist/ui/screenshots/dashboard.png +0 -0
  207. package/dist/ui/screenshots/project-memory.png +0 -0
  208. package/dist/ui/screenshots/reasoning-quality.png +0 -0
  209. package/dist/ui/screenshots/reasoning-session.png +0 -0
  210. package/dist/ui/screenshots/token-session.png +0 -0
  211. package/dist/ui/screenshots/token-trace-main.png +0 -0
  212. package/dist/ui/screenshots/token-turn.png +0 -0
  213. package/dist/ui/unerr-wordmark.png +0 -0
  214. package/dist/ui/unerr-wordmark.svg +0 -9
  215. package/dist/ui/unerr.png +0 -0
  216. package/dist/ui/unerr.svg +0 -25
  217. package/dist/ui/web-app-manifest-192x192.png +0 -0
  218. package/dist/ui/web-app-manifest-512x512.png +0 -0
@@ -1,582 +0,0 @@
1
- /**
2
- * P10-TEST-03: Drift tracker tests — drift computation, overlay management.
3
- */
4
- import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
5
- import { tmpdir } from "node:os";
6
- import { join } from "node:path";
7
- import { afterEach, beforeEach, describe, expect, it } from "vitest";
8
- import { entityKey } from "../intelligence/ast-extractor.js";
9
- import { DRIFT_WATCHER_OPTIONS, DriftTracker, MtimeCache, determineOrigin, } from "../tracking/drift-tracker.js";
10
- import { FileHashManager, contentSha256 } from "../tracking/file-hash-state.js";
11
- let tempDir;
12
- let projectRoot;
13
- let unerrDir;
14
- beforeEach(() => {
15
- tempDir = join(tmpdir(), `unerr-drift-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
16
- projectRoot = join(tempDir, "project");
17
- unerrDir = join(projectRoot, ".unerr");
18
- mkdirSync(join(unerrDir, "state"), { recursive: true });
19
- mkdirSync(join(projectRoot, "src"), { recursive: true });
20
- });
21
- afterEach(() => {
22
- try {
23
- rmSync(tempDir, { recursive: true, force: true });
24
- }
25
- catch {
26
- /* ignore */
27
- }
28
- });
29
- /** Create a mock CozoGraphStore with in-memory drift overlay */
30
- function createMockGraph(baseEntities = []) {
31
- const driftOverlay = new Map();
32
- return {
33
- driftOverlay,
34
- getEntity: (key) => baseEntities.find((e) => e.key === key) ?? null,
35
- getCallersOf: () => [],
36
- getCalleesOf: () => [],
37
- getEntitiesByFile: (fp) => baseEntities.filter((e) => e.file_path === fp),
38
- searchEntities: () => [],
39
- getImports: () => [],
40
- healthCheck: () => ({ status: "up", latencyMs: 0 }),
41
- isLoaded: () => true,
42
- loadSnapshot: () => { },
43
- hasRules: () => false,
44
- getRules: () => [],
45
- getPatterns: () => [],
46
- loadRules: () => { },
47
- loadPatterns: () => { },
48
- hasJustifications: () => false,
49
- getBusinessContext: () => null,
50
- getConventions: () => [],
51
- loadJustifications: () => { },
52
- upsertDriftEntity: (entity) => {
53
- driftOverlay.set(entity.key, entity);
54
- },
55
- removeDriftEntity: (key) => {
56
- driftOverlay.delete(key);
57
- },
58
- getDriftEntitiesForFile: (fp) => {
59
- const result = [];
60
- for (const [, e] of driftOverlay) {
61
- if (e.file_path === fp)
62
- result.push(e);
63
- }
64
- return result;
65
- },
66
- clearDriftOverlay: () => {
67
- driftOverlay.clear();
68
- },
69
- findEntityByName: (name) => baseEntities.find((e) => e.name === name) ?? null,
70
- getAllDriftEdges: () => [],
71
- upsertDriftEdge: () => { },
72
- clearDriftEdges: () => { },
73
- getDriftSummary: () => {
74
- const summary = {
75
- added: 0,
76
- modified: 0,
77
- deleted: 0,
78
- dependency_changed: 0,
79
- total: 0,
80
- };
81
- for (const [, e] of driftOverlay) {
82
- if (e.drift_status === "added")
83
- summary.added++;
84
- else if (e.drift_status === "modified")
85
- summary.modified++;
86
- else if (e.drift_status === "deleted")
87
- summary.deleted++;
88
- else if (e.drift_status === "dependency_changed")
89
- summary.dependency_changed++;
90
- }
91
- summary.total =
92
- summary.added +
93
- summary.modified +
94
- summary.deleted +
95
- summary.dependency_changed;
96
- return summary;
97
- },
98
- };
99
- }
100
- /**
101
- * Create a mock graph with caller edge relationships.
102
- * `callerMap` maps callee keys → array of caller LocalEntity objects.
103
- */
104
- function createMockGraphWithCallers(baseEntities, callerMap) {
105
- const base = createMockGraph(baseEntities);
106
- // Override getCallersOf to return from the caller map
107
- base.getCallersOf = (key) => callerMap.get(key) ?? [];
108
- return base;
109
- }
110
- describe("DriftTracker", () => {
111
- it("detects added entities (new file)", async () => {
112
- const graph = createMockGraph();
113
- const fileHashManager = new FileHashManager(unerrDir);
114
- const tracker = new DriftTracker({ projectRoot, repoId: "repo1", unerrDir }, graph, fileHashManager);
115
- // Write a TypeScript file with a function
116
- writeFileSync(join(projectRoot, "src/new-feature.ts"), `export function newFeature() {
117
- return "hello"
118
- }`);
119
- const result = await tracker.processFile("src/new-feature.ts", "abc123");
120
- expect(result.filesProcessed).toBe(1);
121
- expect(result.entitiesAdded).toBe(1);
122
- expect(result.entitiesModified).toBe(0);
123
- expect(result.entitiesDeleted).toBe(0);
124
- expect(graph.driftOverlay.size).toBe(1);
125
- const driftEntity = [...graph.driftOverlay.values()][0];
126
- expect(driftEntity.name).toBe("newFeature");
127
- expect(driftEntity.drift_status).toBe("added");
128
- expect(driftEntity.file_path).toBe("src/new-feature.ts");
129
- });
130
- it("detects modified entities (content changed)", async () => {
131
- // Base entity
132
- const baseEntities = [
133
- {
134
- key: "test-key-1",
135
- kind: "function",
136
- name: "existingFn",
137
- file_path: "src/existing.ts",
138
- start_line: 1,
139
- end_line: 0,
140
- signature: "()",
141
- body: "function existingFn() {\n return 1\n}",
142
- fan_in: 0,
143
- fan_out: 0,
144
- risk_level: "normal",
145
- community: -1,
146
- },
147
- ];
148
- const graph = createMockGraph(baseEntities);
149
- const fileHashManager = new FileHashManager(unerrDir);
150
- const tracker = new DriftTracker({ projectRoot, repoId: "repo1", unerrDir }, graph, fileHashManager);
151
- // Write modified file
152
- writeFileSync(join(projectRoot, "src/existing.ts"), `function existingFn() {
153
- return 2
154
- }`);
155
- const result = await tracker.processFile("src/existing.ts", "abc123");
156
- expect(result.filesProcessed).toBe(1);
157
- // Either modified or added depending on key match
158
- expect(result.entitiesModified + result.entitiesAdded).toBeGreaterThanOrEqual(1);
159
- });
160
- it("skips unchanged files", async () => {
161
- const graph = createMockGraph();
162
- const fileHashManager = new FileHashManager(unerrDir);
163
- const tracker = new DriftTracker({ projectRoot, repoId: "repo1", unerrDir }, graph, fileHashManager);
164
- const content = "export function unchanged() { return 1 }";
165
- writeFileSync(join(projectRoot, "src/unchanged.ts"), content);
166
- // First process — should process
167
- const r1 = await tracker.processFile("src/unchanged.ts", "abc123");
168
- expect(r1.filesProcessed).toBe(1);
169
- // Mark as processed manually
170
- const sha = contentSha256(content);
171
- fileHashManager.markProcessed("src/unchanged.ts", sha, "abc123");
172
- fileHashManager.save();
173
- // Second process — should skip
174
- const r2 = await tracker.processFile("src/unchanged.ts", "abc123");
175
- expect(r2.filesSkipped).toBe(1);
176
- expect(r2.filesProcessed).toBe(0);
177
- });
178
- it("handles deleted files", async () => {
179
- const baseEntities = [
180
- {
181
- key: "del-key-1",
182
- kind: "function",
183
- name: "deletedFn",
184
- file_path: "src/deleted.ts",
185
- start_line: 1,
186
- end_line: 0,
187
- signature: "",
188
- body: "",
189
- fan_in: 2,
190
- fan_out: 1,
191
- risk_level: "normal",
192
- community: -1,
193
- },
194
- ];
195
- const graph = createMockGraph(baseEntities);
196
- const fileHashManager = new FileHashManager(unerrDir);
197
- const tracker = new DriftTracker({ projectRoot, repoId: "repo1", unerrDir }, graph, fileHashManager);
198
- // Don't create the file — it's "deleted"
199
- const result = await tracker.processFile("src/deleted.ts", "abc123");
200
- expect(result.filesProcessed).toBe(1);
201
- expect(result.entitiesDeleted).toBe(1);
202
- const deletedEntity = [...graph.driftOverlay.values()][0];
203
- expect(deletedEntity.drift_status).toBe("deleted");
204
- expect(deletedEntity.name).toBe("deletedFn");
205
- });
206
- it("skips unsupported languages", async () => {
207
- const graph = createMockGraph();
208
- const fileHashManager = new FileHashManager(unerrDir);
209
- const tracker = new DriftTracker({ projectRoot, repoId: "repo1", unerrDir }, graph, fileHashManager);
210
- writeFileSync(join(projectRoot, "data.json"), '{"key": "value"}');
211
- const result = await tracker.processFile("data.json", "abc123");
212
- expect(result.filesProcessed).toBe(0);
213
- expect(result.filesSkipped).toBe(0);
214
- expect(graph.driftOverlay.size).toBe(0);
215
- });
216
- it("processes batch of files", async () => {
217
- const graph = createMockGraph();
218
- const fileHashManager = new FileHashManager(unerrDir);
219
- const tracker = new DriftTracker({ projectRoot, repoId: "repo1", unerrDir }, graph, fileHashManager);
220
- writeFileSync(join(projectRoot, "src/a.ts"), "export function alpha() { return 1 }");
221
- writeFileSync(join(projectRoot, "src/b.ts"), "export function beta() { return 2 }");
222
- const result = await tracker.processFiles(["src/a.ts", "src/b.ts"], "abc123");
223
- expect(result.filesProcessed).toBe(2);
224
- expect(result.entitiesAdded).toBe(2);
225
- });
226
- it("drift summary aggregates correctly", async () => {
227
- const baseEntities = [
228
- {
229
- key: "mod-key-1",
230
- kind: "function",
231
- name: "oldFn",
232
- file_path: "src/old.ts",
233
- start_line: 1,
234
- end_line: 0,
235
- signature: "",
236
- body: "function oldFn() { return 1 }",
237
- fan_in: 0,
238
- fan_out: 0,
239
- risk_level: "normal",
240
- community: -1,
241
- },
242
- ];
243
- const graph = createMockGraph(baseEntities);
244
- const fileHashManager = new FileHashManager(unerrDir);
245
- const tracker = new DriftTracker({ projectRoot, repoId: "repo1", unerrDir }, graph, fileHashManager);
246
- // Add a new file
247
- writeFileSync(join(projectRoot, "src/new.ts"), "export function newOne() { return 1 }");
248
- await tracker.processFile("src/new.ts", "abc123");
249
- // Delete an existing file (don't write it)
250
- await tracker.processFile("src/old.ts", "abc123");
251
- const summary = await tracker.getDriftSummary();
252
- expect(summary.total).toBeGreaterThan(0);
253
- expect(summary.added + summary.modified + summary.deleted).toBe(summary.total);
254
- });
255
- it("clears overlay on branch switch", async () => {
256
- const graph = createMockGraph();
257
- const fileHashManager = new FileHashManager(unerrDir);
258
- const tracker = new DriftTracker({ projectRoot, repoId: "repo1", unerrDir }, graph, fileHashManager);
259
- writeFileSync(join(projectRoot, "src/a.ts"), "export function alpha() { return 1 }");
260
- await tracker.processFile("src/a.ts", "abc123");
261
- expect(graph.driftOverlay.size).toBe(1);
262
- // Branch switch — clears and recomputes
263
- writeFileSync(join(projectRoot, "src/b.ts"), "export function beta() { return 1 }");
264
- await tracker.onBranchSwitch(["src/b.ts"], "def456");
265
- // Old overlay should be cleared, new file processed
266
- const summary = await tracker.getDriftSummary();
267
- expect(summary.added).toBe(1);
268
- });
269
- it("saves drift summary to disk", async () => {
270
- const graph = createMockGraph();
271
- const fileHashManager = new FileHashManager(unerrDir);
272
- const tracker = new DriftTracker({ projectRoot, repoId: "repo1", unerrDir }, graph, fileHashManager);
273
- writeFileSync(join(projectRoot, "src/a.ts"), "export function alpha() { return 1 }");
274
- await tracker.processFiles(["src/a.ts"], "abc123");
275
- const summaryPath = join(unerrDir, "drift", "drift_summary.json");
276
- expect(existsSync(summaryPath)).toBe(true);
277
- });
278
- it("mtime cache skips file when mtime unchanged", async () => {
279
- const graph = createMockGraph();
280
- const fileHashManager = new FileHashManager(unerrDir);
281
- const tracker = new DriftTracker({ projectRoot, repoId: "repo1", unerrDir }, graph, fileHashManager);
282
- const filePath = join(projectRoot, "src/stable.ts");
283
- writeFileSync(filePath, "export function stable() { return 1 }");
284
- // First call — processes the file (mtime is new)
285
- const r1 = await tracker.processFile("src/stable.ts", "abc123");
286
- expect(r1.filesProcessed).toBe(1);
287
- // Second call — mtime unchanged, should skip via mtime cache
288
- const r2 = await tracker.processFile("src/stable.ts", "abc123");
289
- expect(r2.filesSkipped).toBe(1);
290
- expect(r2.filesProcessed).toBe(0);
291
- });
292
- it("mtime cache processes file when content changes", async () => {
293
- const graph = createMockGraph();
294
- const fileHashManager = new FileHashManager(unerrDir);
295
- const tracker = new DriftTracker({ projectRoot, repoId: "repo1", unerrDir }, graph, fileHashManager);
296
- const filePath = join(projectRoot, "src/changing.ts");
297
- writeFileSync(filePath, "export function changing() { return 1 }");
298
- // First call — processes
299
- await tracker.processFile("src/changing.ts", "abc123");
300
- // Modify file — mtime changes
301
- writeFileSync(filePath, "export function changing() { return 2 }");
302
- // Second call — mtime changed, should process
303
- const r2 = await tracker.processFile("src/changing.ts", "abc123");
304
- expect(r2.filesProcessed).toBe(1);
305
- });
306
- it("mtime cache clears on branch switch", async () => {
307
- const graph = createMockGraph();
308
- const fileHashManager = new FileHashManager(unerrDir);
309
- const tracker = new DriftTracker({ projectRoot, repoId: "repo1", unerrDir }, graph, fileHashManager);
310
- const filePath = join(projectRoot, "src/branched.ts");
311
- writeFileSync(filePath, "export function branched() { return 1 }");
312
- // Process once to populate mtime cache
313
- await tracker.processFile("src/branched.ts", "abc123");
314
- // Branch switch clears mtime cache
315
- await tracker.onBranchSwitch(["src/branched.ts"], "def456");
316
- // After branch switch, same file should be reprocessed (mtime cache was cleared)
317
- const r = await tracker.processFile("src/branched.ts", "def456");
318
- // Will be processed (mtime cache cleared) but may be skipped by file hash manager
319
- // since content hasn't changed. The point is mtime cache doesn't block it.
320
- expect(r.filesProcessed + r.filesSkipped).toBe(1);
321
- });
322
- });
323
- describe("MtimeCache", () => {
324
- it("returns true for first check (new file)", () => {
325
- const cache = new MtimeCache();
326
- const filePath = join(projectRoot, "src/first.ts");
327
- writeFileSync(filePath, "content");
328
- expect(cache.check(filePath)).toBe(true);
329
- });
330
- it("returns false for unchanged file", () => {
331
- const cache = new MtimeCache();
332
- const filePath = join(projectRoot, "src/unchanged.ts");
333
- writeFileSync(filePath, "content");
334
- cache.check(filePath); // populate
335
- expect(cache.check(filePath)).toBe(false);
336
- });
337
- it("returns true after file modification", () => {
338
- const cache = new MtimeCache();
339
- const filePath = join(projectRoot, "src/modified.ts");
340
- writeFileSync(filePath, "content v1");
341
- cache.check(filePath); // populate
342
- // Modify — mtime changes
343
- writeFileSync(filePath, "content v2");
344
- expect(cache.check(filePath)).toBe(true);
345
- });
346
- it("returns true for non-existent file and evicts cache", () => {
347
- const cache = new MtimeCache();
348
- expect(cache.check("/nonexistent/file.ts")).toBe(true);
349
- expect(cache.size).toBe(0);
350
- });
351
- it("evict removes file from cache", () => {
352
- const cache = new MtimeCache();
353
- const filePath = join(projectRoot, "src/evicted.ts");
354
- writeFileSync(filePath, "content");
355
- cache.check(filePath); // populate
356
- expect(cache.size).toBe(1);
357
- cache.evict(filePath);
358
- expect(cache.size).toBe(0);
359
- // Next check should return true (new entry)
360
- expect(cache.check(filePath)).toBe(true);
361
- });
362
- it("clear removes all entries", () => {
363
- const cache = new MtimeCache();
364
- const file1 = join(projectRoot, "src/a.ts");
365
- const file2 = join(projectRoot, "src/b.ts");
366
- writeFileSync(file1, "a");
367
- writeFileSync(file2, "b");
368
- cache.check(file1);
369
- cache.check(file2);
370
- expect(cache.size).toBe(2);
371
- cache.clear();
372
- expect(cache.size).toBe(0);
373
- });
374
- });
375
- describe("DRIFT_WATCHER_OPTIONS", () => {
376
- it("has awaitWriteFinish with stabilityThreshold 300ms", () => {
377
- expect(DRIFT_WATCHER_OPTIONS.awaitWriteFinish.stabilityThreshold).toBe(300);
378
- expect(DRIFT_WATCHER_OPTIONS.awaitWriteFinish.pollInterval).toBe(100);
379
- });
380
- });
381
- describe("determineOrigin", () => {
382
- it("returns 'human' when lastSyncTimestamp is 0 (no sync)", () => {
383
- expect(determineOrigin(0)).toBe("human");
384
- });
385
- it("returns 'ai' when <10s after sync", () => {
386
- const recentSync = Date.now() - 5_000; // 5s ago
387
- expect(determineOrigin(recentSync)).toBe("ai");
388
- });
389
- it("returns 'mixed' when 10-60s after sync", () => {
390
- const mediumSync = Date.now() - 30_000; // 30s ago
391
- expect(determineOrigin(mediumSync)).toBe("mixed");
392
- });
393
- it("returns 'human' when >60s after sync", () => {
394
- const oldSync = Date.now() - 120_000; // 2min ago
395
- expect(determineOrigin(oldSync)).toBe("human");
396
- });
397
- it("returns 'ai' at exactly 0ms after sync", () => {
398
- const justNow = Date.now();
399
- expect(determineOrigin(justNow)).toBe("ai");
400
- });
401
- });
402
- describe("DriftTracker origin attribution", () => {
403
- it("sets origin on added entities based on sync timestamp", async () => {
404
- const graph = createMockGraph();
405
- const fileHashManager = new FileHashManager(unerrDir);
406
- const tracker = new DriftTracker({ projectRoot, repoId: "repo1", unerrDir }, graph, fileHashManager);
407
- // Simulate recent AI sync
408
- tracker.setLastSyncTimestamp(Date.now() - 3_000);
409
- writeFileSync(join(projectRoot, "src/ai-created.ts"), "export function aiCreated() { return 1 }");
410
- await tracker.processFile("src/ai-created.ts", "abc123");
411
- const entity = [...graph.driftOverlay.values()][0];
412
- expect(entity?.origin).toBe("ai");
413
- });
414
- it("sets origin to 'human' when no sync has occurred", async () => {
415
- const graph = createMockGraph();
416
- const fileHashManager = new FileHashManager(unerrDir);
417
- const tracker = new DriftTracker({ projectRoot, repoId: "repo1", unerrDir }, graph, fileHashManager);
418
- // No setLastSyncTimestamp call — default is 0
419
- writeFileSync(join(projectRoot, "src/human-created.ts"), "export function humanCreated() { return 1 }");
420
- await tracker.processFile("src/human-created.ts", "abc123");
421
- const entity = [...graph.driftOverlay.values()][0];
422
- expect(entity?.origin).toBe("human");
423
- });
424
- it("sets origin on deleted entities", async () => {
425
- const baseEntities = [
426
- {
427
- key: "del-origin-key",
428
- kind: "function",
429
- name: "aboutToDelete",
430
- file_path: "src/will-delete.ts",
431
- start_line: 1,
432
- end_line: 0,
433
- signature: "",
434
- body: "",
435
- fan_in: 0,
436
- fan_out: 0,
437
- risk_level: "normal",
438
- community: -1,
439
- },
440
- ];
441
- const graph = createMockGraph(baseEntities);
442
- const fileHashManager = new FileHashManager(unerrDir);
443
- const tracker = new DriftTracker({ projectRoot, repoId: "repo1", unerrDir }, graph, fileHashManager);
444
- // Simulate mixed timing
445
- tracker.setLastSyncTimestamp(Date.now() - 25_000);
446
- // File doesn't exist — deleted
447
- await tracker.processFile("src/will-delete.ts", "abc123");
448
- const entity = [...graph.driftOverlay.values()][0];
449
- expect(entity?.origin).toBe("mixed");
450
- expect(entity?.drift_status).toBe("deleted");
451
- });
452
- });
453
- describe("Cross-file drift invalidation", () => {
454
- const repoId = "repo1";
455
- /** Build a base entity with a computable key */
456
- function makeBaseEntity(filePath, name, kind, body) {
457
- const key = entityKey(repoId, filePath, kind, name, "()");
458
- return {
459
- key,
460
- kind,
461
- name,
462
- file_path: filePath,
463
- start_line: 1,
464
- end_line: 0,
465
- signature: "()",
466
- body,
467
- fan_in: 0,
468
- fan_out: 0,
469
- risk_level: "normal",
470
- community: -1,
471
- };
472
- }
473
- it("propagates dependency_changed to callers in other files", async () => {
474
- // File A has function `helperFn`, File B has function `callerFn` that calls it
475
- const helperEntity = makeBaseEntity("src/helper.ts", "helperFn", "function", "function helperFn() {\n return 1\n}");
476
- const callerEntity = makeBaseEntity("src/caller.ts", "callerFn", "function", "function callerFn() {\n return helperFn()\n}");
477
- const callerMap = new Map();
478
- callerMap.set(helperEntity.key, [callerEntity]);
479
- const graph = createMockGraphWithCallers([helperEntity, callerEntity], callerMap);
480
- const fileHashManager = new FileHashManager(unerrDir);
481
- const tracker = new DriftTracker({ projectRoot, repoId, unerrDir }, graph, fileHashManager);
482
- // Write modified helper (body changed)
483
- writeFileSync(join(projectRoot, "src/helper.ts"), "export function helperFn() {\n return 999\n}");
484
- // Write caller file (unchanged, but needs to exist)
485
- writeFileSync(join(projectRoot, "src/caller.ts"), "export function callerFn() {\n return helperFn()\n}");
486
- const result = await tracker.processFile("src/helper.ts", "abc123");
487
- expect(result.crossFileInvalidated).toBe(1);
488
- // callerFn should have dependency_changed in the overlay
489
- const callerDrift = graph.driftOverlay.get(callerEntity.key);
490
- expect(callerDrift).toBeDefined();
491
- expect(callerDrift?.drift_status).toBe("dependency_changed");
492
- expect(callerDrift?.file_path).toBe("src/caller.ts");
493
- });
494
- it("does NOT invalidate callers in the same file", async () => {
495
- // Both entities in the same file
496
- const entity1 = makeBaseEntity("src/same-file.ts", "baseFunc", "function", "function baseFunc() {\n return 1\n}");
497
- const entity2 = makeBaseEntity("src/same-file.ts", "callerFunc", "function", "function callerFunc() {\n return baseFunc()\n}");
498
- const callerMap = new Map();
499
- callerMap.set(entity1.key, [entity2]); // callerFunc calls baseFunc, same file
500
- const graph = createMockGraphWithCallers([entity1, entity2], callerMap);
501
- const fileHashManager = new FileHashManager(unerrDir);
502
- const tracker = new DriftTracker({ projectRoot, repoId, unerrDir }, graph, fileHashManager);
503
- writeFileSync(join(projectRoot, "src/same-file.ts"), "export function baseFunc() {\n return 999\n}\nexport function callerFunc() {\n return baseFunc()\n}");
504
- const result = await tracker.processFile("src/same-file.ts", "abc123");
505
- // Same-file callers should NOT be cross-file invalidated
506
- expect(result.crossFileInvalidated).toBe(0);
507
- });
508
- it("does NOT overwrite stronger drift status with dependency_changed", async () => {
509
- const helperEntity = makeBaseEntity("src/dep.ts", "depFn", "function", "function depFn() {\n return 1\n}");
510
- const callerEntity = makeBaseEntity("src/consumer.ts", "consumerFn", "function", "function consumerFn() {\n return depFn()\n}");
511
- const callerMap = new Map();
512
- callerMap.set(helperEntity.key, [callerEntity]);
513
- const graph = createMockGraphWithCallers([helperEntity, callerEntity], callerMap);
514
- const fileHashManager = new FileHashManager(unerrDir);
515
- const tracker = new DriftTracker({ projectRoot, repoId, unerrDir }, graph, fileHashManager);
516
- // Pre-populate overlay with a "modified" status for the caller
517
- graph.driftOverlay.set(callerEntity.key, {
518
- key: callerEntity.key,
519
- name: "consumerFn",
520
- kind: "function",
521
- signature: "()",
522
- body: "function consumerFn() { return depFn() }",
523
- file_path: "src/consumer.ts",
524
- line_start: 1,
525
- line_end: 3,
526
- content_hash: "abc",
527
- drift_status: "modified",
528
- intent_id: "",
529
- modified_at: new Date().toISOString(),
530
- origin: "human",
531
- previous_body: "",
532
- previous_signature: "",
533
- });
534
- // Modify the dependency
535
- writeFileSync(join(projectRoot, "src/dep.ts"), "export function depFn() {\n return 999\n}");
536
- const result = await tracker.processFile("src/dep.ts", "abc123");
537
- // Should NOT overwrite the "modified" status
538
- expect(result.crossFileInvalidated).toBe(0);
539
- const callerDrift = graph.driftOverlay.get(callerEntity.key);
540
- expect(callerDrift?.drift_status).toBe("modified");
541
- });
542
- it("propagates dependency_changed when entity is deleted", async () => {
543
- const deletedEntity = makeBaseEntity("src/removed.ts", "removedFn", "function", "function removedFn() {\n return 1\n}");
544
- const callerEntity = makeBaseEntity("src/uses-removed.ts", "usesFn", "function", "function usesFn() {\n return removedFn()\n}");
545
- const callerMap = new Map();
546
- callerMap.set(deletedEntity.key, [callerEntity]);
547
- const graph = createMockGraphWithCallers([deletedEntity, callerEntity], callerMap);
548
- const fileHashManager = new FileHashManager(unerrDir);
549
- const tracker = new DriftTracker({ projectRoot, repoId, unerrDir }, graph, fileHashManager);
550
- // Write the caller file (exists), but don't write removed.ts (deleted)
551
- writeFileSync(join(projectRoot, "src/uses-removed.ts"), "export function usesFn() {\n return removedFn()\n}");
552
- // Process the deleted file — entities should propagate dependency_changed
553
- const result = await tracker.processFile("src/removed.ts", "abc123");
554
- expect(result.entitiesDeleted).toBe(1);
555
- // The caller in another file should get dependency_changed
556
- const callerDrift = graph.driftOverlay.get(callerEntity.key);
557
- expect(callerDrift).toBeDefined();
558
- expect(callerDrift?.drift_status).toBe("dependency_changed");
559
- });
560
- it("counts crossFileInvalidated correctly in batch", async () => {
561
- const entityA = makeBaseEntity("src/a.ts", "funcA", "function", "function funcA() {\n return 1\n}");
562
- const callerB = makeBaseEntity("src/b.ts", "funcB", "function", "function funcB() {\n return funcA()\n}");
563
- const callerC = makeBaseEntity("src/c.ts", "funcC", "function", "function funcC() {\n return funcA()\n}");
564
- const callerMap = new Map();
565
- callerMap.set(entityA.key, [callerB, callerC]);
566
- const graph = createMockGraphWithCallers([entityA, callerB, callerC], callerMap);
567
- const fileHashManager = new FileHashManager(unerrDir);
568
- const tracker = new DriftTracker({ projectRoot, repoId, unerrDir }, graph, fileHashManager);
569
- // Modify source entity
570
- writeFileSync(join(projectRoot, "src/a.ts"), "export function funcA() {\n return 999\n}");
571
- writeFileSync(join(projectRoot, "src/b.ts"), "export function funcB() {\n return funcA()\n}");
572
- writeFileSync(join(projectRoot, "src/c.ts"), "export function funcC() {\n return funcA()\n}");
573
- const result = await tracker.processFiles(["src/a.ts"], "abc123");
574
- expect(result.crossFileInvalidated).toBe(2);
575
- // Both callers should be in the overlay
576
- expect(graph.driftOverlay.get(callerB.key)?.drift_status).toBe("dependency_changed");
577
- expect(graph.driftOverlay.get(callerC.key)?.drift_status).toBe("dependency_changed");
578
- // Summary should reflect it
579
- const summary = await tracker.getDriftSummary();
580
- expect(summary.dependency_changed).toBe(2);
581
- });
582
- });