@ruso-0/nreki 10.18.0 → 10.18.1
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 +247 -174
- package/package.json +1 -1
- package/src/compressor-foveal.ts +5 -7
- package/src/database.ts +52 -9
- package/src/engine.ts +41 -37
- package/src/middleware/circuit-breaker.ts +36 -1
- package/src/parser.ts +28 -6
- package/src/semantic-edit.ts +21 -8
- package/src/utils/to-posix.ts +13 -13
package/CHANGELOG.md
CHANGED
|
@@ -1,176 +1,249 @@
|
|
|
1
|
-
# Changelog
|
|
2
|
-
|
|
3
|
-
All notable changes to NREKI will be documented in this file.
|
|
4
|
-
|
|
5
|
-
## [10.18.
|
|
6
|
-
|
|
7
|
-
# v10.18.
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
###
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to NREKI will be documented in this file.
|
|
4
|
+
|
|
5
|
+
## [10.18.1] - 2026-05-04
|
|
6
|
+
|
|
7
|
+
# v10.18.1 — Cache schema versioning + web-symbol normalization + sprint hygiene
|
|
8
|
+
|
|
9
|
+
Sprint focus: bug fixes, cache invalidation correctness, test isolation, and
|
|
10
|
+
documentation precision. Closes the migration TODO from v10.18.0
|
|
11
|
+
("schema-versioned cache invalidation shipping in v10.18.1") and the web-symbol
|
|
12
|
+
asymmetry between parser storage and edit lookup.
|
|
13
|
+
|
|
14
|
+
## Fixed
|
|
15
|
+
|
|
16
|
+
### `batch_edit` / `edit` symbol normalization on web files (CSS / HTML / JSON)
|
|
17
|
+
Asymmetric normalization: the parser stripped prefixes at chunk storage time
|
|
18
|
+
(`.` for CSS classes, `#` for IDs, quotes for JSON keys), but `semanticEdit`
|
|
19
|
+
and `batchSemanticEdit` matched against the raw caller-supplied symbol. LLM
|
|
20
|
+
callers sending `.className` or `"key"` now correctly match parser-stored
|
|
21
|
+
chunks. Also closes a key-leak in blast-radius detection where `oldRawCodes` /
|
|
22
|
+
`newRawCodes` were keyed off the normalized symbol while downstream lookups
|
|
23
|
+
used the raw input. (`59c3934`)
|
|
24
|
+
|
|
25
|
+
### Test isolation leak in router and backward-compat tests
|
|
26
|
+
Tests no longer pollute global state; previously-flaky tests now pass
|
|
27
|
+
deterministically. (`77ae56c`)
|
|
28
|
+
|
|
29
|
+
## Added
|
|
30
|
+
|
|
31
|
+
### Parser schema version gate for cache invalidation (`69e5acd`, `bb10bb4`)
|
|
32
|
+
Bumped `PARSER_SCHEMA_VERSION` to 2. Existing `.nreki.db` caches with v1 schema
|
|
33
|
+
are auto-wiped on next run, eliminating the manual `rm -rf .nreki .nreki.db`
|
|
34
|
+
step that v10.18.0 documented as a workaround. Includes characterization tests
|
|
35
|
+
covering version-gate transitions.
|
|
36
|
+
|
|
37
|
+
### `READ_ONLY_ACTIONS` whitelist in circuit breaker (`9717ce7`)
|
|
38
|
+
20 read-only actions skip the `containsError()` heuristic to prevent
|
|
39
|
+
false-positive doom-loop trips during normal navigation. `filter_output` is
|
|
40
|
+
deliberately excluded — it remains the primary gateway for detecting
|
|
41
|
+
test/build error loops. `response.isError === true` continues to trip the
|
|
42
|
+
breaker even on read-only actions (hardware signal vs heuristic).
|
|
43
|
+
|
|
44
|
+
### NREKI artifact exclusion from `DEFAULT_IGNORE` (`a50df2d`)
|
|
45
|
+
`**/.nreki/**` and `**/.nreki.db*` patterns now ignored by default. Critical
|
|
46
|
+
for upcoming corpus work — without this, NREKI re-indexed its own artifacts on
|
|
47
|
+
every repo it scanned.
|
|
48
|
+
|
|
49
|
+
## Changed
|
|
50
|
+
|
|
51
|
+
### Removed `nreki-enforcer` hook integration (`fe2c0d0`)
|
|
52
|
+
Cleanup of unused Claude hook. No functional impact on package consumers.
|
|
53
|
+
|
|
54
|
+
## Documentation
|
|
55
|
+
|
|
56
|
+
### Clarified v7.x spectral gap language in CHANGELOG (`2efa26a`)
|
|
57
|
+
"for predictive analysis" → "for spectral gap analysis" in the v7.x λ₃ entry.
|
|
58
|
+
AHI is a deterministic descriptive composite (percentile bucketing P20/P50/P80
|
|
59
|
+
over peer-group distribution), not a supervised classifier — phrasing aligned
|
|
60
|
+
with that design.
|
|
61
|
+
|
|
62
|
+
## Tests
|
|
63
|
+
|
|
64
|
+
- 909 passed | 1 skipped (893 base + 16 new from
|
|
65
|
+
`tests/web-symbol-normalization.test.ts`).
|
|
66
|
+
- `tsc --noEmit` clean.
|
|
67
|
+
|
|
68
|
+
## Compatibility
|
|
69
|
+
|
|
70
|
+
- **Recommended runtime: Claude Sonnet 4.6+** for 1M context window support.
|
|
71
|
+
The 1M context beta header was retired for Sonnet 4.5/4 on 2026-04-30;
|
|
72
|
+
NREKI's compression remains effective on 200k contexts but benefits more
|
|
73
|
+
from 1M.
|
|
74
|
+
- `@modelcontextprotocol/sdk` remains pinned at `1.23.1` (1.24.0+ injects
|
|
75
|
+
`execution: { taskSupport: 'forbidden' }` which breaks NREKI's tool
|
|
76
|
+
registration).
|
|
77
|
+
|
|
78
|
+
## [10.18.0] - 2026-05-02
|
|
79
|
+
|
|
80
|
+
# v10.18.0 — Schema compatibility + CSS parser + UX deadlock fix
|
|
81
|
+
|
|
82
|
+
## Fixed
|
|
83
|
+
|
|
84
|
+
### Critical: Schema break in strict MCP clients (Bloque A)
|
|
85
|
+
SDK MCP `@modelcontextprotocol/sdk` was silently resolving from `^1.12.1`
|
|
86
|
+
to `1.27.1` via `npx -y` distribution. Versions 1.24.0+ inject an
|
|
87
|
+
`execution.taskSupport: 'forbidden'` field in tool registrations that
|
|
88
|
+
strict MCP clients (Gemini CLI, Codex CLI) reject as non-spec, causing
|
|
89
|
+
NREKI tools to be silently dropped. Now pinned to `1.23.1` (last clean
|
|
90
|
+
version, verified empirically).
|
|
91
|
+
|
|
92
|
+
All other runtime dependencies pinned to current lockfile resolutions to
|
|
93
|
+
prevent recurrence:
|
|
94
|
+
- `@typescript-eslint/parser`, `chokidar`, `eslint`, `eslint-plugin-jsx-a11y`,
|
|
95
|
+
`eslint-plugin-react`, `eslint-plugin-react-hooks`, `picomatch`,
|
|
96
|
+
`web-tree-sitter`, `zod`, `@xenova/transformers`
|
|
97
|
+
- TypeScript peer dependency capped at `>=5.5.0 <6.0.0`
|
|
98
|
+
|
|
99
|
+
### Critical: P0 deadlock in Chronos + Cognitive Enforcer (Bug D reclassified)
|
|
100
|
+
Files marked HIGH friction by Chronos triggered an infinite loop:
|
|
101
|
+
1. Layer 2 (edit gate) blocks edit and instructs LLM to `read compress:false`
|
|
102
|
+
2. LLM obeys
|
|
103
|
+
3. Layer 1 (Cognitive Enforcer) blocks the raw read on >100 line files
|
|
104
|
+
4. Layer 2 keeps demanding raw read, Layer 1 keeps rejecting
|
|
105
|
+
5. Context budget exhausted
|
|
106
|
+
|
|
107
|
+
Fix: Layer 2 now emits the JSON-RPC tool call including
|
|
108
|
+
`_nreki_bypass:"chronos_recovery"` token that Layer 1 honors. Both
|
|
109
|
+
single-edit and batch-edit gates updated.
|
|
110
|
+
|
|
111
|
+
### CSS parser captures at-rules (Bloque C2)
|
|
112
|
+
Tree-sitter-css query previously only captured `rule_set` selectors,
|
|
113
|
+
making `@keyframes`, `@media`, `@import`, and `@font-face` blocks
|
|
114
|
+
invisible to the chunks index. `fast_grep` for at-rule strings returned
|
|
115
|
+
"No matches" even when present in source files.
|
|
116
|
+
|
|
117
|
+
Now captures 5 node types with `@symbol_name` binding:
|
|
118
|
+
- `rule_set` (regular CSS rules)
|
|
119
|
+
- `keyframes_statement` (`@keyframes`)
|
|
120
|
+
- `media_statement` (`@media`)
|
|
121
|
+
- `import_statement` (`@import`)
|
|
122
|
+
- `at_rule` (catch-all for `@font-face`, `@supports`, etc.)
|
|
123
|
+
|
|
124
|
+
### CSS focus normalization (Hallazgo Colateral)
|
|
125
|
+
Parser strips punctuation from selectors at extraction (`.foo` → `foo`),
|
|
126
|
+
but `compressor-foveal.ts` kept the user's focus raw. TFC-Pro silently
|
|
127
|
+
failed to find `.home-sidebar` against the indexed `home-sidebar`. Now
|
|
128
|
+
applies the same normalization to focus when the file is CSS or HTML.
|
|
129
|
+
|
|
130
|
+
### TFC-Pro returns discriminated union (Bloque C1)
|
|
131
|
+
Previously returned `TfcResult | null` and emitted misleading
|
|
132
|
+
"NOT FOUND or TOO LARGE (Density Shield)" which conflated two distinct
|
|
133
|
+
cases. New `TfcResultPayload` type:
|
|
134
|
+
- `{ kind: "success", data: TfcResult }`
|
|
135
|
+
- `{ kind: "not_found" }` → "symbol X not found, run outline first"
|
|
136
|
+
- `{ kind: "shield_tripped", ratio, ... }` → "focus spans >85% of file,
|
|
137
|
+
use compress:false or omit focus"
|
|
138
|
+
|
|
139
|
+
### Multi-focus validation with domain shield (Bloque C3.A)
|
|
140
|
+
Agents passing `focus="func1 func2 func3"` (space-separated) previously
|
|
141
|
+
became a single token that never matched, returning generic "not_found".
|
|
142
|
+
Now emits an educational error in non-web file types. CSS/HTML preserve
|
|
143
|
+
multi-word selectors (e.g., `body .foo`).
|
|
144
|
+
|
|
145
|
+
## Migration notes
|
|
146
|
+
|
|
147
|
+
If upgrading from earlier versions and you observe that:
|
|
148
|
+
- `fast_grep` returns no matches for CSS at-rules in projects you
|
|
149
|
+
previously indexed
|
|
150
|
+
- Indexed paths still point to old project locations (e.g., after
|
|
151
|
+
moving the project to a different directory)
|
|
152
|
+
|
|
153
|
+
**Workaround:** delete the `.nreki/` directory and `.nreki.db` file from
|
|
154
|
+
your project root to force reindex with the new parser.
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
# Linux/Mac
|
|
158
|
+
rm -rf .nreki/ .nreki.db
|
|
159
|
+
|
|
160
|
+
# Windows PowerShell
|
|
161
|
+
Remove-Item -Recurse -Force .nreki, .nreki.db
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
A permanent migration mechanism (paths relative to projectRoot,
|
|
165
|
+
schema-versioned cache invalidation) is shipping in v10.18.1.
|
|
166
|
+
|
|
167
|
+
## Known issues for v10.18.1
|
|
168
|
+
|
|
169
|
+
- Path stale: paths in `.nreki.db` and `.nreki/backups/` are stored
|
|
170
|
+
absolute, breaking when projects are moved between directories.
|
|
171
|
+
- 8 npm audit vulnerabilities in transitive dependencies (review case
|
|
172
|
+
by case in v10.18.1).
|
|
173
|
+
|
|
174
|
+
## Dependencies
|
|
175
|
+
|
|
176
|
+
- `@modelcontextprotocol/sdk`: `^1.12.1` → `1.23.1` (pinned, downgrade)
|
|
177
|
+
- All other runtime deps pinned to current lockfile resolutions
|
|
178
|
+
- 9 commits in this release including 3 internal smoke tests as
|
|
179
|
+
permanent regression tooling
|
|
180
|
+
|
|
181
|
+
## [10.17.0] - 2026-04-25
|
|
182
|
+
|
|
183
|
+
### Reliability pass: 6 critical bugs fixed
|
|
184
|
+
|
|
185
|
+
Pre-publish audit revealed 6 bugs in v10.16.x tags (never published to
|
|
186
|
+
npm). All fixed in this release.
|
|
187
|
+
|
|
188
|
+
- **fix(parser)**: `.tsx` files now use dedicated tree-sitter-tsx
|
|
189
|
+
grammar. Previously JSX caused 0 chunks, making React/Next/Remix
|
|
190
|
+
components invisible to NREKI. `da8275e`
|
|
191
|
+
- **fix(engine)**: File watcher auto-starts on boot. `fast_grep`
|
|
192
|
+
bootstraps index on first call. chokidar config uses
|
|
193
|
+
`ignoreInitial: true` and root-directory watching (required by
|
|
194
|
+
chokidar@4, which dropped glob support) to prevent race with
|
|
195
|
+
handler-side indexing. `77afbed`
|
|
196
|
+
- **fix(parser)**: Top-level `const`/`let` declarations now indexed.
|
|
197
|
+
Previously only arrow-function/function-expression constants
|
|
198
|
+
captured. Config objects, lookup tables, hardcoded values now
|
|
199
|
+
visible to fast_grep. `25287b2`
|
|
200
|
+
- **fix(semantic-edit)**: `batch_edit` path keys normalized via
|
|
201
|
+
`safePath()` for both stores and lookups, including hologram branch.
|
|
202
|
+
Relative paths no longer silently skip blast radius detection.
|
|
203
|
+
`907b7d2` + follow-up commit.
|
|
204
|
+
- **fix(path-jail)**: Windows drive letter comparison case-insensitive.
|
|
205
|
+
Lowercase `d:/` no longer blocked as traversal. Affects VSCode URIs,
|
|
206
|
+
WSL symlinks, shell copy-paste. `9c346d9`
|
|
207
|
+
- **fix(signature)**: `extractSignature` strips body from one-liner
|
|
208
|
+
functions, cutting at `=>` or `{` at zero paren/angle depth. No more
|
|
209
|
+
false positive blast radius on body-only edits. `76d778e`
|
|
210
|
+
|
|
211
|
+
### Performance improvements
|
|
212
|
+
|
|
213
|
+
Zero-allocation rewrite + FFI diet from abandoned v10.16.0 branch now
|
|
214
|
+
published in this release:
|
|
215
|
+
|
|
216
|
+
- **perf(navigate)**: fast_grep O(L) pointer walk via `indexOf`
|
|
217
|
+
(V8 memchr SIMD internally). Replaces per-hit allocations.
|
|
218
|
+
- **perf(fast_grep)**: Lean SELECT (4 cols) reduces WASM-to-JS FFI
|
|
219
|
+
decoding overhead. Legacy 10-col method removed (breaking for
|
|
220
|
+
any hypothetical external consumer).
|
|
221
|
+
|
|
222
|
+
Combined benchmark delta on NREKI src/ (10 iter, median):
|
|
223
|
+
- export (156 hits): 11.24ms -> 5.22ms (-54%)
|
|
224
|
+
- findDefinition: 2.98ms -> 2.26ms (-24%)
|
|
225
|
+
- chronosMemory: 2.16ms -> 1.73ms (-20%)
|
|
226
|
+
- [OK]: 1.81ms -> 1.52ms (-16%)
|
|
227
|
+
|
|
228
|
+
### Multi-agent manifest
|
|
229
|
+
|
|
230
|
+
`nreki init` now generates CLAUDE.md + AGENTS.md + SKILL.md. All three
|
|
231
|
+
share the IMMUNE SYSTEM operational rules: 80L Guillotine, Multi-patching
|
|
232
|
+
ACID, Anti-Sweep Shield, TTRD Shield with CFI, Engrams with ASSERT prefix.
|
|
233
|
+
Compatible with Claude Code, Codex, Gemini, and any MCP-capable agent.
|
|
234
|
+
|
|
235
|
+
### Tests
|
|
236
|
+
|
|
237
|
+
877 passing, 1 skipped (Linux-specific case-sensitivity test skipped on
|
|
238
|
+
win32). +26 tests added across 6 bug fixes.
|
|
239
|
+
|
|
240
|
+
### Note on unreleased versions
|
|
241
|
+
|
|
242
|
+
v10.16.0 (`c08e61a`) and v10.16.1 (`ae9a526`) were git-tagged but never
|
|
243
|
+
reached npm registry due to the 6 bugs above discovered in pre-publish
|
|
244
|
+
audit. v10.17.0 is the next version reaching npm after v10.15.1.
|
|
245
|
+
|
|
246
|
+
## [10.16.1] - 2026-04-24
|
|
174
247
|
|
|
175
248
|
### Multi-agent manifest completion
|
|
176
249
|
|
|
@@ -1036,7 +1109,7 @@ Three localized security/correctness fixes shipped as separate bisectable commit
|
|
|
1036
1109
|
|
|
1037
1110
|
### Added
|
|
1038
1111
|
- **Fiedler Vector extraction**: `analyzeTopology` now returns the full eigenvector `v2` (bridge fragility map)
|
|
1039
|
-
- **Third eigenvalue (λ₃)**: Enables spectral gap computation ∇(λ₃ - λ₂) for
|
|
1112
|
+
- **Third eigenvalue (λ₃)**: Enables spectral gap computation ∇(λ₃ - λ₂) for spectral gap analysis
|
|
1040
1113
|
- **Third eigenvector (v3)**: Topological stress coordinates per node
|
|
1041
1114
|
- **Gauge Fixing**: Deterministic phase canonicalization prevents sign ambiguity across commits (critical for ML pipelines)
|
|
1042
1115
|
- **Gram-Schmidt deflation**: Reusable `powerIteration()` function extracts arbitrary eigenvectors
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ruso-0/nreki",
|
|
3
|
-
"version": "10.18.
|
|
3
|
+
"version": "10.18.1",
|
|
4
4
|
"description": "MCP plugin that validates AI agent edits in RAM before they touch disk. Spectral clustering, architecture diffs, bridge detection, dead code oracle, and cross-file semantic checks for TypeScript, Go (gopls), and Python (pyright). Zero cloud dependencies.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|
package/src/compressor-foveal.ts
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
|
|
23
23
|
import path from "path";
|
|
24
24
|
import crypto from "crypto";
|
|
25
|
-
import { type ParsedChunk, type ParseResult } from "./parser.js";
|
|
25
|
+
import { type ParsedChunk, type ParseResult, normalizeWebSymbol } from "./parser.js";
|
|
26
26
|
import { extractDependencies, cleanSignature } from "./utils/imports.js";
|
|
27
27
|
import { Embedder } from "./embedder.js";
|
|
28
28
|
import type { NrekiEngine } from "./engine.js";
|
|
@@ -109,15 +109,13 @@ export async function tfcCompress(
|
|
|
109
109
|
if (parseResult.chunks.length === 0) return { kind: "not_found" };
|
|
110
110
|
|
|
111
111
|
// 1. MULTI-FOCAL TARGETS + OVERLOAD FIX
|
|
112
|
+
// v10.18.1: shared normalizeWebSymbol covers CSS+HTML+JSON uniformly.
|
|
113
|
+
// Previous inline regex covered only CSS (despite isCss naming HTML too,
|
|
114
|
+
// it ran identical CSS rules). JSON foci were never normalized.
|
|
112
115
|
const ext = path.extname(filePath).toLowerCase();
|
|
113
|
-
const isCss = ext === ".css" || ext === ".html";
|
|
114
116
|
|
|
115
117
|
const foci = focusInput.split(",").map(s => {
|
|
116
|
-
|
|
117
|
-
if (isCss) {
|
|
118
|
-
clean = clean.replace(/[.#,]/g, " ").replace(/\s+/g, " ").trim();
|
|
119
|
-
}
|
|
120
|
-
return clean;
|
|
118
|
+
return normalizeWebSymbol(s.trim(), ext).toLowerCase();
|
|
121
119
|
}).filter(Boolean);
|
|
122
120
|
const foveas = new Set<ParsedChunk>();
|
|
123
121
|
|
package/src/database.ts
CHANGED
|
@@ -126,6 +126,23 @@ export class NrekiDB {
|
|
|
126
126
|
// Setup schema first (creates metadata table needed for dimension lookup)
|
|
127
127
|
this.setupSchema();
|
|
128
128
|
|
|
129
|
+
// ─── Schema Version Gate ──────────────────────────────────────
|
|
130
|
+
// Force full reindex when parser/index format changes between
|
|
131
|
+
// versions. Users on older schema get a clean slate without
|
|
132
|
+
// manual .nreki.db deletion. Bumped in v10.18.1.
|
|
133
|
+
const PARSER_SCHEMA_VERSION = 2;
|
|
134
|
+
const storedSchema = parseInt(this.getMetadata("parser_schema_version") ?? "0", 10);
|
|
135
|
+
if (storedSchema < PARSER_SCHEMA_VERSION) {
|
|
136
|
+
if (storedSchema > 0) {
|
|
137
|
+
logger.warn(
|
|
138
|
+
`[NREKI] Parser schema upgrade (v${storedSchema} -> v${PARSER_SCHEMA_VERSION}). ` +
|
|
139
|
+
`Forcing full AST reindex.`
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
this.wipeAllIndexedData();
|
|
143
|
+
this.setMetadata("parser_schema_version", String(PARSER_SCHEMA_VERSION));
|
|
144
|
+
}
|
|
145
|
+
|
|
129
146
|
// Load vector index using stored dimension (default 512)
|
|
130
147
|
const storedDim = parseInt(this.getMetadata("embedding_dim") ?? "512", 10);
|
|
131
148
|
if (fs.existsSync(this.vecPath)) {
|
|
@@ -277,9 +294,42 @@ export class NrekiDB {
|
|
|
277
294
|
);
|
|
278
295
|
}
|
|
279
296
|
|
|
297
|
+
/**
|
|
298
|
+
* Wipes all indexed data from RAM and disk.
|
|
299
|
+
* Prevents ghost vectors and ID drift by cleaning up sequence counters
|
|
300
|
+
* and orphaned .vec files. Used during cache invalidation.
|
|
301
|
+
*/
|
|
302
|
+
private wipeAllIndexedData(): void {
|
|
303
|
+
// 1. Purge disk artifacts to prevent ghost data on next boot
|
|
304
|
+
if (fs.existsSync(this.vecPath)) {
|
|
305
|
+
try {
|
|
306
|
+
fs.unlinkSync(this.vecPath);
|
|
307
|
+
} catch (err) {
|
|
308
|
+
logger.warn(`[NREKI] Could not delete stale vector index ${this.vecPath}: ${err}`);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// 2. Wipe SQL tables
|
|
313
|
+
this.db.run("DELETE FROM chunks");
|
|
314
|
+
this.db.run("DELETE FROM files");
|
|
315
|
+
|
|
316
|
+
// 3. Reset AUTOINCREMENT sequences (ONLY for wiped tables)
|
|
317
|
+
try {
|
|
318
|
+
this.db.run("DELETE FROM sqlite_sequence WHERE name = 'chunks'");
|
|
319
|
+
} catch {
|
|
320
|
+
// Ignored: sqlite_sequence is created automatically by SQLite on first INSERT
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// 4. Reset RAM state
|
|
324
|
+
this.vecIndex = new VectorIndex();
|
|
325
|
+
this.kwIndex = new KeywordIndex();
|
|
326
|
+
this.rawIdentsByFile.clear();
|
|
327
|
+
this.rawIdentsLoaded = false;
|
|
328
|
+
}
|
|
329
|
+
|
|
280
330
|
/**
|
|
281
331
|
* Check if the active embedding dimension matches what was stored.
|
|
282
|
-
* If they differ,
|
|
332
|
+
* If they differ, wipe all indexed data and update the stored dimension.
|
|
283
333
|
* Returns true if a re-index is needed.
|
|
284
334
|
*/
|
|
285
335
|
checkEmbeddingDimension(activeDim: number): boolean {
|
|
@@ -287,14 +337,7 @@ export class NrekiDB {
|
|
|
287
337
|
|
|
288
338
|
if (storedDim && parseInt(storedDim, 10) !== activeDim) {
|
|
289
339
|
logger.warn(`Embedding dimension changed (${storedDim} -> ${activeDim}). Clearing index.`);
|
|
290
|
-
|
|
291
|
-
this.vecIndex = new VectorIndex();
|
|
292
|
-
// Clear all chunks and files so they get re-indexed
|
|
293
|
-
this.db.run("DELETE FROM chunks");
|
|
294
|
-
this.db.run("DELETE FROM files");
|
|
295
|
-
this.kwIndex = new KeywordIndex();
|
|
296
|
-
this.rawIdentsByFile.clear();
|
|
297
|
-
this.rawIdentsLoaded = false;
|
|
340
|
+
this.wipeAllIndexedData();
|
|
298
341
|
this.setMetadata("embedding_dim", String(activeDim));
|
|
299
342
|
return true;
|
|
300
343
|
}
|
package/src/engine.ts
CHANGED
|
@@ -49,6 +49,10 @@ const DEFAULT_IGNORE = [
|
|
|
49
49
|
"**/__pycache__/**",
|
|
50
50
|
"**/*.min.js",
|
|
51
51
|
"**/*.d.ts",
|
|
52
|
+
// NREKI's own artifacts (prevents auto-indexing of self in dogfooding
|
|
53
|
+
// and contamination of corpus benchmarks against external repos)
|
|
54
|
+
"**/.nreki/**",
|
|
55
|
+
"**/.nreki.db*",
|
|
52
56
|
];
|
|
53
57
|
|
|
54
58
|
// ─── Session Tracker ─────────────────────────────────────────────────
|
|
@@ -112,15 +116,15 @@ export class NrekiEngine {
|
|
|
112
116
|
private embedder: Embedder;
|
|
113
117
|
private parser: ASTParser;
|
|
114
118
|
private compressor: Compressor;
|
|
115
|
-
private advancedCompressor: AdvancedCompressor;
|
|
119
|
+
private advancedCompressor: AdvancedCompressor;
|
|
116
120
|
|
|
117
121
|
// Sub-pipelines (injected after initialize)
|
|
118
122
|
private indexer!: IndexPipeline;
|
|
119
123
|
private searcher!: SearchEngine;
|
|
120
124
|
|
|
121
|
-
private watcher: FSWatcher | null = null;
|
|
122
|
-
private watcherReady: Promise<void> | null = null;
|
|
123
|
-
private config: Required<EngineConfig>;
|
|
125
|
+
private watcher: FSWatcher | null = null;
|
|
126
|
+
private watcherReady: Promise<void> | null = null;
|
|
127
|
+
private config: Required<EngineConfig>;
|
|
124
128
|
private indexingQueue = new Set<string>();
|
|
125
129
|
private isIndexing = false;
|
|
126
130
|
private initialized = false;
|
|
@@ -454,33 +458,33 @@ export class NrekiEngine {
|
|
|
454
458
|
|
|
455
459
|
// ─── File Watching ────────────────────────────────────────────
|
|
456
460
|
|
|
457
|
-
startWatcher(): Promise<void> {
|
|
458
|
-
if (this.watcher) return this.watcherReady ?? Promise.resolve();
|
|
459
|
-
|
|
460
|
-
const shouldWatchFile = (fp: string) => this.config.extensions.includes(path.extname(fp).toLowerCase());
|
|
461
|
-
|
|
462
|
-
this.watcher = chokidar.watch(this.config.watchPaths, {
|
|
463
|
-
ignored: this.config.ignorePaths,
|
|
464
|
-
persistent: true,
|
|
465
|
-
ignoreInitial: true, // prevent race with handler bootstrap
|
|
466
|
-
});
|
|
467
|
-
|
|
468
|
-
this.watcherReady = new Promise((resolve, reject) => {
|
|
469
|
-
this.watcher!.once("ready", resolve);
|
|
470
|
-
this.watcher!.once("error", reject);
|
|
471
|
-
});
|
|
472
|
-
|
|
473
|
-
this.watcher
|
|
474
|
-
.on("add", (fp: string) => { if (shouldWatchFile(fp)) this.queueIndexing(fp); })
|
|
475
|
-
.on("change", (fp: string) => { if (shouldWatchFile(fp)) this.queueIndexing(fp); })
|
|
476
|
-
.on("unlink", (fp: string) => {
|
|
477
|
-
if (!shouldWatchFile(fp)) return;
|
|
478
|
-
this.db.clearChunks(fp);
|
|
479
|
-
this.scheduleSave();
|
|
480
|
-
});
|
|
481
|
-
|
|
482
|
-
return this.watcherReady;
|
|
483
|
-
}
|
|
461
|
+
startWatcher(): Promise<void> {
|
|
462
|
+
if (this.watcher) return this.watcherReady ?? Promise.resolve();
|
|
463
|
+
|
|
464
|
+
const shouldWatchFile = (fp: string) => this.config.extensions.includes(path.extname(fp).toLowerCase());
|
|
465
|
+
|
|
466
|
+
this.watcher = chokidar.watch(this.config.watchPaths, {
|
|
467
|
+
ignored: this.config.ignorePaths,
|
|
468
|
+
persistent: true,
|
|
469
|
+
ignoreInitial: true, // prevent race with handler bootstrap
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
this.watcherReady = new Promise((resolve, reject) => {
|
|
473
|
+
this.watcher!.once("ready", resolve);
|
|
474
|
+
this.watcher!.once("error", reject);
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
this.watcher
|
|
478
|
+
.on("add", (fp: string) => { if (shouldWatchFile(fp)) this.queueIndexing(fp); })
|
|
479
|
+
.on("change", (fp: string) => { if (shouldWatchFile(fp)) this.queueIndexing(fp); })
|
|
480
|
+
.on("unlink", (fp: string) => {
|
|
481
|
+
if (!shouldWatchFile(fp)) return;
|
|
482
|
+
this.db.clearChunks(fp);
|
|
483
|
+
this.scheduleSave();
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
return this.watcherReady;
|
|
487
|
+
}
|
|
484
488
|
|
|
485
489
|
/** Queue a file for indexing (debounced). */
|
|
486
490
|
private queueIndexing(filePath: string): void {
|
|
@@ -544,12 +548,12 @@ export class NrekiEngine {
|
|
|
544
548
|
|
|
545
549
|
/** Stop the file watcher. */
|
|
546
550
|
stopWatcher(): void {
|
|
547
|
-
if (this.watcher) {
|
|
548
|
-
this.watcher.close();
|
|
549
|
-
this.watcher = null;
|
|
550
|
-
this.watcherReady = null;
|
|
551
|
-
}
|
|
552
|
-
}
|
|
551
|
+
if (this.watcher) {
|
|
552
|
+
this.watcher.close();
|
|
553
|
+
this.watcher = null;
|
|
554
|
+
this.watcherReady = null;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
553
557
|
|
|
554
558
|
// ─── DB Delegation Wrappers (Stats & Tracking) ─────────────────
|
|
555
559
|
|
|
@@ -17,6 +17,33 @@ import type { McpToolResponse } from "../router.js";
|
|
|
17
17
|
/** Inactivity timeout for auto-reset (60 seconds). */
|
|
18
18
|
const INACTIVITY_TIMEOUT_MS = 60_000;
|
|
19
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Read-only actions exempt from heuristic text-matching of error patterns
|
|
22
|
+
* via containsError(). These actions can legitimately return responses
|
|
23
|
+
* containing words like "error", "failed", "exception" (search hits on
|
|
24
|
+
* try/catch blocks, fast_grep over real source code, audit reports with
|
|
25
|
+
* "Type errors" frequency, agent's own memorize/engram annotations).
|
|
26
|
+
*
|
|
27
|
+
* Important: this skip applies ONLY to containsError() text heuristic.
|
|
28
|
+
* Explicit response.isError === true (hardware signal: path jail
|
|
29
|
+
* violation, security error, etc.) STILL trips the breaker for these
|
|
30
|
+
* actions, because that is a contract violation, not a false-positive.
|
|
31
|
+
*
|
|
32
|
+
* Excluded from this set: filter_output (it is the gateway for the
|
|
33
|
+
* agent to read Bash/test/build output - text matching of "FAIL"/"error"
|
|
34
|
+
* there is exactly the doom-loop signal the breaker must catch).
|
|
35
|
+
*/
|
|
36
|
+
const READ_ONLY_ACTIONS = new Set<string>([
|
|
37
|
+
// nreki_navigate
|
|
38
|
+
"search", "definition", "references", "outline", "map",
|
|
39
|
+
"fast_grep", "prepare_refactor", "orphan_oracle", "type_shape",
|
|
40
|
+
// nreki_code (read-only sub-actions; filter_output deliberately excluded)
|
|
41
|
+
"read", "compress",
|
|
42
|
+
// nreki_guard (state inspection + agent's own memory annotations)
|
|
43
|
+
"status", "report", "audit",
|
|
44
|
+
"pin", "unpin", "reset", "set_plan", "memorize", "engram",
|
|
45
|
+
]);
|
|
46
|
+
|
|
20
47
|
|
|
21
48
|
// ─── Level-Specific Payloads ─────────────────────────────────────────
|
|
22
49
|
|
|
@@ -182,7 +209,15 @@ export function wrapWithCircuitBreaker(
|
|
|
182
209
|
// Record the result for pattern detection
|
|
183
210
|
// Truncate to 2KB before hashing — terminal output can be megabytes
|
|
184
211
|
const responseText = response.content.map(c => c.text ?? "").filter(Boolean).join("\n").slice(0, 2048);
|
|
185
|
-
|
|
212
|
+
// Read-only actions skip the containsError() text heuristic to avoid
|
|
213
|
+
// false-positive trips on legitimate error mentions in source code,
|
|
214
|
+
// search results, audit reports, or agent's own memory annotations.
|
|
215
|
+
// Explicit response.isError === true (hardware signal) still trips
|
|
216
|
+
// the breaker for these actions: it is a contract violation, not a
|
|
217
|
+
// heuristic match.
|
|
218
|
+
const isReadOnly = READ_ONLY_ACTIONS.has(action);
|
|
219
|
+
const hasError = response.isError === true ||
|
|
220
|
+
(!isReadOnly && containsError(responseText));
|
|
186
221
|
|
|
187
222
|
if (hasError) {
|
|
188
223
|
const loopCheck = cb.recordToolCall(
|
package/src/parser.ts
CHANGED
|
@@ -398,13 +398,9 @@ export class ASTParser {
|
|
|
398
398
|
}
|
|
399
399
|
}
|
|
400
400
|
|
|
401
|
-
// Web symbol name normalization (D1)
|
|
401
|
+
// Web symbol name normalization (D1) — extracted to shared utility (v10.18.1)
|
|
402
402
|
if (symbolName) {
|
|
403
|
-
|
|
404
|
-
symbolName = symbolName.replace(/[.#,]/g, " ").replace(/\s+/g, " ").trim();
|
|
405
|
-
} else if (ext === ".json" || ext === ".html") {
|
|
406
|
-
symbolName = symbolName.replace(/^["']|["']$/g, "").trim();
|
|
407
|
-
}
|
|
403
|
+
symbolName = normalizeWebSymbol(symbolName, ext);
|
|
408
404
|
}
|
|
409
405
|
|
|
410
406
|
const node = mainCapture.node;
|
|
@@ -574,3 +570,29 @@ export class ASTParser {
|
|
|
574
570
|
return typeMap[captureName] ?? captureName;
|
|
575
571
|
}
|
|
576
572
|
}
|
|
573
|
+
|
|
574
|
+
/**
|
|
575
|
+
* Normalize a web symbol (CSS selector, HTML attribute value, JSON key)
|
|
576
|
+
* to match the canonical form stored in ParsedChunk.symbolName.
|
|
577
|
+
*
|
|
578
|
+
* CSS: strips ".", "#", "," prefixes; collapses whitespace.
|
|
579
|
+
* ".foo, #bar" → "foo bar"
|
|
580
|
+
* HTML/JSON: strips surrounding quotes.
|
|
581
|
+
* '"key"' → "key"
|
|
582
|
+
* Other extensions: no-op (returns input unchanged).
|
|
583
|
+
*
|
|
584
|
+
* Used at the API boundary of semantic-edit + compressor-foveal to
|
|
585
|
+
* accept LLM-supplied symbols with web prefixes that the parser stripped
|
|
586
|
+
* during chunk creation. Without this normalization, ".foo" sent by an
|
|
587
|
+
* LLM caller fails to match a parser-stored chunk named "foo".
|
|
588
|
+
*/
|
|
589
|
+
export function normalizeWebSymbol(symbolName: string, ext: string): string {
|
|
590
|
+
if (!symbolName) return symbolName;
|
|
591
|
+
if (ext === ".css") {
|
|
592
|
+
return symbolName.replace(/[.#,]/g, " ").replace(/\s+/g, " ").trim();
|
|
593
|
+
}
|
|
594
|
+
if (ext === ".json" || ext === ".html") {
|
|
595
|
+
return symbolName.replace(/^["']|["']$/g, "").trim();
|
|
596
|
+
}
|
|
597
|
+
return symbolName;
|
|
598
|
+
}
|
package/src/semantic-edit.ts
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
import fs from "fs";
|
|
16
16
|
import path from "path";
|
|
17
|
-
import { ASTParser, type ParsedChunk } from "./parser.js";
|
|
17
|
+
import { ASTParser, type ParsedChunk, normalizeWebSymbol } from "./parser.js";
|
|
18
18
|
import { AstSandbox } from "./ast-sandbox.js";
|
|
19
19
|
import { Embedder } from "./embedder.js";
|
|
20
20
|
import { readSource } from "./utils/read-source.js";
|
|
@@ -458,11 +458,20 @@ export async function batchSemanticEdit(
|
|
|
458
458
|
for (const [filePath, fileEdits] of editsByFile.entries()) {
|
|
459
459
|
let virtualCode = vfs.get(filePath)!;
|
|
460
460
|
const parseResult = await parser.parse(filePath, virtualCode);
|
|
461
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
461
462
|
|
|
462
463
|
// ─── PHASE A: Cluster edits by AST node identity (startIndex) ───
|
|
463
464
|
const chunkGroups = new Map<number, { chunk: ParsedChunk; edits: BatchEditOp[] }>();
|
|
464
465
|
|
|
465
466
|
for (const edit of fileEdits) {
|
|
467
|
+
// v10.18.1: normalize web symbols IN-PLACE so downstream
|
|
468
|
+
// oldRawCodes/newRawCodes keys (built from chunk.symbolName)
|
|
469
|
+
// align with handleBatchEdit's blast-radius lookup
|
|
470
|
+
// (built from edit.symbol). Without in-place mutation,
|
|
471
|
+
// signature changes on .css/.json/.html files would evade
|
|
472
|
+
// detection silently.
|
|
473
|
+
edit.symbol = normalizeWebSymbol(edit.symbol, ext);
|
|
474
|
+
|
|
466
475
|
// AMBIGUITY CHECK: Reject if multiple symbols share the same name.
|
|
467
476
|
const allMatches = parseResult.chunks.filter(c => {
|
|
468
477
|
const name = c.symbolName || extractName(c);
|
|
@@ -781,12 +790,12 @@ export async function batchSemanticEdit(
|
|
|
781
790
|
}
|
|
782
791
|
}
|
|
783
792
|
if (!topologyChanged) {
|
|
784
|
-
for (const edit of edits) {
|
|
785
|
-
if (edit.mode && edit.mode !== "replace" && edit.mode !== "patch") continue;
|
|
786
|
-
const key = `${safePath(projectRoot, edit.path)}::${edit.symbol}`;
|
|
787
|
-
const oldRaw = oldRawCodes.get(key);
|
|
788
|
-
const newRaw = newRawCodes.get(key);
|
|
789
|
-
if (oldRaw && newRaw && detectSignatureChange(oldRaw, newRaw)) {
|
|
793
|
+
for (const edit of edits) {
|
|
794
|
+
if (edit.mode && edit.mode !== "replace" && edit.mode !== "patch") continue;
|
|
795
|
+
const key = `${safePath(projectRoot, edit.path)}::${edit.symbol}`;
|
|
796
|
+
const oldRaw = oldRawCodes.get(key);
|
|
797
|
+
const newRaw = newRawCodes.get(key);
|
|
798
|
+
if (oldRaw && newRaw && detectSignatureChange(oldRaw, newRaw)) {
|
|
790
799
|
topologyChanged = true;
|
|
791
800
|
break;
|
|
792
801
|
}
|
|
@@ -862,6 +871,11 @@ export async function semanticEdit(
|
|
|
862
871
|
};
|
|
863
872
|
}
|
|
864
873
|
|
|
874
|
+
// v10.18.1: normalize web symbols at API boundary so LLM-supplied
|
|
875
|
+
// ".foo" / '"key"' match parser-normalized chunk.symbolName.
|
|
876
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
877
|
+
symbolName = normalizeWebSymbol(symbolName, ext);
|
|
878
|
+
|
|
865
879
|
// Find matching chunks by name (prefer AST symbolName, fallback to regex)
|
|
866
880
|
const matches: Array<{ chunk: ParsedChunk; name: string }> = [];
|
|
867
881
|
const allNames: string[] = [];
|
|
@@ -1123,7 +1137,6 @@ export async function semanticEdit(
|
|
|
1123
1137
|
const newCodeTokens = Embedder.estimateTokens(newCode ?? spliceRes.newRawCode);
|
|
1124
1138
|
const tokensAvoided = Math.max(0, fullFileTokens + symbolTokens - newCodeTokens);
|
|
1125
1139
|
|
|
1126
|
-
const ext = path.extname(filePath).toLowerCase();
|
|
1127
1140
|
let topologyChanged = false;
|
|
1128
1141
|
if (rawCode && spliceRes.newRawCode && detectSignatureChange(rawCode, spliceRes.newRawCode)) {
|
|
1129
1142
|
topologyChanged = true;
|
package/src/utils/to-posix.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* to-posix.ts — Cross-platform path normalization
|
|
3
|
-
*
|
|
4
|
-
* Converts Windows backslash paths to forward-slash POSIX format.
|
|
5
|
-
* Single source of truth — eliminates 4 duplicated copies across
|
|
6
|
-
* the kernel, backends, and hologram modules.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import * as path from "path";
|
|
10
|
-
|
|
11
|
-
export function toPosix(p: string): string {
|
|
12
|
-
return path.normalize(p).replace(/\\/g, "/");
|
|
13
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* to-posix.ts — Cross-platform path normalization
|
|
3
|
+
*
|
|
4
|
+
* Converts Windows backslash paths to forward-slash POSIX format.
|
|
5
|
+
* Single source of truth — eliminates 4 duplicated copies across
|
|
6
|
+
* the kernel, backends, and hologram modules.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import * as path from "path";
|
|
10
|
+
|
|
11
|
+
export function toPosix(p: string): string {
|
|
12
|
+
return path.normalize(p).replace(/\\/g, "/");
|
|
13
|
+
}
|