uneven-ai 0.12.6 → 1.0.2

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 +381 -0
  2. package/README.md +3 -3
  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,387 @@ 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.2] - 2026-04-15
9
+
10
+ ### Fixed
11
+
12
+ - **CI (release)**: rename prebuild assets to unique filenames (`linux-x64.node`, etc.) before
13
+ uploading to GitHub Release — previously all 5 platforms shared the name `uneven_core.node`,
14
+ causing asset upload to fail with HTTP 404
15
+
16
+ ## [1.0.1] - 2026-04-15
17
+
18
+ ### Fixed
19
+
20
+ - **CI (linux-arm64)**: install `g++-aarch64-linux-gnu` and set `CXX_aarch64_unknown_linux_gnu`
21
+ so crates with C++ build scripts (e.g. `esaxx-rs`) cross-compile correctly on the release pipeline
22
+
23
+ ## [1.0.0] - 2026-04-15
24
+
25
+ ### Added — Test suite
26
+
27
+ - **`__tests__/engine.test.ts`**: 26 unit tests covering the orchestration engine — `init()`,
28
+ `stop()`, `handleErrorFix()` (anti-loop, deduplication, file queue), `_runFixForError()`
29
+ (confidence gate, autoFix on/off paths, fix-applied/fix-reverted/fix-pending events)
30
+ - **`crates/.../error_parser.rs`**: 9 Rust unit tests — TypeScript, absolute path, relative,
31
+ nested path formats; edge cases: empty string, missing column, plain URL rejection
32
+ - **`crates/.../retrieval/mod.rs`**: 11 Rust unit tests — `cosine_sim` (identical, orthogonal,
33
+ opposite, mismatched lengths, empty vectors) and `assemble_context` (empty, single, multiple
34
+ documents, score formatting, header presence)
35
+ - **`crates/.../providers.rs`**: 4 Rust unit tests — all valid providers, case-insensitive
36
+ parsing, unknown provider error, error message content
37
+ - **`crates/.../embeddings.rs`**: 7 pure Rust unit tests — `normalize_l2` (unit vector, scaling,
38
+ zero-vector error), `hash_embed` (dimension, normalization, determinism, distinctness), `fnv3`
39
+ (determinism, collision); existing 4 model-dependent tests fixed (`#[ignore]` + multi-thread
40
+ runtime to prevent `block_in_place` panic)
41
+ - **Total**: 135 TypeScript tests (7 suites) + 34 Rust tests (0 failures, 4 ignored)
42
+
43
+ ### Added — CI workflow
44
+
45
+ - **`.github/workflows/ci.yml`**: `cargo test --lib` added to `build-rust` job — pure Rust
46
+ unit tests run on every push to `main`; Jest job decoupled from full Rust build (bridge
47
+ mocked, binary not required); `cargo clippy -D warnings` catches lint regressions before
48
+ they reach a release tag
49
+
50
+ ### Fixed — ESM test infrastructure
51
+
52
+ - **`jest.config.cjs`**: bridge `moduleNameMapper` patterns reordered before the `.js` stripper
53
+ so patterns match `bridge.js` before the extension is stripped
54
+ - **`__tests__/engine.test.ts`**: uses `jest.unstable_mockModule()` + explicit
55
+ `import { jest } from '@jest/globals'` — required for `--experimental-vm-modules` where
56
+ `jest` is not injected as a bare global into ESM module scope
57
+ - **`__tests__/error-parser.test.ts`**: Python traceback fixture corrected — added the
58
+ intermediate executing-code line that the parser requires to identify the last stack frame
59
+
60
+ ## [0.16.0] - 2026-04-15
61
+
62
+ ### Added — NAPI streaming inference
63
+
64
+ - **`inference.rs`**: `generate_streaming` and `run_inference_streaming` — token-by-token generation
65
+ via callback, enabling progressive output instead of waiting for full completion
66
+ - **`llm/orchestrator.rs`**: `infer_with_params_streaming` wires the streaming path end-to-end
67
+ - **`api/napi_api.rs`**: `llm_infer_stream` exposed as a NAPI function with `ThreadsafeFunction`
68
+ callback — each generated token is delivered to JS as it is produced
69
+ - **`bridge.ts`**: `llmInferStream` bound and wired into `engine.ts ask()` — `ask` and `askf`
70
+ commands now stream tokens inline instead of printing the full response at once
71
+ - **`test/mocks`**: `llmInferStream` mock added to simulate token-by-token delivery in unit tests
72
+
73
+ ### Added — pendingDiffs protocol (autoFix=false agent coexistence)
74
+
75
+ - **`engine.ts`**: P2 pendingDiffs protocol — when `autoFix: false`, detected errors are posted
76
+ to `session.json` as `PendingDiff` records instead of being discarded; agents or the human
77
+ user can inspect, apply, or reject them independently
78
+ - **`cli/commands/diff.ts`**: `uneven-ai diff` command — lists pending diffs, applies them with
79
+ `--apply <id>` (checksum guard + snapshot + atomic write), rejects with `--reject <id>`,
80
+ clears resolved history with `--clear`
81
+
82
+ ### Fixed — autoFix pipeline: P0+P1 correctness
83
+
84
+ - **`engine.ts`**: P0 — concurrent writes to the same file during autoFix now serialized via a
85
+ per-file `fileFixQueue` — without this, two rapid errors in the same file could produce an
86
+ interleaved, corrupted result
87
+ - **`engine.ts`**: P0 — session lock (`acquireLock`/`releaseLock`) now wraps file writes;
88
+ snapshot taken before every write to support `uneven-ai restore`
89
+ - **`engine.ts`**: P0 — checksum guard before `applyFix` aborts if the file was modified between
90
+ error detection and LLM response, preventing the fix from being applied to the wrong version
91
+ - **`engine.ts`**: P0 — removed early return at the `autoFix=false` branch that silently discarded
92
+ all errors before the session check at line 1183 — `autoFix: false` via `uneven.config.ts`
93
+ was completely inoperative; `pendingDiff` was never posted
94
+ - **`engine.ts`**: P1 — 500 ms debounce applied to both autoFix paths; debounce timers cleared on
95
+ `stop()` to prevent a queued fix from firing after the engine shuts down
96
+ - **`engine.ts`**: P1 — `promptUserConfirmFix` returns `false` in non-TTY environments instead
97
+ of hanging indefinitely waiting for stdin
98
+ - **`engine.ts`**: P1 — `autoFix`/`confirmBeforeFix` shadow variables removed from error handler
99
+ closure — outer config values now used directly, eliminating stale-capture bug
100
+
101
+ ### Fixed — malware scanner
102
+
103
+ - **`supply-chain-auditor.ts`**: `levenshtein` call in typosquatting explanation replaced with
104
+ `editDistance` — function was removed in v0.14.0 audit but one call site in the explanation
105
+ string was missed; caused `ReferenceError` at runtime on any project with typosquat findings
106
+ - **`supply-chain-auditor.ts`**: unified `POPULAR_PACKAGES` list; minimum name length guard added
107
+ to typosquatting — single-character package names produced false positives with edit distance 1
108
+ - **`malware-scanner.ts`**: `isTestFile` now detects `__tests__` directories via `filePath`
109
+ instead of `basename` — test files in `__tests__/` were scanned and flagged as production code
110
+ - **`malware-scanner.ts`**: `computeRiskLevel` now applies confidence threshold symmetrically —
111
+ low-confidence findings no longer elevated the risk level
112
+ - **`malware-scanner.ts`**: `dependenciesScanned` count fixed — was always `0` due to wrong
113
+ variable reference
114
+ - **`malware-scanner.ts`**: LLM evaluation prompt now sanitizes the filename — special characters
115
+ in filenames could break the prompt structure
116
+ - **`malware-scanner.ts`**: non-global regex in `STATIC_RULES` loop now uses `break` guard
117
+ correctly; `peerDependencies` added to dependency scan scope; dead `dirs` variable removed
118
+ - **`scan.ts`**: `spinner.stop()` → `spinner.succeed('Scan complete')` — spinner disappeared
119
+ silently with no completion feedback
120
+ - **`scan.ts`**: logger flushed before `process.exit` to prevent log truncation on findings
121
+ - **`cli/index.ts`**: `--report` help text corrected from `.uneven/` to `uneven-reports/`
122
+
123
+ ### Fixed — pentest & security analyzer
124
+
125
+ - **`security-analyzer.ts`**: `COOKIE_CALL` regex rewritten — options object now captured via
126
+ `\{[^}]*\}` group; `\b` word boundary added before `cookie` to eliminate false positives on
127
+ `_cookie(` and `cookieStore(`
128
+ - **`security-analyzer.ts`**: CORS wildcard+credentials check scoped to each individual
129
+ `cors({...})` block — file-wide flag produced false positives when the two options appeared in
130
+ separate route configs that never co-existed
131
+ - **`security-analyzer.ts`**: `scanHeaders` now respects the `dirs` parameter — was ignoring it
132
+ and always scanning the full project
133
+ - **`security-analyzer.ts`**: `execSync npm audit` replaced with async `execFile` — blocked the
134
+ event loop and could not be interrupted
135
+ - **`security-analyzer.ts`**: `walkDir` skip list hoisted to module-level `Set` — was
136
+ reallocated on every call
137
+ - **`security-analyzer.ts`**: `let mc` redeclaration in CORS_BLOCK loop renamed to `mc2` —
138
+ TypeScript block-scoped variable redeclaration error in same function scope
139
+ - **`pentest-context.ts`**: `matchesTarget` CIDR — captures 4th octet correctly; supports
140
+ `/25`–`/32` prefixes that were silently rejected
141
+ - **`pentest-context.ts`**: credential spray blocked at `>= 50` threads — the `100–499` range
142
+ was missing from the guard condition
143
+ - **`pentest.ts`**: IP octet range and CIDR prefix validated on input — malformed targets
144
+ previously reached the scanner unchecked
145
+
146
+ ### Fixed — terminal watch engine
147
+
148
+ - **`process-lock.ts`**: lock file created with `O_EXCL` (atomic) — race between two processes
149
+ reading "no lock exists" then both writing was possible with the old check-then-create pattern;
150
+ signal handler accumulation on repeated `watch` invocations fixed with `once`
151
+ - **`watcher.ts`**: zombie process prevention — `ProcessWatcher` now guards against
152
+ restart-after-stop; output buffers capped at 100 KB to prevent OOM on runaway processes;
153
+ `restartCount` reset after stability window; spawn errors surfaced; `watch()` resolves on max
154
+ restarts
155
+ - **`file-watcher.ts`**: `running = true` set before the `ready` event so that `stop()` called
156
+ immediately after `start()` actually closes the watcher
157
+
158
+ ### Fixed — data analyst & document intel
159
+
160
+ - **`db-loader.ts`**: `LOAD_LIMIT` applied to `loadDocuments` queries — unbounded `SELECT *` on
161
+ large tables caused OOM on the analyst pipeline
162
+ - **`data-analyst.ts`**: MongoDB schema inference now samples multiple documents — single-document
163
+ sample missed fields that only appeared in a subset of records
164
+ - **`data-analyst.ts`**: `generateSQL` prompt truncated when schema exceeds context budget —
165
+ oversized schema was silently overflowing LLM context
166
+ - **`data-analyst.ts`**: MongoDB `executeQuery` now parses and applies `WHERE` clause filters —
167
+ all MongoDB queries previously returned unfiltered collections
168
+ - **`data-analyst.ts`**: SQLite `PRAGMA table_info` now quotes the table name — table names with
169
+ spaces or reserved words caused a parse error
170
+ - **`data-security-context.ts`**: wildcard column regex fixed to match single-segment names;
171
+ arbitrary `slice(0, 30)` cap on `buildPromptRules` removed — truncated security rules were
172
+ silently omitted from the LLM prompt
173
+ - **`analyze.ts`**: requires explicit `A` or `APPROVE` to execute destructive queries — any
174
+ truthy input previously approved execution
175
+ - **`analyze.ts`**: `dbUrl` (not `dbType`) now passed to `jobManager.createJob` — job was
176
+ created without a connection string, failing on first query
177
+ - **`report-packager.ts`**: `execSync` replaced with async `execFile`; shell injection in
178
+ filename argument fixed
179
+ - **`dashboard-generator.ts`**: `</script>` escaped in JSON chart data to prevent XSS when
180
+ the dashboard HTML is served from a web origin
181
+ - **`excel-exporter.ts`**: `require()` replaced with ESM-compatible dynamic `import()`
182
+ - **`llm-security-reviewer.ts`**: operator precedence bug in `sanitizeContext` fixed — `&&`
183
+ was binding tighter than intended, causing some sensitive patterns to pass through unsanitized
184
+
185
+ ### Fixed — logger & error parser
186
+
187
+ - **`logger/index.ts`**: `getTimestamp` now includes date (`YYYY-MM-DD HH:MM:SS`) — log entries
188
+ across midnight were indistinguishable
189
+ - **`logger/index.ts`**: triple backticks in `fixApplied` before/after code blocks escaped —
190
+ unescaped backticks broke the Markdown fence in the log file
191
+ - **`logger/index.ts`**: `securityFinding` uses `line != null` — `line === 0` (first line of
192
+ file) was treated as falsy and silently omitted from the log
193
+ - **`error-parser.ts`**: `parsePythonErrors` reads exception message at the frame boundary, not
194
+ at `i+1` — off-by-one caused the wrong line to be stored as the error message
195
+ - **`error-parser.ts`**: `getContext` now receives the terminal output index, not the source line
196
+ number — produced context slices from the wrong position
197
+ - **`error-parser.ts`**: `parseRustErrors` searches forward for the `-->` location line —
198
+ fixed cases where the location appeared after the error header, not on the same line
199
+ - **`error-parser.ts`**: `FATAL` mapped to `'error'` and `WARN` mapped to `'warning'` in
200
+ `parseGenericErrors` — unrecognized severity strings were dropped
201
+
202
+ ### Fixed — CLI commands
203
+
204
+ - **`init.ts`**: downloads are now atomic (`.tmp` + rename) — SIGINT during download no longer
205
+ leaves a partial file that `fileExists()` accepts as valid on the next run
206
+ - **`init.ts`**: `readline` wrapped in `try/finally` — `rl.close()` now called on Ctrl+C or
207
+ EOF during the provider prompt, preventing stdin from being held open
208
+ - **`init.ts`**: `fetch()` for model downloads now carries `AbortSignal.timeout(20 * 60 * 1000)`
209
+ — stalled CDN connections previously blocked `init` indefinitely
210
+ - **`init.ts`**: `hardware.gpu.recommendation.gpuLayers` now propagated to both `saveConfig` and
211
+ `makeConfig` — was detected and displayed but always overridden by the hardcoded `gpuLayers: 32`
212
+ default; users with 1.5 GB VRAM received 32 layers and crashed at runtime
213
+ - **`init.ts`**: invalid provider choice now emits a `warn()` instead of silently defaulting —
214
+ a typo during setup produced no diagnostic
215
+ - **`init.ts`**: `uneven-reports/` added to `.gitignore` — HTML pentest and Excel data reports
216
+ were committed accidentally
217
+ - **`init.ts`**: `res.body` null check moved inside the try block so the partial file descriptor
218
+ is cleaned up before throwing
219
+ - **`init.ts`**: `threads` value now derived from `Math.max(2, Math.min(hardware.cpuCores, 8))`
220
+ in both `saveConfig` and `makeConfig` — the two had diverged (2 vs 4)
221
+ - **`init.ts`**: `defaultModelFor(provider)` pure function extracted — duplicate model-resolution
222
+ logic in `makeConfig` and `initCommand` had already diverged
223
+ - **`init.ts`**: `User-Agent` updated to use the runtime `version` string instead of hardcoded
224
+ `'uneven-ai-init/0.7.1'`
225
+ - **`ask.ts`**: `guardian` hoisted before the `try` block so `catch` can call `guardian?.stop()`
226
+ — was a block-scope error; `ResourceGuardian` was never stopped on exception
227
+ - **`ask.ts`**: `config.brain.temperature` forwarded to local inference — `ask` was always using
228
+ the default temperature regardless of user config
229
+ - **`ask.ts`**: tokens now streamed inline — each token was printed on a new line instead of
230
+ appended to the current line
231
+ - **`askf.ts`**: `readline` interface closed in `finally` to prevent stdin leak on any exit path
232
+ - **`askf.ts`**: `path.sep` added to `startsWith` check — sibling directories sharing a prefix
233
+ (e.g. `src/foo-extra`) could bypass the allowed-paths guard
234
+ - **`askf.ts`**: files and summary parsed from the sanitized answer, not from raw tokens —
235
+ streaming tokens arrived in arbitrary chunks, making per-token parsing unreliable
236
+ - **`index.ts` (CLI)**: lock released on all exit paths including early-return guards — a failed
237
+ file read during indexing left the lock held indefinitely
238
+ - **`engine.ts` (index)**: Phase 2B file guards aligned with Phase 2A — binary, ignored-dir, and
239
+ extension checks were missing from the Phase 2B worker path
240
+ - **`diff.ts`**: missing `/**` JSDoc opening delimiter added — absence generated dozens of tsc
241
+ parse errors that saturated compiler output and masked all other real type errors in the project
242
+ - **`engine.ts`**: `'uneven-ai diff reject'` hint corrected to `'uneven-ai diff --reject'`
243
+
244
+ ### Fixed — CI/CD pipeline
245
+
246
+ - **`.github/workflows`**: `timeout-minutes` added to all jobs — runaway builds previously
247
+ consumed the full Actions quota before timing out
248
+ - **`.github/workflows`**: `uneven-ci` job dependency chain fixed; `lint-and-typecheck` added as
249
+ a required dependency of the test job
250
+ - **`local-release.sh`**: `wait_actions` fixed for `workflow_dispatch` trigger — was polling the
251
+ wrong event type and timing out immediately
252
+ - **`build/release`**: tag-push trigger restored for darwin and win32 Actions builds
253
+ - **`postinstall.ts`**: banner box alignment corrected
254
+
255
+ ### Changed
256
+
257
+ - Version bumped to `0.16.0` across `package.json`, `Cargo.toml`, `napi_api.rs`, `session.ts`,
258
+ and `sbom-generator.ts`
259
+ - Local release pipeline added (`build/local-release.sh`) — Linux builds locally, darwin and
260
+ win32 via Actions; win32 cross-compile via MinGW auto-detected if `gcc-mingw` is installed
261
+
262
+ ## [0.15.0] - 2026-04-14
263
+
264
+ ### Performance — local LLM inference
265
+
266
+ - **`llm/inference.rs`**: replaced manual `argmax` with `candle_transformers::generation::LogitsProcessor` —
267
+ supports greedy decode (temperature=0, default) and temperature/top-p sampling
268
+ - **`llm/inference.rs`**: added repetition penalty via `apply_repeat_penalty` on a 64-token sliding
269
+ window of generated output (default penalty=1.1) — prevents degenerate loops that previously
270
+ exhausted the full `max_tokens` budget without hitting EOS
271
+ - **`llm/inference.rs`**: context-window guard — prompts exceeding `max_context - max_tokens` tokens
272
+ are truncated from the left (most-recent context kept); default `max_context=2048`
273
+ - **`llm/inference.rs`**: `tokens.reserve(max_tokens)` — eliminates O(log n) Vec reallocations
274
+ during generation
275
+ - **`api/napi_api.rs`**: `llm_infer` now accepts `temperature: f64` and `repeat_penalty: f64`;
276
+ TypeScript callers default to `temperature=0.0, repeatPenalty=1.1`
277
+
278
+ ## [0.14.1] - 2026-04-14
279
+
280
+ ### Fixed — CI
281
+
282
+ - **`retrieval/mod.rs`**: gated `STORE_USEARCH` and `STORE_META_BIN` constants behind
283
+ `#[cfg(feature = "vector-store")]` — dead-code lint under `-D warnings` blocked CI on v0.14.0
284
+ - **`retrieval/mod.rs`**: replaced `&*store` with `&store` in two `write_binary` call sites —
285
+ `clippy::explicit_auto_deref` lint also blocked CI
286
+ - **`error-parser.ts`**: added `startsWith('PHP ')` fallback to `hasPHP` detection —
287
+ single-line PHP output was never parsed, causing 3 test failures in CI on v0.13.0
288
+
289
+ ## [0.14.0] - 2026-04-14
290
+
291
+ ### Performance — Rust core: vector store dual-backend
292
+
293
+ - **`retrieval/mod.rs`**: replaced JSON persistence with bincode in the default HashMap backend —
294
+ 10–50× smaller files, ~100× faster parse; atomic writes (tmp + rename) prevent corruption
295
+ - **`retrieval/mod.rs`**: added optional HNSW backend (`--features vector-store`) via usearch —
296
+ O(log n) ANN search replaces O(n) brute-force; `StoreMeta` serialized with bincode; automatic
297
+ migration chain from legacy JSON and old bincode formats
298
+ - **`retrieval/mod.rs`**: `VECTOR_STORE` changed from `Mutex` to `RwLock` — concurrent readers
299
+ no longer serialize during search
300
+ - **`embeddings.rs`**: `BERT` changed from `Mutex` to `RwLock` — rayon threads in `batch_embed`
301
+ can now hold concurrent read locks, restoring true parallelism
302
+ - **`api/napi_api.rs`**: `retrieval_search` returns `NapiRetrievalResult` struct instead of JSON
303
+ string — eliminates per-query `serde_json::to_string` + JS `JSON.parse` round-trip
304
+
305
+ ### Performance — TypeScript: indexing pipeline
306
+
307
+ - **`engine.ts`**: `processFile` reads each file once (raw `Buffer`), reuses it for binary
308
+ detection, SHA-256 hash, and content extraction — eliminates two extra syscalls per file
309
+ - **`engine.ts`**: `getFilesRecursive` parallelized with `Promise.all` — subdirectory traversal
310
+ no longer sequential
311
+ - **`engine.ts`**: Phase 2B and Phase 2D use the same worker-pool pattern as Phase 2A —
312
+ `INDEX_CONCURRENCY` scales to `os.cpus().length`
313
+ - **`engine.ts`**: `watch()` busy-wait loop replaced with event-driven `stopSignal` promise —
314
+ eliminates 1 s polling
315
+ - **`engine.ts`**: `IGNORED_DIRS`, `SUPPORTED_EXTENSIONS`, `IGNORED_EXTENSIONS` converted to
316
+ module-level `Set` — O(1) lookup instead of O(n) array scan per file
317
+ - **`incremental-index.ts`**: all sync I/O eliminated; `status`, `diff`, `needsIndexing` are
318
+ fully async; `diff` uses `Promise.all` for concurrent hash checks; `markIndexedWithHash`
319
+ avoids re-reading a file whose buffer was already read by the caller
320
+ - **`supply-chain-auditor.ts`**: `editDistance` rewritten with two rolling rows (O(n) memory)
321
+ and early-exit when row minimum exceeds `maxDist` — O(m×n) → sublinear for typosquatting
322
+ threshold of 2
323
+
324
+ ## [0.13.0] - 2026-04-13
325
+
326
+ ### Fixed — Rust core: critical bug fixes and anti-pattern removal
327
+
328
+ - **`embeddings.rs`**: extracted `embed_sync()` synchronous path for `batch_embed` — previous
329
+ implementation called `futures::executor::block_on(generate_embedding())` inside rayon threads,
330
+ which panicked at runtime because `block_in_place` is only valid on tokio worker threads
331
+ - **`retrieval/mod.rs`**: removed redundant `Arc` wrapping from `VECTOR_STORE` static —
332
+ `Lazy<RwLock<T>>` is already thread-safe; the extra `Arc` added indirection with no benefit
333
+ - **`inference.rs`**: removed dead `prompt_len` variable and its `let _ = prompt_len` suppressor
334
+ - **`retrieval/mod.rs`**: removed unused `use std::sync::Arc` import
335
+
336
+ ### Fixed — Rust core: performance and correctness (previous session)
337
+
338
+ - **`napi_api.rs`**: replaced hardcoded `"0.12.3"` with `UNEVEN_VERSION` constant in `get_engine_info()`
339
+ - **`napi_api.rs`**: removed `spawn_blocking(|| block_on(...))` anti-pattern — CPU-bound LLM
340
+ inference now offloaded correctly via `block_in_place` inside `run_inference`
341
+ - **`inference.rs`**: removed `Arc` from `LLAMA`, `LOADED_MODEL`, `INTERRUPT` statics
342
+ - **`embeddings.rs`**: wrapped BERT forward pass in `block_in_place` to prevent tokio starvation
343
+ - **`retrieval/mod.rs`**: replaced O(n log n) full sort with O(n log k) min-heap in `semantic_search`;
344
+ replaced blocking `std::fs` calls with async `tokio::fs` in `init_store` and `persist_store`
345
+ - **`error_parser.rs`**: regex compiled once via `Lazy<Regex>` instead of on every parse call
346
+ - **`file_loader.rs`**: `is_supported()` changed from O(n) `Vec::contains` to O(1) `HashSet` lookup
347
+ via `OnceLock`; `FileLoader` struct is now zero-sized
348
+ - **`port_scanner.rs`**: `scan_common_ports` changed from sequential to concurrent via `join_all`,
349
+ reducing wall time from ~9 s to ~1 s
350
+
351
+ ### Fixed — TypeScript layer: bugs and anti-patterns
352
+
353
+ - **`git-manager.ts`**: `undoLastFix` and `listFixCommits` were grepping for `"uneven-ai"` but
354
+ `commitFix` generates messages with `"fix(uneven):"` — both commands were completely broken
355
+ - **`bridge.ts`**: duplicate `process.env.UNEVEN_DEBUG` condition `|| process.env.UNEVEN_DEBUG`
356
+ removed (always evaluated the same branch)
357
+ - **`logger/index.ts`**: duplicate `=== '1' || === '1'` condition collapsed to single check
358
+ - **`process-watcher.ts`**: `command.split(' ')` replaced with quoted-arg-aware regex tokenizer —
359
+ commands with quoted arguments (e.g. `npm run "my script"`) were split incorrectly
360
+ - **`process-lock.ts`**: `process.on()` replaced with `process.once()` for exit/signal handlers —
361
+ repeated `watch` invocations were stacking handlers
362
+ - **`web-scraper.ts`**: `fetchMultiple` changed from sequential loop to `Promise.allSettled`;
363
+ `urlToId` hash upgraded from charCode-sum to FNV-1a 32-bit for better distribution
364
+ - **`chunker.ts`**: removed O(n) `.includes()` guard before `.push()` in line-tracking loop —
365
+ was causing chunks to silently drop line references on large files
366
+ - **`error-parser.ts`**: added O(1) pre-scan dispatch — only relevant parsers run per error,
367
+ skipping all 9 parsers for every line
368
+ - **`data-analyst.ts`**: fixed `raw.length + (raw.length - raw.length)` expression (always
369
+ equalled `raw.length`) — total-before-filter count now captured correctly before filtering
370
+ - **`sbom-generator.ts`**: stale fallback version updated
371
+ - **`incremental-index.ts`**: `save()` now wraps atomic write in try/catch and unlinks temp file
372
+ on failure to prevent corrupted state replacing valid state on next run
373
+ - **`cli/index.ts`**: dead third condition in help display logic removed
374
+
375
+ ### Fixed — TypeScript layer: second-pass subtle bugs
376
+
377
+ - **`supply-chain-auditor.ts`**: phantom-package inner condition `const inLock` was always false
378
+ (outer `if` already guaranteed `!lockedPkgs.has(pkgName)`) — dead code removed
379
+ - **`supply-chain-auditor.ts`**: typosquatting loop used `break` on exact name match, exiting
380
+ the entire popular-packages loop instead of skipping only that entry — changed to `continue`
381
+ - **`engine.ts`**: shadowed `threads` re-declaration inside `if (isNativeAvailable())` removed;
382
+ fixed indentation that made `initLlmEngine` appear outside the conditional block
383
+
384
+ ### Changed
385
+
386
+ - Version bumped to `0.13.0` across `package.json`, `Cargo.toml`, `napi_api.rs`,
387
+ `session.ts`, and `sbom-generator.ts`
388
+
8
389
  ## [0.12.0] - 2026-04-12
9
390
 
10
391
  ### 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
 
@@ -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
  : '',
@@ -1 +1 @@
1
- {"version":3,"file":"excel-exporter.d.ts","sourceRoot":"","sources":["../../../src/application/analysis/excel-exporter.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAIpD,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,wCAAwC;IACxC,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB,6CAA6C;IAC7C,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,EAAE,MAAM,CAAA;IACd,WAAW,EAAE,MAAM,CAAA;CACpB;AAED,qBAAa,aAAa;IACxB;;OAEG;IACG,YAAY,CAAC,MAAM,EAAE,WAAW,EAAE,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,YAAY,CAAC;IAsBtF;;OAEG;IACG,cAAc,CAClB,OAAO,EAAE,KAAK,CAAC;QAAE,MAAM,EAAE,WAAW,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC,EAC1D,OAAO,EAAE,aAAa,GACrB,OAAO,CAAC,YAAY,CAAC;IA2BxB,OAAO,CAAC,UAAU;CAwEnB"}
1
+ {"version":3,"file":"excel-exporter.d.ts","sourceRoot":"","sources":["../../../src/application/analysis/excel-exporter.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAIpD,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,wCAAwC;IACxC,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB,6CAA6C;IAC7C,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,EAAE,MAAM,CAAA;IACd,WAAW,EAAE,MAAM,CAAA;CACpB;AAED,qBAAa,aAAa;IACxB;;OAEG;IACG,YAAY,CAAC,MAAM,EAAE,WAAW,EAAE,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,YAAY,CAAC;IAqBtF;;OAEG;IACG,cAAc,CAClB,OAAO,EAAE,KAAK,CAAC;QAAE,MAAM,EAAE,WAAW,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC,EAC1D,OAAO,EAAE,aAAa,GACrB,OAAO,CAAC,YAAY,CAAC;IA0BxB,OAAO,CAAC,UAAU;CAwEnB"}