@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,82 +0,0 @@
1
- import { existsSync, mkdirSync, readFileSync, rmSync } from "node:fs";
2
- import os from "node:os";
3
- import { join } from "node:path";
4
- import { afterEach, beforeEach, describe, expect, it } from "vitest";
5
- import { installFileLogger } from "../utils/file-logger.js";
6
- describe("installFileLogger", () => {
7
- let tmpDir;
8
- let logPath;
9
- let uninstall = null;
10
- beforeEach(() => {
11
- tmpDir = join(os.tmpdir(), `unerr-file-logger-${Date.now()}-${Math.random()}`);
12
- mkdirSync(tmpDir, { recursive: true });
13
- logPath = join(tmpDir, "mirror.log");
14
- });
15
- afterEach(() => {
16
- if (uninstall) {
17
- uninstall();
18
- uninstall = null;
19
- }
20
- rmSync(tmpDir, { recursive: true, force: true });
21
- });
22
- it("mirrors stderr.write to the target file", () => {
23
- uninstall = installFileLogger({ filePath: logPath });
24
- process.stderr.write("hello from stderr\n");
25
- expect(existsSync(logPath)).toBe(true);
26
- expect(readFileSync(logPath, "utf-8")).toContain("hello from stderr");
27
- });
28
- it("strips ANSI escape codes from the file copy", () => {
29
- uninstall = installFileLogger({ filePath: logPath });
30
- const colored = "\x1b[31mred text\x1b[0m\n";
31
- process.stderr.write(colored);
32
- const fileContent = readFileSync(logPath, "utf-8");
33
- expect(fileContent).toBe("red text\n");
34
- expect(fileContent).not.toContain("\x1b[");
35
- });
36
- it("rotates when the file exceeds maxBytes", () => {
37
- uninstall = installFileLogger({
38
- filePath: logPath,
39
- maxBytes: 512,
40
- keep: 3,
41
- });
42
- // Write enough bytes to trigger rotation
43
- for (let i = 0; i < 20; i++) {
44
- process.stderr.write(`${"x".repeat(100)}\n`);
45
- }
46
- // After rotation, at least one .log.1 should exist
47
- expect(existsSync(`${logPath}.1`)).toBe(true);
48
- });
49
- it("honors the `keep` parameter — older rotations get dropped", () => {
50
- uninstall = installFileLogger({
51
- filePath: logPath,
52
- maxBytes: 256,
53
- keep: 2,
54
- });
55
- // Trigger many rotations
56
- for (let cycle = 0; cycle < 6; cycle++) {
57
- for (let i = 0; i < 10; i++) {
58
- process.stderr.write(`${"x".repeat(100)}\n`);
59
- }
60
- }
61
- // .log, .log.1, .log.2 may exist; .log.3 must not
62
- expect(existsSync(`${logPath}.3`)).toBe(false);
63
- });
64
- it("uninstaller restores original stderr.write", () => {
65
- const original = process.stderr.write;
66
- const off = installFileLogger({ filePath: logPath });
67
- expect(process.stderr.write).not.toBe(original);
68
- off();
69
- expect(process.stderr.write).toBe(original);
70
- // No further writes should land in the file
71
- const before = existsSync(logPath) ? readFileSync(logPath, "utf-8") : "";
72
- process.stderr.write("post-uninstall noise\n");
73
- const after = existsSync(logPath) ? readFileSync(logPath, "utf-8") : "";
74
- expect(after).toBe(before);
75
- });
76
- it("creates the parent directory if missing", () => {
77
- const nested = join(tmpDir, "a", "b", "c", "mirror.log");
78
- uninstall = installFileLogger({ filePath: nested });
79
- process.stderr.write("nested\n");
80
- expect(existsSync(nested)).toBe(true);
81
- });
82
- });
@@ -1,141 +0,0 @@
1
- import { mkdirSync, writeFileSync } from "node:fs";
2
- import { tmpdir } from "node:os";
3
- import { join } from "node:path";
4
- import { describe, expect, it } from "vitest";
5
- import { buildFileOutline } from "../tools/coding/file-outline.js";
6
- function makeTmpDir(label) {
7
- const dir = join(tmpdir(), `fo-${label}-${Date.now()}`);
8
- mkdirSync(dir, { recursive: true });
9
- return dir;
10
- }
11
- describe("buildFileOutline", () => {
12
- it("extracts entities from TypeScript without graph", async () => {
13
- const dir = makeTmpDir("ts");
14
- writeFileSync(join(dir, "sample.ts"), "export function alpha(x: number): number {\n return x + 1;\n}\n", "utf-8");
15
- const o = await buildFileOutline({
16
- cwd: dir,
17
- filePathArg: "sample.ts",
18
- graph: null,
19
- });
20
- expect(o.total_lines).toBeGreaterThanOrEqual(3);
21
- expect(o.language).toBe("typescript");
22
- expect(o.entities.some((e) => e.name === "alpha")).toBe(true);
23
- expect(o.imports.length + o.exports.length).toBeGreaterThanOrEqual(1);
24
- });
25
- it("parses JSON config keys for small .json files", async () => {
26
- const dir = makeTmpDir("json");
27
- writeFileSync(join(dir, "cfg.json"), JSON.stringify({ foo: 1, bar: { nested: true } }), "utf-8");
28
- const o = await buildFileOutline({
29
- cwd: dir,
30
- filePathArg: "cfg.json",
31
- graph: null,
32
- });
33
- expect(o.config_keys?.includes("foo")).toBe(true);
34
- expect(o.config_keys?.includes("bar")).toBe(true);
35
- });
36
- it("extracts a Go function without graph", async () => {
37
- const dir = makeTmpDir("go");
38
- writeFileSync(join(dir, "main.go"), "package main\n\nfunc goAlpha() int { return 1 }\n", "utf-8");
39
- const o = await buildFileOutline({
40
- cwd: dir,
41
- filePathArg: "main.go",
42
- graph: null,
43
- });
44
- expect(o.language).toBe("go");
45
- expect(o.entities.some((e) => e.name === "goAlpha")).toBe(true);
46
- });
47
- it("extracts a Python function without graph", async () => {
48
- const dir = makeTmpDir("py");
49
- writeFileSync(join(dir, "mod.py"), "def py_alpha(n):\n return n + 1\n", "utf-8");
50
- const o = await buildFileOutline({
51
- cwd: dir,
52
- filePathArg: "mod.py",
53
- graph: null,
54
- });
55
- expect(o.language).toBe("python");
56
- expect(o.entities.some((e) => e.name === "py_alpha")).toBe(true);
57
- });
58
- it("lists markdown headings", async () => {
59
- const dir = makeTmpDir("md");
60
- writeFileSync(join(dir, "doc.md"), "# Title\n\n## Section\nbody\n", "utf-8");
61
- const o = await buildFileOutline({
62
- cwd: dir,
63
- filePathArg: "doc.md",
64
- graph: null,
65
- });
66
- expect(o.headings?.some((h) => h.includes("Title"))).toBe(true);
67
- });
68
- // ─── New FRP-3 tests ──────────────────────────────────────────────────────
69
- it("marks exported entities with exported: true", async () => {
70
- const dir = makeTmpDir("exported");
71
- writeFileSync(join(dir, "lib.ts"), [
72
- "export function publicFn(): void {}",
73
- "function privateFn(): void {}",
74
- "export const PUBLIC_CONST = 1;",
75
- "const PRIVATE_CONST = 2;",
76
- "export class MyClass {}",
77
- ].join("\n"), "utf-8");
78
- const o = await buildFileOutline({
79
- cwd: dir,
80
- filePathArg: "lib.ts",
81
- graph: null,
82
- });
83
- const publicFn = o.entities.find((e) => e.name === "publicFn");
84
- const privateFn = o.entities.find((e) => e.name === "privateFn");
85
- const myClass = o.entities.find((e) => e.name === "MyClass");
86
- expect(publicFn?.exported).toBe(true);
87
- expect(privateFn?.exported).toBe(false);
88
- expect(myClass?.exported).toBe(true);
89
- });
90
- it("provides token_estimate roughly proportional to file size", async () => {
91
- const dir = makeTmpDir("token-est");
92
- const content = "x".repeat(400); // 400 chars → ~100 tokens at 4 chars/token
93
- writeFileSync(join(dir, "small.ts"), content, "utf-8");
94
- const o = await buildFileOutline({
95
- cwd: dir,
96
- filePathArg: "small.ts",
97
- graph: null,
98
- });
99
- // 400 chars / 4 chars_per_token = 100 tokens
100
- expect(o.token_estimate).toBe(100);
101
- });
102
- it("stable sort: entities with same start line sorted by name", async () => {
103
- const dir = makeTmpDir("sort");
104
- // Two entities at same line is unusual but can happen with type + const on same line
105
- // We'll simulate with a file where AST extracts multiple entities
106
- writeFileSync(join(dir, "multi.ts"), [
107
- "export function zebra(): void {}",
108
- "export function alpha(): void {}",
109
- "export function beta(): void {}",
110
- ].join("\n"), "utf-8");
111
- const o = await buildFileOutline({
112
- cwd: dir,
113
- filePathArg: "multi.ts",
114
- graph: null,
115
- });
116
- // Entities should be sorted by start line first
117
- for (let i = 1; i < o.entities.length; i++) {
118
- const prev = o.entities[i - 1];
119
- const curr = o.entities[i];
120
- if (prev.lines[0] === curr.lines[0]) {
121
- // Same start line → alphabetical
122
- expect(prev.name.localeCompare(curr.name)).toBeLessThanOrEqual(0);
123
- }
124
- else {
125
- expect(prev.lines[0]).toBeLessThan(curr.lines[0]);
126
- }
127
- }
128
- });
129
- it("all entities have the exported field defined", async () => {
130
- const dir = makeTmpDir("exported-all");
131
- writeFileSync(join(dir, "mod.ts"), "export function a() {}\nfunction b() {}\nexport class C {}\n", "utf-8");
132
- const o = await buildFileOutline({
133
- cwd: dir,
134
- filePathArg: "mod.ts",
135
- graph: null,
136
- });
137
- for (const entity of o.entities) {
138
- expect(typeof entity.exported).toBe("boolean");
139
- }
140
- });
141
- });
@@ -1,188 +0,0 @@
1
- import { mkdirSync, writeFileSync } from "node:fs";
2
- import { tmpdir } from "node:os";
3
- import { join } from "node:path";
4
- import { describe, expect, it } from "vitest";
5
- import { rankEntityMatches, runFileReadForRouter, } from "../tools/coding/file-read-protocol.js";
6
- function makeTmpDir(label) {
7
- const dir = join(tmpdir(), `frp-${label}-${Date.now()}`);
8
- mkdirSync(dir, { recursive: true });
9
- return dir;
10
- }
11
- describe("runFileReadForRouter", () => {
12
- it("returns full content for small files", async () => {
13
- const dir = makeTmpDir("small");
14
- writeFileSync(join(dir, "a.txt"), "one\ntwo\nthree\n", "utf-8");
15
- const r = await runFileReadForRouter({ file_path: "a.txt" }, { cwd: dir, graph: null });
16
- expect(typeof r.content).toBe("string");
17
- expect(r.content.includes("1\tone")).toBe(true);
18
- expect(r._layer6_meta?.gated).toBeUndefined();
19
- });
20
- it("gates files with >200 lines when no offset/entity", async () => {
21
- const dir = makeTmpDir("gate");
22
- const lines = Array.from({ length: 205 }, () => "x").join("\n");
23
- writeFileSync(join(dir, "big.txt"), lines, "utf-8");
24
- const r = await runFileReadForRouter({ file_path: "big.txt" }, { cwd: dir, graph: null });
25
- expect(r.content && typeof r.content === "object").toBe(true);
26
- const c = r.content;
27
- expect(c.gated).toBe(true);
28
- expect(r._layer6_meta?.format).toBe("outline");
29
- expect(r._layer6_meta?.gated).toBe(true);
30
- });
31
- it("entity slice includes ±5 line context around the match", async () => {
32
- const dir = makeTmpDir("ctx");
33
- const prefix = Array.from({ length: 12 }, (_, i) => `// line ${i + 1}`).join("\n");
34
- writeFileSync(join(dir, "deep.ts"), `${prefix}\nexport function sliceFn(): number {\n return 42;\n}\n`, "utf-8");
35
- const r = await runFileReadForRouter({ file_path: "deep.ts", entity: "sliceFn" }, { cwd: dir, graph: null });
36
- const body = r.content;
37
- expect(body.includes("// line 8")).toBe(true);
38
- expect(body.includes("sliceFn")).toBe(true);
39
- expect(body.includes("(Showing lines")).toBe(true);
40
- });
41
- it("targets entity by name using AST when graph is null", async () => {
42
- const dir = makeTmpDir("ent");
43
- writeFileSync(join(dir, "mod.ts"), "// head\nexport function targetFn(): void {\n return;\n}\n", "utf-8");
44
- const r = await runFileReadForRouter({ file_path: "mod.ts", entity: "targetFn" }, { cwd: dir, graph: null });
45
- expect(typeof r.content).toBe("string");
46
- expect(r.content.includes("targetFn")).toBe(true);
47
- expect(r.content.includes("Showing lines")).toBe(true);
48
- });
49
- it("rejects binary files", async () => {
50
- const dir = makeTmpDir("bin");
51
- writeFileSync(join(dir, "bin.dat"), Buffer.from([0, 1, 0]));
52
- const r = await runFileReadForRouter({ file_path: "bin.dat" }, { cwd: dir, graph: null });
53
- expect(r.content && typeof r.content === "object").toBe(true);
54
- expect(r.content.error).toMatch(/Binary/);
55
- });
56
- // ─── Token Budget Tests ─────────────────────────────────────────────────
57
- it("adaptive gating: budget=5000 allows 500-line file through without gating", async () => {
58
- const dir = makeTmpDir("budget-high");
59
- const lines = Array.from({ length: 300 }, (_, i) => `line ${i + 1}`).join("\n");
60
- writeFileSync(join(dir, "medium.ts"), lines, "utf-8");
61
- const r = await runFileReadForRouter({ file_path: "medium.ts", token_budget: 5000 }, { cwd: dir, graph: null });
62
- // With budget=5000, budgetLines = (5000*4)/80 = 250. effectiveGate = max(200, 250) = 250.
63
- // File has 300 lines > 250 → still gated
64
- // But let's use a higher budget to prove the adaptive gating works
65
- const r2 = await runFileReadForRouter({ file_path: "medium.ts", token_budget: 30000 }, { cwd: dir, graph: null });
66
- // budget=30000 → budgetLines = (30000*4)/80 = 1500. effectiveGate = max(200, 1500) = 1500.
67
- // File has 300 lines < 1500 → NOT gated
68
- expect(typeof r2.content).toBe("string");
69
- expect(r2._layer6_meta?.gated).toBeUndefined();
70
- });
71
- it("token budget constrains output line count", async () => {
72
- const dir = makeTmpDir("budget-cap");
73
- // 150 lines, each ~40 chars → fits in default budget but let's constrain
74
- const lines = Array.from({ length: 150 }, (_, i) => `const x${i} = ${i}; // padding here for length`).join("\n");
75
- writeFileSync(join(dir, "vars.ts"), lines, "utf-8");
76
- const r = await runFileReadForRouter({ file_path: "vars.ts", token_budget: 300 }, { cwd: dir, graph: null });
77
- // budget=300 → budgetLines = (300*4)/80 = 15
78
- // File has 150 lines, effLimit = min(15, 150) = 15
79
- const body = r.content;
80
- expect(body.includes("(Showing lines")).toBe(true);
81
- // Should only show ~15 lines
82
- const outputLines = body.split("\n").filter((l) => /^\d+\t/.test(l));
83
- expect(outputLines.length).toBeLessThanOrEqual(20); // some tolerance
84
- });
85
- it("default behavior unchanged: no token_budget → same as before", async () => {
86
- const dir = makeTmpDir("default");
87
- const lines = Array.from({ length: 205 }, () => "x").join("\n");
88
- writeFileSync(join(dir, "big.txt"), lines, "utf-8");
89
- // Default token_budget=2000 → budgetLines=(2000*4)/80=100 → effectiveGate=max(200,100)=200
90
- // File has 205 > 200 → gated (same as before)
91
- const r = await runFileReadForRouter({ file_path: "big.txt" }, { cwd: dir, graph: null });
92
- expect(r.content.gated).toBe(true);
93
- });
94
- // ─── Ranked Entity Matching Tests ───────────────────────────────────────
95
- it("case-insensitive match: 'CompressOutput' finds 'compressOutput'", async () => {
96
- const dir = makeTmpDir("case-ins");
97
- writeFileSync(join(dir, "fn.ts"), "// top\nexport function compressOutput(): string {\n return '';\n}\n", "utf-8");
98
- const r = await runFileReadForRouter({ file_path: "fn.ts", entity: "CompressOutput" }, { cwd: dir, graph: null });
99
- expect(typeof r.content).toBe("string");
100
- expect(r.content.includes("compressOutput")).toBe(true);
101
- });
102
- it("prefix match: 'compress' matches 'compressShellOutput'", async () => {
103
- const dir = makeTmpDir("prefix");
104
- writeFileSync(join(dir, "fn.ts"), "// filler\nexport function compressShellOutput(): string {\n return '';\n}\n", "utf-8");
105
- const r = await runFileReadForRouter({ file_path: "fn.ts", entity: "compress" }, { cwd: dir, graph: null });
106
- expect(typeof r.content).toBe("string");
107
- expect(r.content.includes("compressShellOutput")).toBe(true);
108
- });
109
- it("entity not found on large file: returns outline with suggestions", async () => {
110
- const dir = makeTmpDir("not-found");
111
- const filler = Array.from({ length: 210 }, (_, i) => `// line ${i}`).join("\n");
112
- writeFileSync(join(dir, "big.ts"), `${filler}\nexport function realFunction(): void {}\n`, "utf-8");
113
- const r = await runFileReadForRouter({ file_path: "big.ts", entity: "nonExistentThing" }, { cwd: dir, graph: null });
114
- const c = r.content;
115
- expect(c.gated).toBe(true);
116
- expect(c.entity_search).toBeDefined();
117
- const search = c.entity_search;
118
- expect(search.matched).toBe(false);
119
- expect(search.query).toBe("nonExistentThing");
120
- });
121
- it("provides tokens_estimate in _layer6_meta", async () => {
122
- const dir = makeTmpDir("tokens-est");
123
- writeFileSync(join(dir, "a.ts"), "const x = 1;\nconst y = 2;\n", "utf-8");
124
- const r = await runFileReadForRouter({ file_path: "a.ts" }, { cwd: dir, graph: null });
125
- expect(r._layer6_meta?.tokens_estimate).toBeGreaterThan(0);
126
- expect(r._layer6_meta?.tokens_estimate).toBeLessThan(100);
127
- });
128
- it("empty file returns empty content without error", async () => {
129
- const dir = makeTmpDir("empty");
130
- writeFileSync(join(dir, "empty.ts"), "", "utf-8");
131
- const r = await runFileReadForRouter({ file_path: "empty.ts" }, { cwd: dir, graph: null });
132
- // Empty file has totalLines=1 (one empty string from split), body may be empty
133
- // Should not throw or return error
134
- expect(r._layer6_meta?.format).toBe("json");
135
- });
136
- });
137
- describe("rankEntityMatches", () => {
138
- const entities = [
139
- { name: "compressShellOutput", start_line: 10, body: "function body" },
140
- { name: "compressOutput", start_line: 20, body: "function body" },
141
- { name: "decompressInput", start_line: 30, body: "function body" },
142
- { name: "CompressOutput", start_line: 40, body: "function body" },
143
- ];
144
- it("exact match scores 100", () => {
145
- const ranked = rankEntityMatches(entities, "compressOutput");
146
- expect(ranked[0].score).toBe(100);
147
- expect(ranked[0].entity.name).toBe("compressOutput");
148
- expect(ranked[0].matchType).toBe("exact");
149
- });
150
- it("case-insensitive match scores 90", () => {
151
- const ranked = rankEntityMatches(entities, "compressoutput");
152
- expect(ranked[0].score).toBe(90);
153
- expect(ranked[0].entity.name).toBe("compressOutput");
154
- expect(ranked[0].matchType).toBe("case_insensitive");
155
- });
156
- it("prefix match scores 80", () => {
157
- const ranked = rankEntityMatches(entities, "compress");
158
- expect(ranked[0].score).toBe(80);
159
- // Both compressShellOutput and compressOutput start with "compress"
160
- // Both get score 80, sort is stable (order depends on input array)
161
- expect(ranked[0].matchType).toBe("prefix");
162
- });
163
- it("camelCase segment match scores 70", () => {
164
- const ents = [
165
- { name: "compressShellOutput", start_line: 10, body: "fn" },
166
- { name: "handleInput", start_line: 20, body: "fn" },
167
- ];
168
- const ranked = rankEntityMatches(ents, "shell");
169
- expect(ranked[0].score).toBe(70);
170
- expect(ranked[0].entity.name).toBe("compressShellOutput");
171
- expect(ranked[0].matchType).toBe("camelCase_segment");
172
- });
173
- it("substring match scores between 40-60 based on specificity", () => {
174
- const ents = [{ name: "myHandlerForXyz", start_line: 10, body: "fn" }];
175
- // "handler" is not a camelCase segment of "myHandlerForXyz" (segments: my, handler, for, xyz)
176
- // Actually it IS a segment. Let's use a true substring that isn't a segment.
177
- const ents2 = [{ name: "handleAllRequests", start_line: 10, body: "fn" }];
178
- // "eAll" is a substring but not a segment
179
- const ranked = rankEntityMatches(ents2, "eAll");
180
- expect(ranked[0].score).toBeGreaterThanOrEqual(40);
181
- expect(ranked[0].score).toBeLessThanOrEqual(60);
182
- expect(ranked[0].matchType).toBe("substring");
183
- });
184
- it("returns empty array when nothing matches", () => {
185
- const ranked = rankEntityMatches(entities, "zzzzNotHere");
186
- expect(ranked).toHaveLength(0);
187
- });
188
- });
@@ -1,233 +0,0 @@
1
- /**
2
- * Layer 6 Sprint FE-C — format encoder unit + router integration tests.
3
- */
4
- import { describe, expect, it, vi } from "vitest";
5
- import { QueryRouter } from "../intelligence/query-router.js";
6
- import { COLUMNAR_LEGEND_TEXT, detectShape, encodeColumnar, escapeColumnarCell, formatToolOutput, isUniformObjectArray, } from "../proxy/format-encoder.js";
7
- import { createSessionLegendTracker } from "../proxy/session-legend.js";
8
- describe("detectShape", () => {
9
- it("classifies uniform object arrays", () => {
10
- expect(detectShape([
11
- { a: 1, b: 2 },
12
- { b: 3, a: 4 },
13
- ])).toBe("uniform-array");
14
- });
15
- it("classifies heterogeneous arrays", () => {
16
- expect(detectShape([{ a: 1 }, { a: 1, b: 2 }])).toBe("heterogeneous");
17
- expect(detectShape([1, 2])).toBe("heterogeneous");
18
- });
19
- it("classifies single objects", () => {
20
- expect(detectShape({ x: 1 })).toBe("single-object");
21
- });
22
- });
23
- describe("isUniformObjectArray", () => {
24
- it("returns true when keys match across rows", () => {
25
- expect(isUniformObjectArray([
26
- { k: "a", n: 1 },
27
- { n: 2, k: "b" },
28
- ])).toBe(true);
29
- });
30
- it("returns false when keys differ", () => {
31
- expect(isUniformObjectArray([{ k: "a" }, { k: "b", extra: 1 }])).toBe(false);
32
- });
33
- });
34
- describe("escapeColumnarCell", () => {
35
- it("quotes pipes and newlines", () => {
36
- expect(escapeColumnarCell("a|b")).toBe('"a|b"');
37
- expect(escapeColumnarCell('say "hi"')).toBe('"say ""hi"""');
38
- expect(escapeColumnarCell("x\ny")).toBe("x\\ny");
39
- });
40
- it("renders null as empty", () => {
41
- expect(escapeColumnarCell(null)).toBe("");
42
- expect(escapeColumnarCell(undefined)).toBe("");
43
- });
44
- });
45
- describe("encodeColumnar", () => {
46
- it("emits _fmt header and aligned rows", () => {
47
- const text = encodeColumnar([
48
- { name: "x", v: 1 },
49
- { name: "y", v: 2 },
50
- ], ["name", "v"]);
51
- expect(text.startsWith("_fmt:columnar\nname|v\n")).toBe(true);
52
- expect(text).toContain("x|1");
53
- expect(text).toContain("y|2");
54
- });
55
- });
56
- describe("formatToolOutput", () => {
57
- it("columnar-encodes uniform arrays", () => {
58
- const meta = { format: "json" };
59
- const out = formatToolOutput("get_callers", [{ k: "a", z: 1 }], meta);
60
- expect(typeof out).toBe("string");
61
- expect(out).toContain("_fmt:columnar");
62
- expect(meta.format).toBe("columnar");
63
- expect(meta.columns).toEqual(["k", "z"]);
64
- });
65
- it("does not format outline payloads", () => {
66
- const meta = { format: "outline", gated: true };
67
- const payload = [{ a: 1 }];
68
- expect(formatToolOutput("file_read", payload, meta)).toBe(payload);
69
- });
70
- });
71
- describe("formatToolOutput FE-E (legends, tiers)", () => {
72
- it("attaches columnar legend once per tracker until invalidated", () => {
73
- const legend = createSessionLegendTracker();
74
- const metaC = {};
75
- formatToolOutput("get_callers", [{ k: "a", z: 1 }], metaC, {
76
- legend,
77
- tier: "columnar",
78
- });
79
- expect(metaC.columnar_legend).toBe(COLUMNAR_LEGEND_TEXT);
80
- const metaC2 = {};
81
- formatToolOutput("get_callers", [{ k: "b", z: 2 }], metaC2, {
82
- legend,
83
- tier: "columnar",
84
- });
85
- expect(metaC2.columnar_legend).toBeUndefined();
86
- });
87
- it("minified tier skips columnar but keeps structured JSON", () => {
88
- const meta = {};
89
- const out = formatToolOutput("get_callers", [
90
- { k: "a", z: 1 },
91
- { k: "b", z: 2 },
92
- ], meta, { tier: "minified" });
93
- expect(Array.isArray(out)).toBe(true);
94
- expect(meta.format).toBe("json");
95
- });
96
- it("expanded tier returns untouched object", () => {
97
- const obj = { key: "fn1", kind: "function", name: "doStuff" };
98
- const meta = {};
99
- const out = formatToolOutput("get_function", obj, meta, {
100
- tier: "expanded",
101
- });
102
- expect(out).toBe(obj);
103
- expect(meta.format).toBe("json");
104
- });
105
- });
106
- describe("performance: columnar encode 500 rows", () => {
107
- it("encodes in under 5ms", () => {
108
- const row = {
109
- key: "k",
110
- kind: "function",
111
- name: "n",
112
- file_path: "f.ts",
113
- start_line: 1,
114
- signature: "()",
115
- body: "x",
116
- fan_in: 0,
117
- fan_out: 0,
118
- risk_level: "normal",
119
- community: -1,
120
- };
121
- const rows = Array.from({ length: 500 }, (_, i) => ({
122
- ...row,
123
- key: `k${i}`,
124
- }));
125
- const cols = Object.keys(row).sort();
126
- const t0 = performance.now();
127
- encodeColumnar(rows, cols);
128
- const ms = performance.now() - t0;
129
- expect(ms).toBeLessThan(5);
130
- });
131
- });
132
- function minimalGraphWithCallers(rows) {
133
- return {
134
- getEntity: vi.fn(),
135
- getCallersOf: vi.fn().mockResolvedValue(rows),
136
- getCalleesOf: vi.fn().mockResolvedValue([]),
137
- searchEntities: vi.fn().mockResolvedValue([]),
138
- getImports: vi.fn().mockResolvedValue([]),
139
- getBlastRadius: vi.fn().mockReturnValue({
140
- direct_callers: 0,
141
- direct_callees: 0,
142
- transitive_count: 0,
143
- transitive_depth: 0,
144
- is_chokepoint: false,
145
- summary: "",
146
- }),
147
- getBlastRadiusEntities: vi.fn().mockReturnValue([]),
148
- getConventionsForEntity: vi.fn().mockReturnValue([]),
149
- getCorrections: vi.fn().mockReturnValue([]),
150
- getCommunityForEntity: vi.fn().mockReturnValue(null),
151
- getCrossCommunityEdges: vi.fn().mockReturnValue([]),
152
- getEntitiesByFile: vi.fn().mockReturnValue([]),
153
- queryEntities: vi.fn().mockReturnValue([]),
154
- getDriftOverlayEntity: vi.fn().mockReturnValue(null),
155
- getDriftSummary: vi
156
- .fn()
157
- .mockReturnValue({ added: 0, modified: 0, removed: 0, total: 0 }),
158
- getCriticalNodes: vi.fn().mockReturnValue([]),
159
- getCrossBoundaryLinks: vi.fn().mockReturnValue([]),
160
- getRules: vi.fn().mockReturnValue([]),
161
- getJustificationsForEntity: vi.fn().mockReturnValue([]),
162
- getDriftEntitiesForFile: vi.fn().mockReturnValue([]),
163
- db: { run: vi.fn().mockResolvedValue({ rows: [] }) },
164
- close: vi.fn(),
165
- };
166
- }
167
- describe("QueryRouter + FE-C integration", () => {
168
- it("get_callers returns columnar text for uniform caller rows", async () => {
169
- const row = {
170
- key: "caller1",
171
- kind: "function",
172
- name: "c",
173
- file_path: "src/x.ts",
174
- start_line: 1,
175
- signature: "()",
176
- body: "",
177
- fan_in: 1,
178
- fan_out: 0,
179
- risk_level: "normal",
180
- community: -1,
181
- };
182
- const graph = minimalGraphWithCallers([row]);
183
- const router = new QueryRouter(graph);
184
- const result = await router.execute("get_callers", { key: "fn1" });
185
- expect(result._meta.format).toBe("columnar");
186
- expect(typeof result.content).toBe("string");
187
- expect(result.content).toContain("_fmt:columnar");
188
- expect(result._meta.columns?.length).toBeGreaterThan(0);
189
- });
190
- it("search_code returns columnar for uniform entity rows", async () => {
191
- const row = {
192
- key: "e1",
193
- kind: "function",
194
- name: "n",
195
- file_path: "a.ts",
196
- start_line: 1,
197
- signature: "()",
198
- body: "",
199
- fan_in: 0,
200
- fan_out: 0,
201
- risk_level: "low",
202
- community: 0,
203
- };
204
- const graph = minimalGraphWithCallers([]);
205
- graph.searchEntities.mockResolvedValue([row]);
206
- const router = new QueryRouter(graph);
207
- const result = await router.execute("search_code", { query: "foo" });
208
- expect(result._meta.format).toBe("columnar");
209
- expect(result.content.includes("_fmt:columnar")).toBe(true);
210
- });
211
- it("get_function strips community but preserves body (useful code)", async () => {
212
- const graph = minimalGraphWithCallers([]);
213
- graph.getEntity.mockResolvedValue({
214
- key: "fn1",
215
- kind: "function",
216
- name: "doStuff",
217
- file_path: "src/index.ts",
218
- start_line: 10,
219
- signature: "()",
220
- body: "{}",
221
- fan_in: 0,
222
- fan_out: 0,
223
- risk_level: "normal",
224
- community: 0,
225
- });
226
- const router = new QueryRouter(graph);
227
- const result = await router.execute("get_function", { key: "fn1" });
228
- const content = result.content;
229
- expect(content.key).toBe("fn1");
230
- expect(content.body).toBe("{}");
231
- expect(content.community).toBeUndefined();
232
- });
233
- });