uneven-ai 0.12.6 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/CHANGELOG.md +387 -0
  2. package/README.md +3 -17
  3. package/dist/application/analysis/dashboard-generator.js +7 -4
  4. package/dist/application/analysis/data-analyst.d.ts.map +1 -1
  5. package/dist/application/analysis/data-analyst.js +78 -12
  6. package/dist/application/analysis/data-security-context.d.ts.map +1 -1
  7. package/dist/application/analysis/data-security-context.js +4 -2
  8. package/dist/application/analysis/excel-exporter.d.ts.map +1 -1
  9. package/dist/application/analysis/excel-exporter.js +2 -4
  10. package/dist/application/analysis/llm-security-reviewer.js +1 -1
  11. package/dist/application/analysis/malware-scanner.d.ts.map +1 -1
  12. package/dist/application/analysis/malware-scanner.js +36 -36
  13. package/dist/application/analysis/pentest-security-context.js +26 -8
  14. package/dist/application/analysis/report-packager.d.ts.map +1 -1
  15. package/dist/application/analysis/report-packager.js +18 -9
  16. package/dist/application/analysis/sbom-generator.js +1 -1
  17. package/dist/application/analysis/security-analyzer.d.ts +1 -1
  18. package/dist/application/analysis/security-analyzer.d.ts.map +1 -1
  19. package/dist/application/analysis/security-analyzer.js +69 -34
  20. package/dist/application/analysis/supply-chain-auditor.d.ts +15 -0
  21. package/dist/application/analysis/supply-chain-auditor.d.ts.map +1 -1
  22. package/dist/application/analysis/supply-chain-auditor.js +86 -42
  23. package/dist/application/development/fix-engine.d.ts +12 -1
  24. package/dist/application/development/fix-engine.d.ts.map +1 -1
  25. package/dist/application/development/fix-engine.js +56 -31
  26. package/dist/application/orchestration/engine.d.ts +30 -2
  27. package/dist/application/orchestration/engine.d.ts.map +1 -1
  28. package/dist/application/orchestration/engine.js +389 -190
  29. package/dist/application/orchestration/incremental-index.d.ts +24 -6
  30. package/dist/application/orchestration/incremental-index.d.ts.map +1 -1
  31. package/dist/application/orchestration/incremental-index.js +87 -31
  32. package/dist/cli/commands/analyze.d.ts.map +1 -1
  33. package/dist/cli/commands/analyze.js +7 -1
  34. package/dist/cli/commands/ask.d.ts.map +1 -1
  35. package/dist/cli/commands/ask.js +20 -9
  36. package/dist/cli/commands/askf.d.ts.map +1 -1
  37. package/dist/cli/commands/askf.js +54 -39
  38. package/dist/cli/commands/diff.d.ts +29 -0
  39. package/dist/cli/commands/diff.d.ts.map +1 -0
  40. package/dist/cli/commands/diff.js +151 -0
  41. package/dist/cli/commands/index.js +67 -67
  42. package/dist/cli/commands/init.d.ts.map +1 -1
  43. package/dist/cli/commands/init.js +74 -38
  44. package/dist/cli/commands/pentest.js +12 -7
  45. package/dist/cli/commands/restore.js +4 -4
  46. package/dist/cli/commands/scan.d.ts.map +1 -1
  47. package/dist/cli/commands/scan.js +4 -2
  48. package/dist/cli/index.d.ts +1 -4
  49. package/dist/cli/index.d.ts.map +1 -1
  50. package/dist/cli/index.js +31 -3
  51. package/dist/domain/entities/session.d.ts +45 -7
  52. package/dist/domain/entities/session.d.ts.map +1 -1
  53. package/dist/domain/entities/session.js +37 -5
  54. package/dist/domain/entities/snapshot.d.ts +10 -10
  55. package/dist/domain/entities/snapshot.d.ts.map +1 -1
  56. package/dist/domain/entities/snapshot.js +43 -60
  57. package/dist/domain/services/chunker.d.ts.map +1 -1
  58. package/dist/domain/services/chunker.js +2 -6
  59. package/dist/infrastructure/adapters/bridge.d.ts +15 -3
  60. package/dist/infrastructure/adapters/bridge.d.ts.map +1 -1
  61. package/dist/infrastructure/adapters/bridge.js +65 -7
  62. package/dist/infrastructure/adapters/external-providers.d.ts.map +1 -1
  63. package/dist/infrastructure/adapters/external-providers.js +47 -21
  64. package/dist/infrastructure/io/db-loader.d.ts.map +1 -1
  65. package/dist/infrastructure/io/db-loader.js +4 -2
  66. package/dist/infrastructure/io/file-watcher.d.ts.map +1 -1
  67. package/dist/infrastructure/io/file-watcher.js +4 -1
  68. package/dist/infrastructure/io/git-manager.js +3 -3
  69. package/dist/infrastructure/io/logger/index.d.ts +7 -2
  70. package/dist/infrastructure/io/logger/index.d.ts.map +1 -1
  71. package/dist/infrastructure/io/logger/index.js +24 -7
  72. package/dist/infrastructure/io/process-watcher.d.ts +5 -1
  73. package/dist/infrastructure/io/process-watcher.d.ts.map +1 -1
  74. package/dist/infrastructure/io/process-watcher.js +70 -6
  75. package/dist/infrastructure/io/web-scraper.d.ts +4 -1
  76. package/dist/infrastructure/io/web-scraper.d.ts.map +1 -1
  77. package/dist/infrastructure/io/web-scraper.js +28 -11
  78. package/dist/infrastructure/utils/error-parser.d.ts +6 -1
  79. package/dist/infrastructure/utils/error-parser.d.ts.map +1 -1
  80. package/dist/infrastructure/utils/error-parser.js +110 -56
  81. package/dist/infrastructure/utils/process-lock.d.ts +10 -0
  82. package/dist/infrastructure/utils/process-lock.d.ts.map +1 -1
  83. package/dist/infrastructure/utils/process-lock.js +46 -7
  84. package/package.json +3 -2
  85. package/prebuilds/darwin-arm64/uneven_core.node +0 -0
  86. package/prebuilds/darwin-x64/uneven_core.node +0 -0
  87. package/prebuilds/linux-arm64/uneven_core.node +0 -0
  88. package/prebuilds/linux-x64/uneven_core.node +0 -0
  89. package/prebuilds/win32-x64/uneven_core.node +0 -0
  90. package/scripts/postinstall.cjs +6 -6
  91. package/types/index.d.ts +1 -0
package/CHANGELOG.md CHANGED
@@ -5,6 +5,393 @@ All notable changes to Uneven AI will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.0.3] - 2026-04-15
9
+
10
+ ### Changed
11
+
12
+ - **README**: remove contributing section — repository is private
13
+
14
+ ## [1.0.2] - 2026-04-15
15
+
16
+ ### Fixed
17
+
18
+ - **CI (release)**: rename prebuild assets to unique filenames (`linux-x64.node`, etc.) before
19
+ uploading to GitHub Release — previously all 5 platforms shared the name `uneven_core.node`,
20
+ causing asset upload to fail with HTTP 404
21
+
22
+ ## [1.0.1] - 2026-04-15
23
+
24
+ ### Fixed
25
+
26
+ - **CI (linux-arm64)**: install `g++-aarch64-linux-gnu` and set `CXX_aarch64_unknown_linux_gnu`
27
+ so crates with C++ build scripts (e.g. `esaxx-rs`) cross-compile correctly on the release pipeline
28
+
29
+ ## [1.0.0] - 2026-04-15
30
+
31
+ ### Added — Test suite
32
+
33
+ - **`__tests__/engine.test.ts`**: 26 unit tests covering the orchestration engine — `init()`,
34
+ `stop()`, `handleErrorFix()` (anti-loop, deduplication, file queue), `_runFixForError()`
35
+ (confidence gate, autoFix on/off paths, fix-applied/fix-reverted/fix-pending events)
36
+ - **`crates/.../error_parser.rs`**: 9 Rust unit tests — TypeScript, absolute path, relative,
37
+ nested path formats; edge cases: empty string, missing column, plain URL rejection
38
+ - **`crates/.../retrieval/mod.rs`**: 11 Rust unit tests — `cosine_sim` (identical, orthogonal,
39
+ opposite, mismatched lengths, empty vectors) and `assemble_context` (empty, single, multiple
40
+ documents, score formatting, header presence)
41
+ - **`crates/.../providers.rs`**: 4 Rust unit tests — all valid providers, case-insensitive
42
+ parsing, unknown provider error, error message content
43
+ - **`crates/.../embeddings.rs`**: 7 pure Rust unit tests — `normalize_l2` (unit vector, scaling,
44
+ zero-vector error), `hash_embed` (dimension, normalization, determinism, distinctness), `fnv3`
45
+ (determinism, collision); existing 4 model-dependent tests fixed (`#[ignore]` + multi-thread
46
+ runtime to prevent `block_in_place` panic)
47
+ - **Total**: 135 TypeScript tests (7 suites) + 34 Rust tests (0 failures, 4 ignored)
48
+
49
+ ### Added — CI workflow
50
+
51
+ - **`.github/workflows/ci.yml`**: `cargo test --lib` added to `build-rust` job — pure Rust
52
+ unit tests run on every push to `main`; Jest job decoupled from full Rust build (bridge
53
+ mocked, binary not required); `cargo clippy -D warnings` catches lint regressions before
54
+ they reach a release tag
55
+
56
+ ### Fixed — ESM test infrastructure
57
+
58
+ - **`jest.config.cjs`**: bridge `moduleNameMapper` patterns reordered before the `.js` stripper
59
+ so patterns match `bridge.js` before the extension is stripped
60
+ - **`__tests__/engine.test.ts`**: uses `jest.unstable_mockModule()` + explicit
61
+ `import { jest } from '@jest/globals'` — required for `--experimental-vm-modules` where
62
+ `jest` is not injected as a bare global into ESM module scope
63
+ - **`__tests__/error-parser.test.ts`**: Python traceback fixture corrected — added the
64
+ intermediate executing-code line that the parser requires to identify the last stack frame
65
+
66
+ ## [0.16.0] - 2026-04-15
67
+
68
+ ### Added — NAPI streaming inference
69
+
70
+ - **`inference.rs`**: `generate_streaming` and `run_inference_streaming` — token-by-token generation
71
+ via callback, enabling progressive output instead of waiting for full completion
72
+ - **`llm/orchestrator.rs`**: `infer_with_params_streaming` wires the streaming path end-to-end
73
+ - **`api/napi_api.rs`**: `llm_infer_stream` exposed as a NAPI function with `ThreadsafeFunction`
74
+ callback — each generated token is delivered to JS as it is produced
75
+ - **`bridge.ts`**: `llmInferStream` bound and wired into `engine.ts ask()` — `ask` and `askf`
76
+ commands now stream tokens inline instead of printing the full response at once
77
+ - **`test/mocks`**: `llmInferStream` mock added to simulate token-by-token delivery in unit tests
78
+
79
+ ### Added — pendingDiffs protocol (autoFix=false agent coexistence)
80
+
81
+ - **`engine.ts`**: P2 pendingDiffs protocol — when `autoFix: false`, detected errors are posted
82
+ to `session.json` as `PendingDiff` records instead of being discarded; agents or the human
83
+ user can inspect, apply, or reject them independently
84
+ - **`cli/commands/diff.ts`**: `uneven-ai diff` command — lists pending diffs, applies them with
85
+ `--apply <id>` (checksum guard + snapshot + atomic write), rejects with `--reject <id>`,
86
+ clears resolved history with `--clear`
87
+
88
+ ### Fixed — autoFix pipeline: P0+P1 correctness
89
+
90
+ - **`engine.ts`**: P0 — concurrent writes to the same file during autoFix now serialized via a
91
+ per-file `fileFixQueue` — without this, two rapid errors in the same file could produce an
92
+ interleaved, corrupted result
93
+ - **`engine.ts`**: P0 — session lock (`acquireLock`/`releaseLock`) now wraps file writes;
94
+ snapshot taken before every write to support `uneven-ai restore`
95
+ - **`engine.ts`**: P0 — checksum guard before `applyFix` aborts if the file was modified between
96
+ error detection and LLM response, preventing the fix from being applied to the wrong version
97
+ - **`engine.ts`**: P0 — removed early return at the `autoFix=false` branch that silently discarded
98
+ all errors before the session check at line 1183 — `autoFix: false` via `uneven.config.ts`
99
+ was completely inoperative; `pendingDiff` was never posted
100
+ - **`engine.ts`**: P1 — 500 ms debounce applied to both autoFix paths; debounce timers cleared on
101
+ `stop()` to prevent a queued fix from firing after the engine shuts down
102
+ - **`engine.ts`**: P1 — `promptUserConfirmFix` returns `false` in non-TTY environments instead
103
+ of hanging indefinitely waiting for stdin
104
+ - **`engine.ts`**: P1 — `autoFix`/`confirmBeforeFix` shadow variables removed from error handler
105
+ closure — outer config values now used directly, eliminating stale-capture bug
106
+
107
+ ### Fixed — malware scanner
108
+
109
+ - **`supply-chain-auditor.ts`**: `levenshtein` call in typosquatting explanation replaced with
110
+ `editDistance` — function was removed in v0.14.0 audit but one call site in the explanation
111
+ string was missed; caused `ReferenceError` at runtime on any project with typosquat findings
112
+ - **`supply-chain-auditor.ts`**: unified `POPULAR_PACKAGES` list; minimum name length guard added
113
+ to typosquatting — single-character package names produced false positives with edit distance 1
114
+ - **`malware-scanner.ts`**: `isTestFile` now detects `__tests__` directories via `filePath`
115
+ instead of `basename` — test files in `__tests__/` were scanned and flagged as production code
116
+ - **`malware-scanner.ts`**: `computeRiskLevel` now applies confidence threshold symmetrically —
117
+ low-confidence findings no longer elevated the risk level
118
+ - **`malware-scanner.ts`**: `dependenciesScanned` count fixed — was always `0` due to wrong
119
+ variable reference
120
+ - **`malware-scanner.ts`**: LLM evaluation prompt now sanitizes the filename — special characters
121
+ in filenames could break the prompt structure
122
+ - **`malware-scanner.ts`**: non-global regex in `STATIC_RULES` loop now uses `break` guard
123
+ correctly; `peerDependencies` added to dependency scan scope; dead `dirs` variable removed
124
+ - **`scan.ts`**: `spinner.stop()` → `spinner.succeed('Scan complete')` — spinner disappeared
125
+ silently with no completion feedback
126
+ - **`scan.ts`**: logger flushed before `process.exit` to prevent log truncation on findings
127
+ - **`cli/index.ts`**: `--report` help text corrected from `.uneven/` to `uneven-reports/`
128
+
129
+ ### Fixed — pentest & security analyzer
130
+
131
+ - **`security-analyzer.ts`**: `COOKIE_CALL` regex rewritten — options object now captured via
132
+ `\{[^}]*\}` group; `\b` word boundary added before `cookie` to eliminate false positives on
133
+ `_cookie(` and `cookieStore(`
134
+ - **`security-analyzer.ts`**: CORS wildcard+credentials check scoped to each individual
135
+ `cors({...})` block — file-wide flag produced false positives when the two options appeared in
136
+ separate route configs that never co-existed
137
+ - **`security-analyzer.ts`**: `scanHeaders` now respects the `dirs` parameter — was ignoring it
138
+ and always scanning the full project
139
+ - **`security-analyzer.ts`**: `execSync npm audit` replaced with async `execFile` — blocked the
140
+ event loop and could not be interrupted
141
+ - **`security-analyzer.ts`**: `walkDir` skip list hoisted to module-level `Set` — was
142
+ reallocated on every call
143
+ - **`security-analyzer.ts`**: `let mc` redeclaration in CORS_BLOCK loop renamed to `mc2` —
144
+ TypeScript block-scoped variable redeclaration error in same function scope
145
+ - **`pentest-context.ts`**: `matchesTarget` CIDR — captures 4th octet correctly; supports
146
+ `/25`–`/32` prefixes that were silently rejected
147
+ - **`pentest-context.ts`**: credential spray blocked at `>= 50` threads — the `100–499` range
148
+ was missing from the guard condition
149
+ - **`pentest.ts`**: IP octet range and CIDR prefix validated on input — malformed targets
150
+ previously reached the scanner unchecked
151
+
152
+ ### Fixed — terminal watch engine
153
+
154
+ - **`process-lock.ts`**: lock file created with `O_EXCL` (atomic) — race between two processes
155
+ reading "no lock exists" then both writing was possible with the old check-then-create pattern;
156
+ signal handler accumulation on repeated `watch` invocations fixed with `once`
157
+ - **`watcher.ts`**: zombie process prevention — `ProcessWatcher` now guards against
158
+ restart-after-stop; output buffers capped at 100 KB to prevent OOM on runaway processes;
159
+ `restartCount` reset after stability window; spawn errors surfaced; `watch()` resolves on max
160
+ restarts
161
+ - **`file-watcher.ts`**: `running = true` set before the `ready` event so that `stop()` called
162
+ immediately after `start()` actually closes the watcher
163
+
164
+ ### Fixed — data analyst & document intel
165
+
166
+ - **`db-loader.ts`**: `LOAD_LIMIT` applied to `loadDocuments` queries — unbounded `SELECT *` on
167
+ large tables caused OOM on the analyst pipeline
168
+ - **`data-analyst.ts`**: MongoDB schema inference now samples multiple documents — single-document
169
+ sample missed fields that only appeared in a subset of records
170
+ - **`data-analyst.ts`**: `generateSQL` prompt truncated when schema exceeds context budget —
171
+ oversized schema was silently overflowing LLM context
172
+ - **`data-analyst.ts`**: MongoDB `executeQuery` now parses and applies `WHERE` clause filters —
173
+ all MongoDB queries previously returned unfiltered collections
174
+ - **`data-analyst.ts`**: SQLite `PRAGMA table_info` now quotes the table name — table names with
175
+ spaces or reserved words caused a parse error
176
+ - **`data-security-context.ts`**: wildcard column regex fixed to match single-segment names;
177
+ arbitrary `slice(0, 30)` cap on `buildPromptRules` removed — truncated security rules were
178
+ silently omitted from the LLM prompt
179
+ - **`analyze.ts`**: requires explicit `A` or `APPROVE` to execute destructive queries — any
180
+ truthy input previously approved execution
181
+ - **`analyze.ts`**: `dbUrl` (not `dbType`) now passed to `jobManager.createJob` — job was
182
+ created without a connection string, failing on first query
183
+ - **`report-packager.ts`**: `execSync` replaced with async `execFile`; shell injection in
184
+ filename argument fixed
185
+ - **`dashboard-generator.ts`**: `</script>` escaped in JSON chart data to prevent XSS when
186
+ the dashboard HTML is served from a web origin
187
+ - **`excel-exporter.ts`**: `require()` replaced with ESM-compatible dynamic `import()`
188
+ - **`llm-security-reviewer.ts`**: operator precedence bug in `sanitizeContext` fixed — `&&`
189
+ was binding tighter than intended, causing some sensitive patterns to pass through unsanitized
190
+
191
+ ### Fixed — logger & error parser
192
+
193
+ - **`logger/index.ts`**: `getTimestamp` now includes date (`YYYY-MM-DD HH:MM:SS`) — log entries
194
+ across midnight were indistinguishable
195
+ - **`logger/index.ts`**: triple backticks in `fixApplied` before/after code blocks escaped —
196
+ unescaped backticks broke the Markdown fence in the log file
197
+ - **`logger/index.ts`**: `securityFinding` uses `line != null` — `line === 0` (first line of
198
+ file) was treated as falsy and silently omitted from the log
199
+ - **`error-parser.ts`**: `parsePythonErrors` reads exception message at the frame boundary, not
200
+ at `i+1` — off-by-one caused the wrong line to be stored as the error message
201
+ - **`error-parser.ts`**: `getContext` now receives the terminal output index, not the source line
202
+ number — produced context slices from the wrong position
203
+ - **`error-parser.ts`**: `parseRustErrors` searches forward for the `-->` location line —
204
+ fixed cases where the location appeared after the error header, not on the same line
205
+ - **`error-parser.ts`**: `FATAL` mapped to `'error'` and `WARN` mapped to `'warning'` in
206
+ `parseGenericErrors` — unrecognized severity strings were dropped
207
+
208
+ ### Fixed — CLI commands
209
+
210
+ - **`init.ts`**: downloads are now atomic (`.tmp` + rename) — SIGINT during download no longer
211
+ leaves a partial file that `fileExists()` accepts as valid on the next run
212
+ - **`init.ts`**: `readline` wrapped in `try/finally` — `rl.close()` now called on Ctrl+C or
213
+ EOF during the provider prompt, preventing stdin from being held open
214
+ - **`init.ts`**: `fetch()` for model downloads now carries `AbortSignal.timeout(20 * 60 * 1000)`
215
+ — stalled CDN connections previously blocked `init` indefinitely
216
+ - **`init.ts`**: `hardware.gpu.recommendation.gpuLayers` now propagated to both `saveConfig` and
217
+ `makeConfig` — was detected and displayed but always overridden by the hardcoded `gpuLayers: 32`
218
+ default; users with 1.5 GB VRAM received 32 layers and crashed at runtime
219
+ - **`init.ts`**: invalid provider choice now emits a `warn()` instead of silently defaulting —
220
+ a typo during setup produced no diagnostic
221
+ - **`init.ts`**: `uneven-reports/` added to `.gitignore` — HTML pentest and Excel data reports
222
+ were committed accidentally
223
+ - **`init.ts`**: `res.body` null check moved inside the try block so the partial file descriptor
224
+ is cleaned up before throwing
225
+ - **`init.ts`**: `threads` value now derived from `Math.max(2, Math.min(hardware.cpuCores, 8))`
226
+ in both `saveConfig` and `makeConfig` — the two had diverged (2 vs 4)
227
+ - **`init.ts`**: `defaultModelFor(provider)` pure function extracted — duplicate model-resolution
228
+ logic in `makeConfig` and `initCommand` had already diverged
229
+ - **`init.ts`**: `User-Agent` updated to use the runtime `version` string instead of hardcoded
230
+ `'uneven-ai-init/0.7.1'`
231
+ - **`ask.ts`**: `guardian` hoisted before the `try` block so `catch` can call `guardian?.stop()`
232
+ — was a block-scope error; `ResourceGuardian` was never stopped on exception
233
+ - **`ask.ts`**: `config.brain.temperature` forwarded to local inference — `ask` was always using
234
+ the default temperature regardless of user config
235
+ - **`ask.ts`**: tokens now streamed inline — each token was printed on a new line instead of
236
+ appended to the current line
237
+ - **`askf.ts`**: `readline` interface closed in `finally` to prevent stdin leak on any exit path
238
+ - **`askf.ts`**: `path.sep` added to `startsWith` check — sibling directories sharing a prefix
239
+ (e.g. `src/foo-extra`) could bypass the allowed-paths guard
240
+ - **`askf.ts`**: files and summary parsed from the sanitized answer, not from raw tokens —
241
+ streaming tokens arrived in arbitrary chunks, making per-token parsing unreliable
242
+ - **`index.ts` (CLI)**: lock released on all exit paths including early-return guards — a failed
243
+ file read during indexing left the lock held indefinitely
244
+ - **`engine.ts` (index)**: Phase 2B file guards aligned with Phase 2A — binary, ignored-dir, and
245
+ extension checks were missing from the Phase 2B worker path
246
+ - **`diff.ts`**: missing `/**` JSDoc opening delimiter added — absence generated dozens of tsc
247
+ parse errors that saturated compiler output and masked all other real type errors in the project
248
+ - **`engine.ts`**: `'uneven-ai diff reject'` hint corrected to `'uneven-ai diff --reject'`
249
+
250
+ ### Fixed — CI/CD pipeline
251
+
252
+ - **`.github/workflows`**: `timeout-minutes` added to all jobs — runaway builds previously
253
+ consumed the full Actions quota before timing out
254
+ - **`.github/workflows`**: `uneven-ci` job dependency chain fixed; `lint-and-typecheck` added as
255
+ a required dependency of the test job
256
+ - **`local-release.sh`**: `wait_actions` fixed for `workflow_dispatch` trigger — was polling the
257
+ wrong event type and timing out immediately
258
+ - **`build/release`**: tag-push trigger restored for darwin and win32 Actions builds
259
+ - **`postinstall.ts`**: banner box alignment corrected
260
+
261
+ ### Changed
262
+
263
+ - Version bumped to `0.16.0` across `package.json`, `Cargo.toml`, `napi_api.rs`, `session.ts`,
264
+ and `sbom-generator.ts`
265
+ - Local release pipeline added (`build/local-release.sh`) — Linux builds locally, darwin and
266
+ win32 via Actions; win32 cross-compile via MinGW auto-detected if `gcc-mingw` is installed
267
+
268
+ ## [0.15.0] - 2026-04-14
269
+
270
+ ### Performance — local LLM inference
271
+
272
+ - **`llm/inference.rs`**: replaced manual `argmax` with `candle_transformers::generation::LogitsProcessor` —
273
+ supports greedy decode (temperature=0, default) and temperature/top-p sampling
274
+ - **`llm/inference.rs`**: added repetition penalty via `apply_repeat_penalty` on a 64-token sliding
275
+ window of generated output (default penalty=1.1) — prevents degenerate loops that previously
276
+ exhausted the full `max_tokens` budget without hitting EOS
277
+ - **`llm/inference.rs`**: context-window guard — prompts exceeding `max_context - max_tokens` tokens
278
+ are truncated from the left (most-recent context kept); default `max_context=2048`
279
+ - **`llm/inference.rs`**: `tokens.reserve(max_tokens)` — eliminates O(log n) Vec reallocations
280
+ during generation
281
+ - **`api/napi_api.rs`**: `llm_infer` now accepts `temperature: f64` and `repeat_penalty: f64`;
282
+ TypeScript callers default to `temperature=0.0, repeatPenalty=1.1`
283
+
284
+ ## [0.14.1] - 2026-04-14
285
+
286
+ ### Fixed — CI
287
+
288
+ - **`retrieval/mod.rs`**: gated `STORE_USEARCH` and `STORE_META_BIN` constants behind
289
+ `#[cfg(feature = "vector-store")]` — dead-code lint under `-D warnings` blocked CI on v0.14.0
290
+ - **`retrieval/mod.rs`**: replaced `&*store` with `&store` in two `write_binary` call sites —
291
+ `clippy::explicit_auto_deref` lint also blocked CI
292
+ - **`error-parser.ts`**: added `startsWith('PHP ')` fallback to `hasPHP` detection —
293
+ single-line PHP output was never parsed, causing 3 test failures in CI on v0.13.0
294
+
295
+ ## [0.14.0] - 2026-04-14
296
+
297
+ ### Performance — Rust core: vector store dual-backend
298
+
299
+ - **`retrieval/mod.rs`**: replaced JSON persistence with bincode in the default HashMap backend —
300
+ 10–50× smaller files, ~100× faster parse; atomic writes (tmp + rename) prevent corruption
301
+ - **`retrieval/mod.rs`**: added optional HNSW backend (`--features vector-store`) via usearch —
302
+ O(log n) ANN search replaces O(n) brute-force; `StoreMeta` serialized with bincode; automatic
303
+ migration chain from legacy JSON and old bincode formats
304
+ - **`retrieval/mod.rs`**: `VECTOR_STORE` changed from `Mutex` to `RwLock` — concurrent readers
305
+ no longer serialize during search
306
+ - **`embeddings.rs`**: `BERT` changed from `Mutex` to `RwLock` — rayon threads in `batch_embed`
307
+ can now hold concurrent read locks, restoring true parallelism
308
+ - **`api/napi_api.rs`**: `retrieval_search` returns `NapiRetrievalResult` struct instead of JSON
309
+ string — eliminates per-query `serde_json::to_string` + JS `JSON.parse` round-trip
310
+
311
+ ### Performance — TypeScript: indexing pipeline
312
+
313
+ - **`engine.ts`**: `processFile` reads each file once (raw `Buffer`), reuses it for binary
314
+ detection, SHA-256 hash, and content extraction — eliminates two extra syscalls per file
315
+ - **`engine.ts`**: `getFilesRecursive` parallelized with `Promise.all` — subdirectory traversal
316
+ no longer sequential
317
+ - **`engine.ts`**: Phase 2B and Phase 2D use the same worker-pool pattern as Phase 2A —
318
+ `INDEX_CONCURRENCY` scales to `os.cpus().length`
319
+ - **`engine.ts`**: `watch()` busy-wait loop replaced with event-driven `stopSignal` promise —
320
+ eliminates 1 s polling
321
+ - **`engine.ts`**: `IGNORED_DIRS`, `SUPPORTED_EXTENSIONS`, `IGNORED_EXTENSIONS` converted to
322
+ module-level `Set` — O(1) lookup instead of O(n) array scan per file
323
+ - **`incremental-index.ts`**: all sync I/O eliminated; `status`, `diff`, `needsIndexing` are
324
+ fully async; `diff` uses `Promise.all` for concurrent hash checks; `markIndexedWithHash`
325
+ avoids re-reading a file whose buffer was already read by the caller
326
+ - **`supply-chain-auditor.ts`**: `editDistance` rewritten with two rolling rows (O(n) memory)
327
+ and early-exit when row minimum exceeds `maxDist` — O(m×n) → sublinear for typosquatting
328
+ threshold of 2
329
+
330
+ ## [0.13.0] - 2026-04-13
331
+
332
+ ### Fixed — Rust core: critical bug fixes and anti-pattern removal
333
+
334
+ - **`embeddings.rs`**: extracted `embed_sync()` synchronous path for `batch_embed` — previous
335
+ implementation called `futures::executor::block_on(generate_embedding())` inside rayon threads,
336
+ which panicked at runtime because `block_in_place` is only valid on tokio worker threads
337
+ - **`retrieval/mod.rs`**: removed redundant `Arc` wrapping from `VECTOR_STORE` static —
338
+ `Lazy<RwLock<T>>` is already thread-safe; the extra `Arc` added indirection with no benefit
339
+ - **`inference.rs`**: removed dead `prompt_len` variable and its `let _ = prompt_len` suppressor
340
+ - **`retrieval/mod.rs`**: removed unused `use std::sync::Arc` import
341
+
342
+ ### Fixed — Rust core: performance and correctness (previous session)
343
+
344
+ - **`napi_api.rs`**: replaced hardcoded `"0.12.3"` with `UNEVEN_VERSION` constant in `get_engine_info()`
345
+ - **`napi_api.rs`**: removed `spawn_blocking(|| block_on(...))` anti-pattern — CPU-bound LLM
346
+ inference now offloaded correctly via `block_in_place` inside `run_inference`
347
+ - **`inference.rs`**: removed `Arc` from `LLAMA`, `LOADED_MODEL`, `INTERRUPT` statics
348
+ - **`embeddings.rs`**: wrapped BERT forward pass in `block_in_place` to prevent tokio starvation
349
+ - **`retrieval/mod.rs`**: replaced O(n log n) full sort with O(n log k) min-heap in `semantic_search`;
350
+ replaced blocking `std::fs` calls with async `tokio::fs` in `init_store` and `persist_store`
351
+ - **`error_parser.rs`**: regex compiled once via `Lazy<Regex>` instead of on every parse call
352
+ - **`file_loader.rs`**: `is_supported()` changed from O(n) `Vec::contains` to O(1) `HashSet` lookup
353
+ via `OnceLock`; `FileLoader` struct is now zero-sized
354
+ - **`port_scanner.rs`**: `scan_common_ports` changed from sequential to concurrent via `join_all`,
355
+ reducing wall time from ~9 s to ~1 s
356
+
357
+ ### Fixed — TypeScript layer: bugs and anti-patterns
358
+
359
+ - **`git-manager.ts`**: `undoLastFix` and `listFixCommits` were grepping for `"uneven-ai"` but
360
+ `commitFix` generates messages with `"fix(uneven):"` — both commands were completely broken
361
+ - **`bridge.ts`**: duplicate `process.env.UNEVEN_DEBUG` condition `|| process.env.UNEVEN_DEBUG`
362
+ removed (always evaluated the same branch)
363
+ - **`logger/index.ts`**: duplicate `=== '1' || === '1'` condition collapsed to single check
364
+ - **`process-watcher.ts`**: `command.split(' ')` replaced with quoted-arg-aware regex tokenizer —
365
+ commands with quoted arguments (e.g. `npm run "my script"`) were split incorrectly
366
+ - **`process-lock.ts`**: `process.on()` replaced with `process.once()` for exit/signal handlers —
367
+ repeated `watch` invocations were stacking handlers
368
+ - **`web-scraper.ts`**: `fetchMultiple` changed from sequential loop to `Promise.allSettled`;
369
+ `urlToId` hash upgraded from charCode-sum to FNV-1a 32-bit for better distribution
370
+ - **`chunker.ts`**: removed O(n) `.includes()` guard before `.push()` in line-tracking loop —
371
+ was causing chunks to silently drop line references on large files
372
+ - **`error-parser.ts`**: added O(1) pre-scan dispatch — only relevant parsers run per error,
373
+ skipping all 9 parsers for every line
374
+ - **`data-analyst.ts`**: fixed `raw.length + (raw.length - raw.length)` expression (always
375
+ equalled `raw.length`) — total-before-filter count now captured correctly before filtering
376
+ - **`sbom-generator.ts`**: stale fallback version updated
377
+ - **`incremental-index.ts`**: `save()` now wraps atomic write in try/catch and unlinks temp file
378
+ on failure to prevent corrupted state replacing valid state on next run
379
+ - **`cli/index.ts`**: dead third condition in help display logic removed
380
+
381
+ ### Fixed — TypeScript layer: second-pass subtle bugs
382
+
383
+ - **`supply-chain-auditor.ts`**: phantom-package inner condition `const inLock` was always false
384
+ (outer `if` already guaranteed `!lockedPkgs.has(pkgName)`) — dead code removed
385
+ - **`supply-chain-auditor.ts`**: typosquatting loop used `break` on exact name match, exiting
386
+ the entire popular-packages loop instead of skipping only that entry — changed to `continue`
387
+ - **`engine.ts`**: shadowed `threads` re-declaration inside `if (isNativeAvailable())` removed;
388
+ fixed indentation that made `initLlmEngine` appear outside the conditional block
389
+
390
+ ### Changed
391
+
392
+ - Version bumped to `0.13.0` across `package.json`, `Cargo.toml`, `napi_api.rs`,
393
+ `session.ts`, and `sbom-generator.ts`
394
+
8
395
  ## [0.12.0] - 2026-04-12
9
396
 
10
397
  ### Changed — Pure Clean Architecture Refactor (Stages 1 & 2)
package/README.md CHANGED
@@ -11,7 +11,7 @@
11
11
  >
12
12
  > Embeds a **local LLM**, indexes your entire codebase, watches running terminals in real time, **autonomously fixes errors**, scans for **malicious code and compromised dependencies**, runs a built-in **AI data analyst**, performs **scoped security testing**, and generates **shareable reports** — all from a single CLI.
13
13
  >
14
- > **Rust-powered high-performance engine** with **Modular Clean Architecture**. Zero network dependencies.
14
+ > **High-performance native engine** with **Modular Clean Architecture**. Zero network dependencies.
15
15
 
16
16
  ## 🏗️ Architecture
17
17
 
@@ -63,7 +63,7 @@ Requirements vary significantly depending on the **brain provider** you choose.
63
63
 
64
64
  > Uneven AI itself uses ~512 MB. The Ollama daemon handles model memory independently — RAM required depends on the model you pull (e.g. `llama3.2` ~2 GB).
65
65
 
66
- ### Local brain (LLaMA via Candle — no API, no cloud)
66
+ ### Local brain (LLaMA — no API, no cloud)
67
67
 
68
68
  | Item | Minimum | Recommended |
69
69
  |---|---|---|
@@ -72,7 +72,7 @@ Requirements vary significantly depending on the **brain provider** you choose.
72
72
  | Disk | 200 MB (embeddings) | 2 GB+ (local LLM) |
73
73
  | OS | Linux, macOS, Windows 11 (Native) | Linux / macOS |
74
74
 
75
- > The independent local mode loads a high-precision neural model directly into system memory. This ensures 100% offline operation and absolute data sovereignty with zero external dependencies.
75
+ > The independent local mode loads a neural model directly into system memory. This ensures 100% offline operation and absolute data sovereignty with zero external dependencies.
76
76
 
77
77
  ---
78
78
 
@@ -454,20 +454,6 @@ New to Uneven AI? Follow the step-by-step guide to verify each feature works cor
454
454
 
455
455
  **[TESTING_GUIDE.md](./TESTING_GUIDE.md)** — covers 10 scenarios: info/hardware detection, indexing, ask, error detection + auto-fix, malware scanner, data analyst, pentest, CI pipeline, log review, and reset.
456
456
 
457
- ---
458
-
459
- ## Contributing
460
-
461
- ```bash
462
- git clone https://github.com/kreivesler/uneven
463
- cd uneven
464
- npm install
465
- npm run build
466
- npm test
467
- ```
468
-
469
- See [CONTRIBUTING.md](./CONTRIBUTING.md) for the full guide.
470
-
471
457
  ## Support & Issues
472
458
 
473
459
  - 🐛 [Report issues](https://github.com/kreivesler/uneven/issues)
@@ -225,11 +225,14 @@ function buildChartSection(result, chartType) {
225
225
  tension: 0.3,
226
226
  }));
227
227
  }
228
- const chartData = JSON.stringify({
228
+ // Escape </script> inside JSON to prevent premature termination of the
229
+ // surrounding <script> block when database values contain that sequence.
230
+ const escapeScript = (json) => json.replace(/<\/script>/gi, '<\\/script>');
231
+ const chartData = escapeScript(JSON.stringify({
229
232
  labels: chartType === 'scatter' ? undefined : labels,
230
233
  datasets,
231
- });
232
- const chartOptions = JSON.stringify({
234
+ }));
235
+ const chartOptions = escapeScript(JSON.stringify({
233
236
  responsive: true,
234
237
  plugins: {
235
238
  legend: { labels: { color: '#94a3b8' } },
@@ -239,7 +242,7 @@ function buildChartSection(result, chartType) {
239
242
  x: { ticks: { color: '#64748b' }, grid: { color: '#1e293b' } },
240
243
  y: { ticks: { color: '#64748b' }, grid: { color: '#334155' } },
241
244
  },
242
- });
245
+ }));
243
246
  return `<canvas id="mainChart"></canvas>
244
247
  <script>
245
248
  new Chart(document.getElementById('mainChart'), {
@@ -1 +1 @@
1
- {"version":3,"file":"data-analyst.d.ts","sourceRoot":"","sources":["../../../src/application/analysis/data-analyst.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,+BAA+B,CAAA;AAEtD,OAAO,EAAE,mBAAmB,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAA;AAUhF,MAAM,MAAM,MAAM,GAAG,YAAY,GAAG,OAAO,GAAG,QAAQ,GAAG,SAAS,CAAA;AAElE,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,EAAE,OAAO,CAAA;CAClB;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,UAAU,EAAE,CAAA;IACrB,0DAA0D;IAC1D,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAED,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,MAAM,EAAE,CAAA;IACjB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAA;IAC/B,QAAQ,EAAE,MAAM,CAAA;IAChB,UAAU,EAAE,MAAM,CAAA;CACnB;AAED,MAAM,WAAW,cAAc;IAC7B,GAAG,EAAE,MAAM,CAAA;IACX,WAAW,EAAE,MAAM,CAAA;IACnB,gEAAgE;IAChE,cAAc,EAAE,OAAO,CAAA;CACxB;AAOD,qBAAa,WAAW;IACtB,OAAO,CAAC,MAAM,CAAQ;IACtB,OAAO,CAAC,UAAU,CAAY;IAC9B,OAAO,CAAC,MAAM,CAAsB;IACpC,OAAO,CAAC,MAAM,CAAoB;IAClC,QAAQ,CAAC,QAAQ,EAAE,mBAAmB,CAAA;gBAE1B,MAAM,EAAE,MAAM,EAAE,cAAc,CAAC,EAAE,OAAO,CAAC,cAAc,CAAC;IAO9D,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IA+CrC,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAYjC;;;;;;;OAOG;IACG,gBAAgB,CAAC,aAAa,CAAC,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC;IAoDxE;;;OAGG;IACG,kBAAkB,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;IAqC7C;;;OAGG;IACG,eAAe,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;YA2B5B,kBAAkB;YAqBlB,eAAe;YAqBf,gBAAgB;YA4BhB,eAAe;IAwB7B;;;OAGG;IACG,WAAW,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC;IAgE/D;;;OAGG;IACG,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG;QAAE,aAAa,CAAC,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;IAiFpF,SAAS,IAAI,WAAW,EAAE;IAC1B,SAAS,IAAI,MAAM,GAAG,IAAI;CAC3B;AAID,8DAA8D;AAC9D,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAOhD"}
1
+ {"version":3,"file":"data-analyst.d.ts","sourceRoot":"","sources":["../../../src/application/analysis/data-analyst.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,+BAA+B,CAAA;AAEtD,OAAO,EAAE,mBAAmB,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAA;AAUhF,MAAM,MAAM,MAAM,GAAG,YAAY,GAAG,OAAO,GAAG,QAAQ,GAAG,SAAS,CAAA;AAElE,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,EAAE,OAAO,CAAA;CAClB;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,UAAU,EAAE,CAAA;IACrB,0DAA0D;IAC1D,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAED,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,MAAM,EAAE,CAAA;IACjB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAA;IAC/B,QAAQ,EAAE,MAAM,CAAA;IAChB,UAAU,EAAE,MAAM,CAAA;CACnB;AAED,MAAM,WAAW,cAAc;IAC7B,GAAG,EAAE,MAAM,CAAA;IACX,WAAW,EAAE,MAAM,CAAA;IACnB,gEAAgE;IAChE,cAAc,EAAE,OAAO,CAAA;CACxB;AAOD,qBAAa,WAAW;IACtB,OAAO,CAAC,MAAM,CAAQ;IACtB,OAAO,CAAC,UAAU,CAAY;IAC9B,OAAO,CAAC,MAAM,CAAsB;IACpC,OAAO,CAAC,MAAM,CAAoB;IAClC,QAAQ,CAAC,QAAQ,EAAE,mBAAmB,CAAA;gBAE1B,MAAM,EAAE,MAAM,EAAE,cAAc,CAAC,EAAE,OAAO,CAAC,cAAc,CAAC;IAO9D,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IA+CrC,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAYjC;;;;;;;OAOG;IACG,gBAAgB,CAAC,aAAa,CAAC,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC;IAqDxE;;;OAGG;IACG,kBAAkB,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;IAqC7C;;;OAGG;IACG,eAAe,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;YA2B5B,kBAAkB;YAqBlB,eAAe;YAqBf,gBAAgB;YAgChB,eAAe;IAqC7B;;;OAGG;IACG,WAAW,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC;IAsE/D;;;OAGG;IACG,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG;QAAE,aAAa,CAAC,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;IAmFpF,SAAS,IAAI,WAAW,EAAE;IAC1B,SAAS,IAAI,MAAM,GAAG,IAAI;CAC3B;AAID,8DAA8D;AAC9D,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAOhD"}
@@ -124,9 +124,10 @@ export class DataAnalyst {
124
124
  }
125
125
  // ── User table filter ─────────────────────────────────────────────────────
126
126
  if (allowedTables && allowedTables.length > 0) {
127
+ const totalBeforeFilter = raw.length;
127
128
  const allowed = new Set(allowedTables.map(t => t.toLowerCase()));
128
129
  raw = raw.filter(t => allowed.has(t.name.toLowerCase()));
129
- this.logger.info(`DataAnalyst: Filtered to ${raw.length}/${raw.length + (raw.length - raw.length)} tables per user selection`);
130
+ this.logger.info(`DataAnalyst: Filtered to ${raw.length}/${totalBeforeFilter} tables per user selection`);
130
131
  }
131
132
  // ── Table count safety cap ────────────────────────────────────────────────
132
133
  if (raw.length > SCHEMA_TABLE_LIMIT) {
@@ -254,7 +255,11 @@ Suggestions:`;
254
255
  .whereNot('name', 'like', 'sqlite_%');
255
256
  const result = [];
256
257
  for (const { name } of tables) {
257
- const cols = await this.connection.raw(`PRAGMA table_info(${name})`);
258
+ // Double-quote the table name and escape any embedded double-quotes
259
+ // (SQLite identifier quoting: "" → literal ") to prevent malformed
260
+ // PRAGMA calls if a table name contains special characters.
261
+ const quotedName = `"${name.replace(/"/g, '""')}"`;
262
+ const cols = await this.connection.raw(`PRAGMA table_info(${quotedName})`);
258
263
  const countRes = await this.connection.raw(`SELECT COUNT(*) AS cnt FROM "${name}"`);
259
264
  const rowCount = countRes[0]?.cnt ?? 0;
260
265
  result.push({
@@ -274,14 +279,24 @@ Suggestions:`;
274
279
  const collections = await db.listCollections().toArray();
275
280
  const result = [];
276
281
  for (const col of collections) {
277
- const sample = await db.collection(col.name).findOne({});
278
282
  const count = await db.collection(col.name).estimatedDocumentCount();
279
- const columns = sample
280
- ? Object.entries(sample).map(([k, v]) => ({
281
- name: k,
282
- type: Array.isArray(v) ? 'array' : typeof v,
283
- nullable: true,
284
- }))
283
+ // Sample up to 20 documents to build a union of all field names and
284
+ // types. A single findOne() misses fields that only appear in some
285
+ // documents, and returns a near-empty schema for heterogeneous collections.
286
+ const samples = await db
287
+ .collection(col.name)
288
+ .find({})
289
+ .limit(20)
290
+ .toArray();
291
+ // Build a field→type map; last-seen type wins on conflicts
292
+ const fieldMap = new Map();
293
+ for (const doc of samples) {
294
+ for (const [k, v] of Object.entries(doc)) {
295
+ fieldMap.set(k, Array.isArray(v) ? 'array' : typeof v);
296
+ }
297
+ }
298
+ const columns = fieldMap.size > 0
299
+ ? Array.from(fieldMap.entries()).map(([name, type]) => ({ name, type, nullable: true }))
285
300
  : [{ name: '_id', type: 'ObjectId', nullable: false }];
286
301
  result.push({ name: col.name, columns, rowCount: count });
287
302
  }
@@ -300,7 +315,13 @@ Suggestions:`;
300
315
  cannotGenerate: true,
301
316
  };
302
317
  }
318
+ // Cap at 40 tables to keep the prompt within the local LLM's context window.
319
+ // suggestAnalyses uses 20; here we allow more since accuracy matters more.
320
+ // Tables beyond the cap are still protected by Layers 1-3 — they just won't
321
+ // be available for LLM-generated queries in this session.
322
+ const GENERATE_TABLE_LIMIT = 40;
303
323
  const schemaText = this.schema
324
+ .slice(0, GENERATE_TABLE_LIMIT)
304
325
  .map(t => {
305
326
  const cols = t.columns.map(c => ` ${c.name} ${c.type}${c.nullable ? '' : ' NOT NULL'}`).join(',\n');
306
327
  return ` Table: ${t.name}${t.rowCount != null ? ` (~${t.rowCount} rows)` : ''}\n${cols}`;
@@ -380,8 +401,9 @@ SQL:`;
380
401
  rows = await withTimeout(this.connection.raw(sql), DB_QUERY_TIMEOUT_MS, 'SQL query');
381
402
  }
382
403
  else if (this.dbType === 'mongodb') {
383
- // MongoDB: parse the pseudo-SQL to extract collection name and run a find()
384
- // Supports "SELECT ... FROM <collection> [WHERE ...] [LIMIT n]"
404
+ // MongoDB: parse the pseudo-SQL to extract collection name, WHERE filter,
405
+ // and LIMIT, then run a find() with the resulting filter document.
406
+ // Supports: SELECT ... FROM <collection> [WHERE <field> = <value> [AND ...]] [LIMIT n]
385
407
  const fromMatch = /FROM\s+(\w+)/i.exec(sql);
386
408
  const limitMatch = /LIMIT\s+(\d+)/i.exec(sql);
387
409
  if (!fromMatch) {
@@ -389,8 +411,9 @@ SQL:`;
389
411
  }
390
412
  const collName = fromMatch[1];
391
413
  const limit = limitMatch ? parseInt(limitMatch[1]) : 100;
414
+ const filter = parseMongoWhereClause(sql);
392
415
  const db = this.connection.db();
393
- const docs = await withTimeout(db.collection(collName).find({}).limit(limit).toArray(), DB_QUERY_TIMEOUT_MS, 'MongoDB find');
416
+ const docs = await withTimeout(db.collection(collName).find(filter).limit(limit).toArray(), DB_QUERY_TIMEOUT_MS, 'MongoDB find');
394
417
  rows = docs.map((d) => {
395
418
  const flat = {};
396
419
  for (const [k, v] of Object.entries(d)) {
@@ -433,6 +456,49 @@ export function isSafeQuery(sql) {
433
456
  const dangerous = /\b(INSERT|UPDATE|DELETE|DROP|TRUNCATE|ALTER|CREATE|GRANT|REVOKE|EXEC|EXECUTE|PRAGMA\s+(?!TABLE_INFO|INDEX_LIST|FOREIGN_KEY_LIST))\b/;
434
457
  return !dangerous.test(normalized);
435
458
  }
459
+ /**
460
+ * Extract simple equality conditions from a SQL WHERE clause and convert them
461
+ * to a MongoDB filter document.
462
+ *
463
+ * Supports: field = 'value', field = 123, field = true/false/null,
464
+ * combined with AND (OR is not converted — returns {} on OR clauses).
465
+ * Conditions that cannot be parsed are silently skipped so the query still
466
+ * runs rather than throwing, matching the best-effort nature of pseudo-SQL.
467
+ */
468
+ function parseMongoWhereClause(sql) {
469
+ // Extract the WHERE clause (everything between WHERE and ORDER/LIMIT/GROUP/end)
470
+ const whereMatch = /WHERE\s+(.+?)(?:\s+(?:ORDER|LIMIT|GROUP|HAVING)\b|$)/is.exec(sql);
471
+ if (!whereMatch)
472
+ return {};
473
+ const whereClause = whereMatch[1].trim();
474
+ // Bail out on OR conditions — too complex to map reliably to Mongo filter
475
+ if (/\bOR\b/i.test(whereClause))
476
+ return {};
477
+ const filter = {};
478
+ // Match field = value pairs (AND-separated)
479
+ // Handles: field = 'string', field = "string", field = number, field = true/false/null
480
+ const conditionRx = /(\w+)\s*=\s*(?:'([^']*)'|"([^"]*)"|(true|false|null|-?\d+(?:\.\d+)?))/gi;
481
+ let match;
482
+ while ((match = conditionRx.exec(whereClause)) !== null) {
483
+ const field = match[1];
484
+ const strVal = match[2] ?? match[3];
485
+ const literalVal = match[4];
486
+ if (strVal !== undefined) {
487
+ filter[field] = strVal;
488
+ }
489
+ else if (literalVal !== undefined) {
490
+ if (literalVal === 'true')
491
+ filter[field] = true;
492
+ else if (literalVal === 'false')
493
+ filter[field] = false;
494
+ else if (literalVal === 'null')
495
+ filter[field] = null;
496
+ else
497
+ filter[field] = parseFloat(literalVal);
498
+ }
499
+ }
500
+ return filter;
501
+ }
436
502
  function sanitizeUrl(url) {
437
503
  try {
438
504
  const u = new URL(url);
@@ -1 +1 @@
1
- {"version":3,"file":"data-security-context.d.ts","sourceRoot":"","sources":["../../../src/application/analysis/data-security-context.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAIjE,MAAM,WAAW,cAAc;IAC7B;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,EAAE,CAAA;IAEzB;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,EAAE,CAAA;IAExB;;;;OAIG;IACH,sBAAsB,CAAC,EAAE,MAAM,EAAE,CAAA;IAEjC;;;OAGG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAA;CAC3B;AAED,MAAM,WAAW,WAAW;IAC1B,+CAA+C;IAC/C,UAAU,EAAE,OAAO,CAAA;IACnB,sDAAsD;IACtD,OAAO,EAAE,MAAM,EAAE,CAAA;CAClB;AAID;;;GAGG;AACH,eAAO,MAAM,cAAc,EAAE,QAAQ,CAAC,cAAc,CAmDnD,CAAA;AAID,qBAAa,mBAAmB;IAC9B,OAAO,CAAC,MAAM,CAA0B;IACxC,kEAAkE;IAClE,OAAO,CAAC,cAAc,CAAyC;IAC/D,OAAO,CAAC,aAAa,CAA2C;gBAEpD,MAAM,GAAE,OAAO,CAAC,cAAc,CAAM;IAiChD;;;OAGG;IACH,YAAY,CAAC,MAAM,EAAE,WAAW,EAAE,GAAG;QAAE,QAAQ,EAAE,WAAW,EAAE,CAAC;QAAC,KAAK,EAAE,WAAW,CAAA;KAAE;IA6BpF;;;;OAIG;IACH,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,WAAW;IAuClC;;;OAGG;IACH,UAAU,CAAC,MAAM,EAAE,WAAW,GAAG;QAAE,MAAM,EAAE,WAAW,CAAC;QAAC,KAAK,EAAE,WAAW,CAAA;KAAE;IA8C5E;;;OAGG;IACH,gBAAgB,IAAI,MAAM;IAiB1B,SAAS,IAAI,QAAQ,CAAC,cAAc,CAAC;IAIrC,OAAO,CAAC,eAAe;IAUvB,OAAO,CAAC,cAAc;CAGvB"}
1
+ {"version":3,"file":"data-security-context.d.ts","sourceRoot":"","sources":["../../../src/application/analysis/data-security-context.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAIjE,MAAM,WAAW,cAAc;IAC7B;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,EAAE,CAAA;IAEzB;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,EAAE,CAAA;IAExB;;;;OAIG;IACH,sBAAsB,CAAC,EAAE,MAAM,EAAE,CAAA;IAEjC;;;OAGG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAA;CAC3B;AAED,MAAM,WAAW,WAAW;IAC1B,+CAA+C;IAC/C,UAAU,EAAE,OAAO,CAAA;IACnB,sDAAsD;IACtD,OAAO,EAAE,MAAM,EAAE,CAAA;CAClB;AAID;;;GAGG;AACH,eAAO,MAAM,cAAc,EAAE,QAAQ,CAAC,cAAc,CAmDnD,CAAA;AAID,qBAAa,mBAAmB;IAC9B,OAAO,CAAC,MAAM,CAA0B;IACxC,kEAAkE;IAClE,OAAO,CAAC,cAAc,CAAyC;IAC/D,OAAO,CAAC,aAAa,CAA2C;gBAEpD,MAAM,GAAE,OAAO,CAAC,cAAc,CAAM;IAmChD;;;OAGG;IACH,YAAY,CAAC,MAAM,EAAE,WAAW,EAAE,GAAG;QAAE,QAAQ,EAAE,WAAW,EAAE,CAAC;QAAC,KAAK,EAAE,WAAW,CAAA;KAAE;IA6BpF;;;;OAIG;IACH,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,WAAW;IAuClC;;;OAGG;IACH,UAAU,CAAC,MAAM,EAAE,WAAW,GAAG;QAAE,MAAM,EAAE,WAAW,CAAC;QAAC,KAAK,EAAE,WAAW,CAAA;KAAE;IA8C5E;;;OAGG;IACH,gBAAgB,IAAI,MAAM;IAiB1B,SAAS,IAAI,QAAQ,CAAC,cAAc,CAAC;IAIrC,OAAO,CAAC,eAAe;IAUvB,OAAO,CAAC,cAAc;CAGvB"}
@@ -97,7 +97,9 @@ export class DataSecurityContext {
97
97
  for (const col of this.policy.blockedColumns) {
98
98
  if (col.startsWith('*')) {
99
99
  const suffix = col.slice(1);
100
- this.columnPatterns.push({ col, rx: new RegExp(`\\w+${escapeRegex(suffix)}\\b`, 'i') });
100
+ // \w* (zero-or-more) instead of \w+ so a column whose name IS the
101
+ // suffix (e.g. a column literally named "_password") is also matched.
102
+ this.columnPatterns.push({ col, rx: new RegExp(`\\w*${escapeRegex(suffix)}\\b`, 'i') });
101
103
  }
102
104
  else {
103
105
  this.columnPatterns.push({ col, rx: new RegExp(`\\b${escapeRegex(col)}\\b`, 'i') });
@@ -223,7 +225,7 @@ export class DataSecurityContext {
223
225
  const wildcards = this.policy.blockedColumns.filter(c => c.startsWith('*'));
224
226
  const lines = [
225
227
  'SECURITY RULES — you MUST follow these unconditionally:',
226
- '- NEVER SELECT: ' + exactCols.slice(0, 30).join(', '),
228
+ '- NEVER SELECT: ' + exactCols.join(', '),
227
229
  wildcards.length > 0
228
230
  ? '- NEVER SELECT any column whose name ends with: ' + wildcards.map(w => w.slice(1)).join(', ')
229
231
  : '',