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.
- package/CHANGELOG.md +387 -0
- package/README.md +3 -17
- package/dist/application/analysis/dashboard-generator.js +7 -4
- package/dist/application/analysis/data-analyst.d.ts.map +1 -1
- package/dist/application/analysis/data-analyst.js +78 -12
- package/dist/application/analysis/data-security-context.d.ts.map +1 -1
- package/dist/application/analysis/data-security-context.js +4 -2
- package/dist/application/analysis/excel-exporter.d.ts.map +1 -1
- package/dist/application/analysis/excel-exporter.js +2 -4
- package/dist/application/analysis/llm-security-reviewer.js +1 -1
- package/dist/application/analysis/malware-scanner.d.ts.map +1 -1
- package/dist/application/analysis/malware-scanner.js +36 -36
- package/dist/application/analysis/pentest-security-context.js +26 -8
- package/dist/application/analysis/report-packager.d.ts.map +1 -1
- package/dist/application/analysis/report-packager.js +18 -9
- package/dist/application/analysis/sbom-generator.js +1 -1
- package/dist/application/analysis/security-analyzer.d.ts +1 -1
- package/dist/application/analysis/security-analyzer.d.ts.map +1 -1
- package/dist/application/analysis/security-analyzer.js +69 -34
- package/dist/application/analysis/supply-chain-auditor.d.ts +15 -0
- package/dist/application/analysis/supply-chain-auditor.d.ts.map +1 -1
- package/dist/application/analysis/supply-chain-auditor.js +86 -42
- package/dist/application/development/fix-engine.d.ts +12 -1
- package/dist/application/development/fix-engine.d.ts.map +1 -1
- package/dist/application/development/fix-engine.js +56 -31
- package/dist/application/orchestration/engine.d.ts +30 -2
- package/dist/application/orchestration/engine.d.ts.map +1 -1
- package/dist/application/orchestration/engine.js +389 -190
- package/dist/application/orchestration/incremental-index.d.ts +24 -6
- package/dist/application/orchestration/incremental-index.d.ts.map +1 -1
- package/dist/application/orchestration/incremental-index.js +87 -31
- package/dist/cli/commands/analyze.d.ts.map +1 -1
- package/dist/cli/commands/analyze.js +7 -1
- package/dist/cli/commands/ask.d.ts.map +1 -1
- package/dist/cli/commands/ask.js +20 -9
- package/dist/cli/commands/askf.d.ts.map +1 -1
- package/dist/cli/commands/askf.js +54 -39
- package/dist/cli/commands/diff.d.ts +29 -0
- package/dist/cli/commands/diff.d.ts.map +1 -0
- package/dist/cli/commands/diff.js +151 -0
- package/dist/cli/commands/index.js +67 -67
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +74 -38
- package/dist/cli/commands/pentest.js +12 -7
- package/dist/cli/commands/restore.js +4 -4
- package/dist/cli/commands/scan.d.ts.map +1 -1
- package/dist/cli/commands/scan.js +4 -2
- package/dist/cli/index.d.ts +1 -4
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +31 -3
- package/dist/domain/entities/session.d.ts +45 -7
- package/dist/domain/entities/session.d.ts.map +1 -1
- package/dist/domain/entities/session.js +37 -5
- package/dist/domain/entities/snapshot.d.ts +10 -10
- package/dist/domain/entities/snapshot.d.ts.map +1 -1
- package/dist/domain/entities/snapshot.js +43 -60
- package/dist/domain/services/chunker.d.ts.map +1 -1
- package/dist/domain/services/chunker.js +2 -6
- package/dist/infrastructure/adapters/bridge.d.ts +15 -3
- package/dist/infrastructure/adapters/bridge.d.ts.map +1 -1
- package/dist/infrastructure/adapters/bridge.js +65 -7
- package/dist/infrastructure/adapters/external-providers.d.ts.map +1 -1
- package/dist/infrastructure/adapters/external-providers.js +47 -21
- package/dist/infrastructure/io/db-loader.d.ts.map +1 -1
- package/dist/infrastructure/io/db-loader.js +4 -2
- package/dist/infrastructure/io/file-watcher.d.ts.map +1 -1
- package/dist/infrastructure/io/file-watcher.js +4 -1
- package/dist/infrastructure/io/git-manager.js +3 -3
- package/dist/infrastructure/io/logger/index.d.ts +7 -2
- package/dist/infrastructure/io/logger/index.d.ts.map +1 -1
- package/dist/infrastructure/io/logger/index.js +24 -7
- package/dist/infrastructure/io/process-watcher.d.ts +5 -1
- package/dist/infrastructure/io/process-watcher.d.ts.map +1 -1
- package/dist/infrastructure/io/process-watcher.js +70 -6
- package/dist/infrastructure/io/web-scraper.d.ts +4 -1
- package/dist/infrastructure/io/web-scraper.d.ts.map +1 -1
- package/dist/infrastructure/io/web-scraper.js +28 -11
- package/dist/infrastructure/utils/error-parser.d.ts +6 -1
- package/dist/infrastructure/utils/error-parser.d.ts.map +1 -1
- package/dist/infrastructure/utils/error-parser.js +110 -56
- package/dist/infrastructure/utils/process-lock.d.ts +10 -0
- package/dist/infrastructure/utils/process-lock.d.ts.map +1 -1
- package/dist/infrastructure/utils/process-lock.js +46 -7
- package/package.json +3 -2
- package/prebuilds/darwin-arm64/uneven_core.node +0 -0
- package/prebuilds/darwin-x64/uneven_core.node +0 -0
- package/prebuilds/linux-arm64/uneven_core.node +0 -0
- package/prebuilds/linux-x64/uneven_core.node +0 -0
- package/prebuilds/win32-x64/uneven_core.node +0 -0
- package/scripts/postinstall.cjs +6 -6
- 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
|
-
> **
|
|
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
|
|
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
|
|
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
|
-
|
|
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;
|
|
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}/${
|
|
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
|
-
|
|
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
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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
|
|
384
|
-
//
|
|
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(
|
|
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;
|
|
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
|
-
|
|
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.
|
|
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
|
: '',
|