llmnav 0.9.1 → 0.9.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.
package/CHANGELOG.md CHANGED
@@ -6,6 +6,13 @@ The npm package follows Semantic Versioning. The `llmnav/N` source protocol is v
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.9.2] — 2026-09-09
10
+
11
+ ### Fixed
12
+
13
+ * Recognize JavaScript/TypeScript regular-expression literals while masking source strings, so quotes and comment-like text inside regexes do not hide later cards or create false cards. Preserve division expressions and ordinary JSX closing tags.
14
+ * Invalidate older parsed-file caches so unchanged consumer files receive the corrected parsing behavior.
15
+
9
16
  ## [0.9.1] — 2026-09-07
10
17
 
11
18
  ### Fixed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "llmnav",
3
- "version": "0.9.1",
3
+ "version": "0.9.2",
4
4
  "description": "A deterministic semantic navigation layer for LLM coding agents.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -27,7 +27,7 @@ import {
27
27
  } from "./util.js";
28
28
 
29
29
  export const FILE_STATE_SCHEMA_VERSION = 1;
30
- export const SOURCE_INDEXER_VERSION = 6;
30
+ export const SOURCE_INDEXER_VERSION = 7;
31
31
  const STAT_HINTS_SCHEMA_VERSION = 1;
32
32
 
33
33
  export async function scanProjectIncremental(root, options = {}) {
package/src/parser.js CHANGED
@@ -39,6 +39,11 @@ const BLOCK_PATTERNS = [
39
39
  ];
40
40
  const MAX_SOURCE_BYTES = 16 * 1024 * 1024;
41
41
  const MAX_BLOCKS_PER_FILE = 10_000;
42
+ const JAVASCRIPT_EXTENSIONS = new Set([".js", ".mjs", ".cjs", ".jsx", ".ts", ".tsx"]);
43
+ const REGEX_PREFIX_KEYWORDS = new Set([
44
+ "await", "case", "delete", "do", "else", "in", "instanceof", "new", "return", "throw", "typeof", "void", "yield",
45
+ ]);
46
+ const CONTROL_PAREN_KEYWORDS = new Set(["if", "while", "for", "with", "switch", "catch"]);
42
47
 
43
48
  export function parseLlmnavBlocks(source, filePath = "<memory>") {
44
49
  if (Buffer.byteLength(source) > MAX_SOURCE_BYTES) {
@@ -217,6 +222,12 @@ function buildLiteralMask(source, filePath) {
217
222
  const hashComments = [".py", ".rb", ".sh", ".bash", ".zsh"].includes(extension);
218
223
  const dashComments = extension === ".sql";
219
224
  const tripleQuotes = extension === ".py";
225
+ const javascript = JAVASCRIPT_EXTENSIONS.has(extension);
226
+ let regexAllowed = true;
227
+ let regexCharacterClass = false;
228
+ let previousToken = "";
229
+ const controlParentheses = [];
230
+ const expressionBraces = [];
220
231
  let state = "normal";
221
232
  let escaped = false;
222
233
  const mask = new Uint8Array(source.length);
@@ -260,6 +271,21 @@ function buildLiteralMask(source, filePath) {
260
271
  }
261
272
  continue;
262
273
  }
274
+ if (state === "regex") {
275
+ if (character === "\n" || character === "\r") {
276
+ state = "normal";
277
+ escaped = false;
278
+ } else if (escaped) escaped = false;
279
+ else if (character === "\\") escaped = true;
280
+ else if (character === "[") regexCharacterClass = true;
281
+ else if (character === "]") regexCharacterClass = false;
282
+ else if (character === "/" && !regexCharacterClass) {
283
+ state = "normal";
284
+ regexAllowed = false;
285
+ previousToken = "value";
286
+ }
287
+ continue;
288
+ }
263
289
  if (state === "single" || state === "double" || state === "backtick") {
264
290
  if (escaped) {
265
291
  escaped = false;
@@ -270,7 +296,11 @@ function buildLiteralMask(source, filePath) {
270
296
  continue;
271
297
  }
272
298
  const terminator = state === "single" ? "'" : state === "double" ? '"' : "`";
273
- if (character === terminator) state = "normal";
299
+ if (character === terminator) {
300
+ state = "normal";
301
+ regexAllowed = false;
302
+ previousToken = "value";
303
+ }
274
304
  continue;
275
305
  }
276
306
 
@@ -283,6 +313,10 @@ function buildLiteralMask(source, filePath) {
283
313
  } else if (character === "/" && next === "/") {
284
314
  state = "line-comment";
285
315
  index += 1;
316
+ } else if (javascript && character === "/" && regexAllowed) {
317
+ state = "regex";
318
+ regexCharacterClass = false;
319
+ escaped = false;
286
320
  } else if (hashComments && character === "#") {
287
321
  state = "line-comment";
288
322
  } else if (dashComments && character === "-" && next === "-") {
@@ -303,6 +337,40 @@ function buildLiteralMask(source, filePath) {
303
337
  } else if (character === "`") {
304
338
  state = "backtick";
305
339
  escaped = false;
340
+ } else if (javascript && !/\s/u.test(character)) {
341
+ // Track lexical expression position without rescanning prefixes. Comments do
342
+ // not change it; a control-condition ')' permits a following regex statement.
343
+ if (/[A-Za-z_$]/u.test(character)) {
344
+ const start = index;
345
+ const memberName = previousToken === ".";
346
+ while (index + 1 < source.length && /[\w$]/u.test(source[index + 1])) index += 1;
347
+ previousToken = source.slice(start, index + 1);
348
+ regexAllowed = !memberName && REGEX_PREFIX_KEYWORDS.has(previousToken);
349
+ } else if (character === "(") {
350
+ controlParentheses.push(CONTROL_PAREN_KEYWORDS.has(previousToken));
351
+ regexAllowed = true;
352
+ previousToken = character;
353
+ } else if (character === ")") {
354
+ regexAllowed = controlParentheses.pop() === true;
355
+ previousToken = character;
356
+ } else if (character === "{") {
357
+ expressionBraces.push(regexAllowed && !["", ";", "else", "do", "try", "finally", ")"].includes(previousToken));
358
+ regexAllowed = true;
359
+ previousToken = character;
360
+ } else if (character === "}") {
361
+ regexAllowed = expressionBraces.pop() !== true;
362
+ previousToken = character;
363
+ } else if ((character === "+" || character === "-") && next === character) {
364
+ previousToken = character + next;
365
+ index += 1;
366
+ } else if (character === "=" && next === ">") {
367
+ regexAllowed = true;
368
+ previousToken = "=>";
369
+ index += 1;
370
+ } else {
371
+ regexAllowed = "=(:,!&|?;[+-*%~^>/".includes(character);
372
+ previousToken = character;
373
+ }
306
374
  }
307
375
  }
308
376
 
package/src/spec.js CHANGED
@@ -10,7 +10,7 @@ rel=workflow>llmnav.rules.validate
10
10
  stability=contract
11
11
  */
12
12
 
13
- export const PACKAGE_VERSION = "0.9.1";
13
+ export const PACKAGE_VERSION = "0.9.2";
14
14
  export const SPEC_VERSION = "1";
15
15
 
16
16
  export const SCOPES = Object.freeze(["file", "module", "symbol"]);