@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,111 +0,0 @@
1
- import * as fs from "node:fs";
2
- import * as os from "node:os";
3
- import * as path from "node:path";
4
- import { afterEach, beforeEach, describe, expect, it } from "vitest";
5
- describe("Ephemeral Sandbox (P5.6-ADV-02)", () => {
6
- let tmpDir;
7
- beforeEach(() => {
8
- tmpDir = path.join(os.tmpdir(), `unerr-ephemeral-${Date.now()}`);
9
- fs.mkdirSync(tmpDir, { recursive: true });
10
- });
11
- afterEach(() => {
12
- fs.rmSync(tmpDir, { recursive: true, force: true });
13
- });
14
- it("connect --ephemeral creates ephemeral repo config", () => {
15
- // Simulate ephemeral repo creation: a config with ephemeral: true and a TTL
16
- const unerrDir = path.join(tmpDir, ".unerr");
17
- fs.mkdirSync(unerrDir, { recursive: true });
18
- const ttlHours = 24;
19
- const expiresAt = new Date(Date.now() + ttlHours * 60 * 60 * 1000).toISOString();
20
- const ephemeralConfig = {
21
- repoId: "eph-repo-123",
22
- orgId: "org-1",
23
- branch: "main",
24
- ephemeral: true,
25
- expiresAt,
26
- };
27
- fs.writeFileSync(path.join(unerrDir, "config.json"), `${JSON.stringify(ephemeralConfig, null, 2)}\n`);
28
- const raw = fs.readFileSync(path.join(unerrDir, "config.json"), "utf-8");
29
- const config = JSON.parse(raw);
30
- expect(config.ephemeral).toBe(true);
31
- expect(config.repoId).toBe("eph-repo-123");
32
- expect(new Date(config.expiresAt).getTime()).toBeGreaterThan(Date.now());
33
- });
34
- it("promote converts ephemeral to permanent config", () => {
35
- const unerrDir = path.join(tmpDir, ".unerr");
36
- fs.mkdirSync(unerrDir, { recursive: true });
37
- // Start with ephemeral config
38
- const ephemeralConfig = {
39
- repoId: "eph-repo-456",
40
- orgId: "org-1",
41
- branch: "main",
42
- ephemeral: true,
43
- expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(),
44
- };
45
- const configPath = path.join(unerrDir, "config.json");
46
- fs.writeFileSync(configPath, JSON.stringify(ephemeralConfig, null, 2));
47
- // Simulate "promote" — remove ephemeral flag and expiry
48
- const raw = fs.readFileSync(configPath, "utf-8");
49
- const config = JSON.parse(raw);
50
- config.ephemeral = undefined;
51
- config.expiresAt = undefined;
52
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
53
- const promoted = JSON.parse(fs.readFileSync(configPath, "utf-8"));
54
- expect(promoted.repoId).toBe("eph-repo-456");
55
- expect(promoted.ephemeral).toBeUndefined();
56
- expect(promoted.expiresAt).toBeUndefined();
57
- });
58
- it("ephemeral repo has TTL that can be checked", () => {
59
- const ttlHours = 2;
60
- const expiresAt = new Date(Date.now() + ttlHours * 60 * 60 * 1000);
61
- // Check if expired
62
- const isExpired = expiresAt.getTime() < Date.now();
63
- expect(isExpired).toBe(false);
64
- // Simulate an already-expired ephemeral repo
65
- const pastExpiry = new Date(Date.now() - 1000);
66
- const isPastExpired = pastExpiry.getTime() < Date.now();
67
- expect(isPastExpired).toBe(true);
68
- });
69
- it("ephemeral config includes all required fields", () => {
70
- const ephemeralConfig = {
71
- repoId: "eph-repo-789",
72
- orgId: "org-1",
73
- branch: "main",
74
- ephemeral: true,
75
- expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(),
76
- };
77
- // Validate required fields
78
- expect(ephemeralConfig.repoId).toBeTruthy();
79
- expect(ephemeralConfig.orgId).toBeTruthy();
80
- expect(ephemeralConfig.branch).toBeTruthy();
81
- expect(ephemeralConfig.ephemeral).toBe(true);
82
- expect(ephemeralConfig.expiresAt).toBeTruthy();
83
- });
84
- it("expired ephemeral repos are detectable for cleanup", () => {
85
- // Simulate multiple ephemeral configs, some expired
86
- const configs = [
87
- {
88
- repoId: "eph-1",
89
- expiresAt: new Date(Date.now() + 1000).toISOString(),
90
- ephemeral: true,
91
- },
92
- {
93
- repoId: "eph-2",
94
- expiresAt: new Date(Date.now() - 1000).toISOString(),
95
- ephemeral: true,
96
- },
97
- {
98
- repoId: "eph-3",
99
- expiresAt: new Date(Date.now() - 5000).toISOString(),
100
- ephemeral: true,
101
- },
102
- { repoId: "perm-1", ephemeral: false, expiresAt: undefined },
103
- ];
104
- const expired = configs.filter((c) => c.ephemeral &&
105
- c.expiresAt &&
106
- new Date(c.expiresAt).getTime() < Date.now());
107
- expect(expired.length).toBe(2);
108
- expect(expired.map((c) => c.repoId)).toContain("eph-2");
109
- expect(expired.map((c) => c.repoId)).toContain("eph-3");
110
- });
111
- });
@@ -1,93 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { estimateExplorationCost } from "../intelligence/exploration-cost.js";
3
- describe("exploration-cost — MCP tool alias resolution", () => {
4
- it("get_references resolves to find_callers rule (not DEFAULT_RULE)", () => {
5
- const aliased = estimateExplorationCost("get_references", 5);
6
- const target = estimateExplorationCost("find_callers", 5);
7
- expect(aliased.tokensWithout).toBe(target.tokensWithout);
8
- expect(aliased.counterfactualMethod).toBe(target.counterfactualMethod);
9
- });
10
- it("search_code resolves to search_entities rule", () => {
11
- const aliased = estimateExplorationCost("search_code", 8);
12
- const target = estimateExplorationCost("search_entities", 8);
13
- expect(aliased.tokensWithout).toBe(target.tokensWithout);
14
- expect(aliased.counterfactualMethod).toBe(target.counterfactualMethod);
15
- });
16
- it("get_conventions resolves to show_conventions rule", () => {
17
- const aliased = estimateExplorationCost("get_conventions", 0, 12);
18
- const target = estimateExplorationCost("show_conventions", 0, 12);
19
- expect(aliased.tokensWithout).toBe(target.tokensWithout);
20
- });
21
- it("get_critical_nodes resolves to risk_assessment rule", () => {
22
- const aliased = estimateExplorationCost("get_critical_nodes", 4);
23
- const target = estimateExplorationCost("risk_assessment", 4);
24
- expect(aliased.tokensWithout).toBe(target.tokensWithout);
25
- });
26
- it("get_cross_boundary_links resolves to risk_assessment rule", () => {
27
- const aliased = estimateExplorationCost("get_cross_boundary_links", 6);
28
- const target = estimateExplorationCost("risk_assessment", 6);
29
- expect(aliased.tokensWithout).toBe(target.tokensWithout);
30
- });
31
- it("file_connections resolves to risk_assessment rule", () => {
32
- const aliased = estimateExplorationCost("file_connections", 3);
33
- const target = estimateExplorationCost("risk_assessment", 3);
34
- expect(aliased.tokensWithout).toBe(target.tokensWithout);
35
- });
36
- it("file_outline falls through to DEFAULT_RULE (returns N entities, not 1)", () => {
37
- // Intentionally NOT aliased: get_entity assumes 1 returned entity and yields
38
- // negative savings as resultSize grows. DEFAULT_RULE scales positively with N.
39
- const result = estimateExplorationCost("file_outline", 15);
40
- // DEFAULT_RULE: 150 + ceil(15/2)*400 = 150 + 8*400 = 3350
41
- expect(result.tokensWithout).toBe(150 + 8 * 400);
42
- expect(result.counterfactualMethod).toBe("generic file exploration");
43
- });
44
- it("get_imports resolves to find_callers rule", () => {
45
- const aliased = estimateExplorationCost("get_imports", 10);
46
- const target = estimateExplorationCost("find_callers", 10);
47
- expect(aliased.tokensWithout).toBe(target.tokensWithout);
48
- });
49
- it("get_test_coverage resolves to find_callers rule", () => {
50
- const aliased = estimateExplorationCost("get_test_coverage", 7);
51
- const target = estimateExplorationCost("find_callers", 7);
52
- expect(aliased.tokensWithout).toBe(target.tokensWithout);
53
- });
54
- it("get_file resolves to get_entity rule", () => {
55
- const aliased = estimateExplorationCost("get_file", 1);
56
- const target = estimateExplorationCost("get_entity", 1);
57
- expect(aliased.tokensWithout).toBe(target.tokensWithout);
58
- });
59
- it("file_read falls through to DEFAULT_RULE (has its own dedicated mechanism)", () => {
60
- // file_read is tracked via mechanism="file_read" with full-file counterfactual.
61
- // Letting it also alias graph_query would double-count. Keep it on DEFAULT_RULE
62
- // so graph_query events fire (when positive) without inheriting get_entity math.
63
- const result = estimateExplorationCost("file_read", 4);
64
- // DEFAULT_RULE: 150 + ceil(4/2)*400 = 150 + 2*400 = 950
65
- expect(result.tokensWithout).toBe(150 + 2 * 400);
66
- expect(result.counterfactualMethod).toBe("generic file exploration");
67
- });
68
- it("get_project_stats resolves to health_grade rule", () => {
69
- const aliased = estimateExplorationCost("get_project_stats", 0, 50);
70
- const target = estimateExplorationCost("health_grade", 0, 50);
71
- expect(aliased.tokensWithout).toBe(target.tokensWithout);
72
- });
73
- it("legacy keys still resolve directly (not regressed by alias layer)", () => {
74
- // blast_radius and find_callers are still used internally — must remain stable
75
- const blast = estimateExplorationCost("blast_radius", 4);
76
- expect(blast.counterfactualMethod).toContain("read each caller file");
77
- const callers = estimateExplorationCost("find_callers", 4);
78
- expect(callers.counterfactualMethod).toContain("grep + read");
79
- });
80
- it("unknown tool name still falls through to DEFAULT_RULE", () => {
81
- const result = estimateExplorationCost("totally_unknown_tool", 4);
82
- // DEFAULT_RULE: fileMultiplier = ceil(resultSize / 2) = 2, tokensPerFile = 400, baseTokens = 150
83
- expect(result.tokensWithout).toBe(150 + 2 * 400);
84
- expect(result.counterfactualMethod).toBe("generic file exploration");
85
- });
86
- it("aliased tools beat DEFAULT_RULE on at least one shape", () => {
87
- // search_code with 10 results: search_entities gives 200 + min(10,20)*500 = 5200
88
- // DEFAULT_RULE would give 150 + ceil(10/2)*400 = 2150
89
- // Confirms the alias is actually changing the calculation
90
- const aliased = estimateExplorationCost("search_code", 10);
91
- expect(aliased.tokensWithout).toBe(200 + 10 * 500);
92
- });
93
- });
@@ -1,197 +0,0 @@
1
- import { mkdirSync, rmSync, writeFileSync } from "node:fs";
2
- import { tmpdir } from "node:os";
3
- import { join } from "node:path";
4
- import { afterEach, beforeEach, describe, expect, it } from "vitest";
5
- import { generateFromCausalBridge, generateFromConventions, generateFromNegativeKnowledge, generateFromSessionAnalysis, } from "../intelligence/fact-generator.js";
6
- import { initFactsSchema } from "../intelligence/facts-schema.js";
7
- import { TemporalFactStore } from "../intelligence/temporal-facts.js";
8
- async function createTestDb() {
9
- const cozoModule = await import("cozo-node");
10
- const CozoDbConstructor = cozoModule.default
11
- ? cozoModule.default.CozoDb
12
- : cozoModule.CozoDb;
13
- return new CozoDbConstructor("mem", "");
14
- }
15
- function makeSessionRecord(overrides = {}) {
16
- return {
17
- session_id: `sess-${Math.random().toString(36).slice(2, 8)}`,
18
- written_at: new Date().toISOString(),
19
- started_at: new Date(Date.now() - 3600000).toISOString(),
20
- ended_at: new Date().toISOString(),
21
- duration_ms: 3600000,
22
- tool_calls: 20,
23
- chains: 5,
24
- files_modified: ["src/proxy.ts", "src/auth.ts"],
25
- entities_touched: ["src/proxy.ts::startProxy"],
26
- tools_used: { get_function: 10, file_read: 10 },
27
- feature_areas: ["src/proxy"],
28
- facts_recorded: 0,
29
- facts_surfaced: [],
30
- revert_count: 0,
31
- rot_score: 0.1,
32
- token_estimate: 10000,
33
- branch: "main",
34
- ...overrides,
35
- };
36
- }
37
- describe("fact-generator", () => {
38
- let db;
39
- let store;
40
- let testDir;
41
- beforeEach(async () => {
42
- db = await createTestDb();
43
- await initFactsSchema(db);
44
- store = TemporalFactStore.fromDb(db);
45
- testDir = join(tmpdir(), `unerr-factgen-${Date.now()}`);
46
- mkdirSync(testDir, { recursive: true });
47
- });
48
- afterEach(() => {
49
- try {
50
- rmSync(testDir, { recursive: true, force: true });
51
- }
52
- catch {
53
- // best-effort
54
- }
55
- });
56
- describe("generateFromConventions", () => {
57
- it("creates facts for conventions with >70% adherence", async () => {
58
- const conventions = [
59
- {
60
- key: "conv-1",
61
- kind: "naming",
62
- name: "camelCase functions",
63
- detail: "^[a-z][a-zA-Z]*$",
64
- exemplarKeys: ["src/proxy/startProxy.ts"],
65
- frequency: 20,
66
- confidence: 0.85,
67
- },
68
- ];
69
- const result = await generateFromConventions(store, conventions);
70
- expect(result.created).toBe(1);
71
- expect(result.source).toBe("convention_detector");
72
- expect(result.details[0]).toContain("camelCase functions");
73
- });
74
- it("skips conventions below 70% adherence", async () => {
75
- const conventions = [
76
- {
77
- key: "conv-low",
78
- kind: "naming",
79
- name: "snake_case",
80
- detail: ".*",
81
- exemplarKeys: [],
82
- frequency: 10,
83
- confidence: 0.5,
84
- },
85
- ];
86
- const result = await generateFromConventions(store, conventions);
87
- expect(result.created).toBe(0);
88
- });
89
- it("skips conventions with too few entities", async () => {
90
- const conventions = [
91
- {
92
- key: "conv-few",
93
- kind: "structure",
94
- name: "rare pattern",
95
- detail: ".*",
96
- exemplarKeys: [],
97
- frequency: 2,
98
- confidence: 0.95,
99
- },
100
- ];
101
- const result = await generateFromConventions(store, conventions);
102
- expect(result.created).toBe(0);
103
- });
104
- });
105
- describe("generateFromNegativeKnowledge", () => {
106
- it("creates negative facts from correction entries", async () => {
107
- const corrections = [
108
- {
109
- id: "corr-1",
110
- entityKey: "src/auth.ts",
111
- pattern: "modified-then-reverted",
112
- reason: "File src/auth.ts was modified and reverted. The approach was incorrect.",
113
- detectedAt: new Date().toISOString(),
114
- rewindEntryId: "rewind-001",
115
- confidence: 0.7,
116
- },
117
- ];
118
- const result = await generateFromNegativeKnowledge(store, corrections);
119
- expect(result.created).toBe(1);
120
- expect(result.source).toBe("negative_knowledge");
121
- const recalled = await store.recallByScope("src/auth.ts", 0);
122
- expect(recalled.length).toBe(1);
123
- expect(recalled[0].fact_type).toBe("negative");
124
- });
125
- });
126
- describe("generateFromCausalBridge", () => {
127
- it("creates episodic facts for survived changes", async () => {
128
- const events = [
129
- {
130
- session_id: "sess-1",
131
- entity_key: "src/proxy.ts::startProxy",
132
- action: "survived",
133
- branch: "main",
134
- timestamp: Date.now() - 86400000,
135
- },
136
- ];
137
- const result = await generateFromCausalBridge(store, events);
138
- expect(result.created).toBe(1);
139
- expect(result.details[0]).toContain("survived");
140
- });
141
- it("creates negative facts for reverted changes", async () => {
142
- const events = [
143
- {
144
- session_id: "sess-2",
145
- entity_key: "src/config.ts",
146
- action: "reverted",
147
- branch: "main",
148
- timestamp: Date.now() - 86400000,
149
- },
150
- ];
151
- const result = await generateFromCausalBridge(store, events);
152
- expect(result.created).toBe(1);
153
- expect(result.details[0]).toContain("reverted");
154
- const recalled = await store.recallByScope("src/config.ts", 0);
155
- expect(recalled[0].fact_type).toBe("negative");
156
- });
157
- });
158
- describe("generateFromSessionAnalysis", () => {
159
- it("detects hot files from multiple sessions", async () => {
160
- const sessionsDir = join(testDir, "sessions");
161
- mkdirSync(sessionsDir, { recursive: true });
162
- for (let i = 0; i < 5; i++) {
163
- const record = makeSessionRecord({
164
- session_id: `s${i}`,
165
- files_modified: ["src/proxy.ts", "src/auth.ts", `src/other${i}.ts`],
166
- });
167
- writeFileSync(join(sessionsDir, `s${i}.jsonl`), `${JSON.stringify(record)}\n`, "utf-8");
168
- }
169
- const result = await generateFromSessionAnalysis(store, testDir);
170
- expect(result.created).toBeGreaterThan(0);
171
- expect(result.details.some((d) => d.includes("src/proxy.ts"))).toBe(true);
172
- });
173
- it("detects high-revert files", async () => {
174
- const sessionsDir = join(testDir, "sessions");
175
- mkdirSync(sessionsDir, { recursive: true });
176
- for (let i = 0; i < 5; i++) {
177
- const record = makeSessionRecord({
178
- session_id: `s${i}`,
179
- files_modified: ["src/fragile.ts"],
180
- revert_count: i < 3 ? 1 : 0, // 3 out of 5 sessions have reverts
181
- });
182
- writeFileSync(join(sessionsDir, `s${i}.jsonl`), `${JSON.stringify(record)}\n`, "utf-8");
183
- }
184
- const result = await generateFromSessionAnalysis(store, testDir);
185
- const fragileDetail = result.details.find((d) => d.includes("src/fragile.ts"));
186
- expect(fragileDetail).toBeDefined();
187
- });
188
- it("returns empty when fewer than 3 sessions", async () => {
189
- const sessionsDir = join(testDir, "sessions");
190
- mkdirSync(sessionsDir, { recursive: true });
191
- const record = makeSessionRecord();
192
- writeFileSync(join(sessionsDir, "s1.jsonl"), `${JSON.stringify(record)}\n`, "utf-8");
193
- const result = await generateFromSessionAnalysis(store, testDir);
194
- expect(result.created).toBe(0);
195
- });
196
- });
197
- });
@@ -1,244 +0,0 @@
1
- /**
2
- * Sprint R.10: File-as-L0 Graph Enrichment Tests
3
- *
4
- * Verifies:
5
- * - File entities created for every indexed file
6
- * - Contains edges from file→entity guarantee zero orphans
7
- * - File→file import edges created from cross-file resolution
8
- * - Co-change edge computation from git history
9
- * - Community detection handles weighted contains edges
10
- * - Blast radius falls back to file siblings when no callers exist
11
- * - File-level queries (getFileEntities, getFileNeighbors) work correctly
12
- */
13
- import { describe, expect, it } from "vitest";
14
- import { resolveCrossFileEdges } from "../intelligence/indexer/cross-file-resolver.js";
15
- import { computeCoChangeEdges } from "../intelligence/indexer/git-cochange.js";
16
- import { resolveImportSourceToFile } from "../intelligence/local-indexer.js";
17
- describe("Sprint R: File-as-L0 Graph Enrichment", () => {
18
- describe("R.3: Cross-file resolver emits file import edges", () => {
19
- it("produces file→file import edges alongside entity resolution", () => {
20
- const fileResults = new Map([
21
- [
22
- "src/a.ts",
23
- {
24
- entities: [
25
- {
26
- key: "src/a.ts::fnA",
27
- name: "fnA",
28
- exported: true,
29
- kind: "function",
30
- file_path: "src/a.ts",
31
- },
32
- ],
33
- edges: [
34
- {
35
- from_key: "src/a.ts::fnA",
36
- to_key: "unresolved:fnB",
37
- type: "calls",
38
- file_path: "src/a.ts",
39
- line: 5,
40
- },
41
- ],
42
- imports: [
43
- {
44
- source: "src/b.ts",
45
- symbols: ["fnB"],
46
- isDefault: false,
47
- isNamespace: false,
48
- localName: undefined,
49
- line: 1,
50
- },
51
- ],
52
- },
53
- ],
54
- [
55
- "src/b.ts",
56
- {
57
- entities: [
58
- {
59
- key: "src/b.ts::fnB",
60
- name: "fnB",
61
- exported: true,
62
- kind: "function",
63
- file_path: "src/b.ts",
64
- },
65
- ],
66
- edges: [],
67
- imports: [],
68
- },
69
- ],
70
- ]);
71
- const result = resolveCrossFileEdges(fileResults);
72
- // Should have file→file import edges
73
- expect(result.fileImportEdges.length).toBeGreaterThan(0);
74
- const fileEdge = result.fileImportEdges.find((e) => e.from_key === "file:src/a.ts" && e.to_key === "file:src/b.ts");
75
- expect(fileEdge).toBeDefined();
76
- expect(fileEdge.type).toBe("imports");
77
- });
78
- it("deduplicates file import edges (multiple imports from same file)", () => {
79
- const fileResults = new Map([
80
- [
81
- "src/a.ts",
82
- {
83
- entities: [
84
- {
85
- key: "src/a.ts::fnA",
86
- name: "fnA",
87
- exported: true,
88
- kind: "function",
89
- file_path: "src/a.ts",
90
- },
91
- ],
92
- edges: [],
93
- imports: [
94
- {
95
- source: "src/b.ts",
96
- symbols: ["fnB"],
97
- isDefault: false,
98
- isNamespace: false,
99
- localName: undefined,
100
- line: 1,
101
- },
102
- {
103
- source: "src/b.ts",
104
- symbols: ["fnC"],
105
- isDefault: false,
106
- isNamespace: false,
107
- localName: undefined,
108
- line: 1,
109
- },
110
- ],
111
- },
112
- ],
113
- [
114
- "src/b.ts",
115
- {
116
- entities: [
117
- {
118
- key: "src/b.ts::fnB",
119
- name: "fnB",
120
- exported: true,
121
- kind: "function",
122
- file_path: "src/b.ts",
123
- },
124
- {
125
- key: "src/b.ts::fnC",
126
- name: "fnC",
127
- exported: true,
128
- kind: "function",
129
- file_path: "src/b.ts",
130
- },
131
- ],
132
- edges: [],
133
- imports: [],
134
- },
135
- ],
136
- ]);
137
- const result = resolveCrossFileEdges(fileResults);
138
- // Only one file→file edge despite two import declarations
139
- const fileEdges = result.fileImportEdges.filter((e) => e.from_key === "file:src/a.ts" && e.to_key === "file:src/b.ts");
140
- expect(fileEdges).toHaveLength(1);
141
- });
142
- });
143
- describe("R.4: Git co-change computation", () => {
144
- it("computes co-change edges from the current repo", () => {
145
- // This test runs against the actual unerr-cli repo
146
- const edges = computeCoChangeEdges(process.cwd(), 50, 10, 2);
147
- // Should find at least some co-change pairs in a real repo
148
- expect(edges.length).toBeGreaterThanOrEqual(0);
149
- // If there are results, verify structure
150
- if (edges.length > 0) {
151
- const first = edges[0];
152
- expect(first.from_file).toBeTruthy();
153
- expect(first.to_file).toBeTruthy();
154
- expect(first.co_occurrences).toBeGreaterThanOrEqual(2);
155
- expect(first.correlation).toBeGreaterThan(0);
156
- expect(first.correlation).toBeLessThanOrEqual(1);
157
- }
158
- });
159
- it("returns empty for non-git directory", () => {
160
- const edges = computeCoChangeEdges("/tmp", 50, 10, 2);
161
- expect(edges).toHaveLength(0);
162
- });
163
- });
164
- describe("R.1/R.2: File entity and contains edge verification (integration)", () => {
165
- it("ingestIndexResult creates file entities for each unique file path", async () => {
166
- // This is a unit-level check of the ingest function's contract
167
- // Full integration test requires CozoDB instance
168
- const { basename } = await import("node:path");
169
- // Verify the file key convention
170
- const filePath = "src/utils/exec.ts";
171
- const fileKey = `file:${filePath}`;
172
- expect(fileKey).toBe("file:src/utils/exec.ts");
173
- expect(basename(filePath)).toBe("exec.ts");
174
- });
175
- it("file entity keys use file: prefix to avoid collisions", () => {
176
- // Entity keys use filepath::name format (e.g., "src/a.ts::fnA")
177
- // File keys use file:filepath format (e.g., "file:src/a.ts")
178
- const entityKey = "src/a.ts::fnA";
179
- const fileKey = "file:src/a.ts";
180
- // They must not collide
181
- expect(entityKey).not.toBe(fileKey);
182
- expect(fileKey.startsWith("file:")).toBe(true);
183
- expect(entityKey.startsWith("file:")).toBe(false);
184
- });
185
- });
186
- describe("R.3 Multi-language: resolveImportSourceToFile", () => {
187
- const projectFiles = new Set([
188
- "src/utils/helper.ts",
189
- "src/utils/index.ts",
190
- "src/commands/status.ts",
191
- "app/models/user.py",
192
- "app/models/__init__.py",
193
- "app/services/auth.py",
194
- "pkg/handlers/auth.go",
195
- "pkg/models/user.go",
196
- "internal/db/connection.go",
197
- "com/example/service/UserService.java",
198
- "com/example/models/User.java",
199
- "src/module/sub.rs",
200
- "src/module/mod.rs",
201
- "src/lib.rs",
202
- "lib/models/user.rb",
203
- "lib/services/auth.rb",
204
- "Services/UserService.cs",
205
- "Models/User.cs",
206
- ]);
207
- it("resolves TS/JS relative imports", () => {
208
- expect(resolveImportSourceToFile("./helper", "src/utils/status.ts", projectFiles)).toBe("src/utils/helper.ts");
209
- expect(resolveImportSourceToFile("../commands/status", "src/utils/helper.ts", projectFiles)).toBe("src/commands/status.ts");
210
- // Index file resolution (../utils from commands/ → src/utils/index.ts)
211
- expect(resolveImportSourceToFile("../utils", "src/commands/status.ts", projectFiles)).toBe("src/utils/index.ts");
212
- });
213
- it("resolves Python dotted module paths", () => {
214
- expect(resolveImportSourceToFile("models.user", "app/services/auth.py", projectFiles)).toBe("app/models/user.py");
215
- // __init__.py as directory index
216
- expect(resolveImportSourceToFile("models", "app/services/auth.py", projectFiles)).toBe("app/models/__init__.py");
217
- });
218
- it("resolves Go package paths by suffix matching", () => {
219
- expect(resolveImportSourceToFile("github.com/myapp/pkg/handlers", "cmd/main.go", projectFiles)).toBe("pkg/handlers/auth.go");
220
- });
221
- it("resolves Java dot-separated package paths", () => {
222
- expect(resolveImportSourceToFile("com.example.models.User", "com/example/service/UserService.java", projectFiles)).toBe("com/example/models/User.java");
223
- });
224
- it("resolves Rust crate paths", () => {
225
- expect(resolveImportSourceToFile("crate::module::sub", "src/lib.rs", projectFiles)).toBe("src/module/sub.rs");
226
- // Rust mod.rs as directory index
227
- expect(resolveImportSourceToFile("crate::module", "src/lib.rs", projectFiles)).toBe("src/module/mod.rs");
228
- });
229
- it("resolves Ruby require paths", () => {
230
- expect(resolveImportSourceToFile("models/user", "lib/services/auth.rb", projectFiles)).toBe("lib/models/user.rb");
231
- });
232
- it("resolves C# namespace paths", () => {
233
- expect(resolveImportSourceToFile("Models.User", "Services/UserService.cs", projectFiles)).toBe("Models/User.cs");
234
- });
235
- it("returns null for external/unresolvable imports", () => {
236
- // npm package
237
- expect(resolveImportSourceToFile("lodash", "src/utils/helper.ts", projectFiles)).toBeNull();
238
- // Python stdlib
239
- expect(resolveImportSourceToFile("os.path", "app/services/auth.py", projectFiles)).toBeNull();
240
- // Go stdlib
241
- expect(resolveImportSourceToFile("fmt", "pkg/handlers/auth.go", projectFiles)).toBeNull();
242
- });
243
- });
244
- });