@nathapp/nax 0.62.0-canary.7 → 0.62.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/nax.js +384 -126
  2. package/package.json +1 -1
package/dist/nax.js CHANGED
@@ -4976,21 +4976,54 @@ Rules:`, STEP3_SHARED_RULES = `- **One test per AC**, named exactly "AC-N: <desc
4976
4976
  var init_acceptance_builder = () => {};
4977
4977
 
4978
4978
  // src/test-runners/conventions.ts
4979
- function suffixRegex(pattern) {
4980
- const lastStar = pattern.lastIndexOf("*");
4981
- if (lastStar === -1)
4979
+ function globToRegex(pattern) {
4980
+ if (pattern.length === 0)
4982
4981
  return null;
4983
- const suffix = pattern.slice(lastStar + 1);
4984
- if (suffix.length === 0)
4982
+ if (/^\*+\/?$/.test(pattern))
4985
4983
  return null;
4986
- const escaped = suffix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4987
- return new RegExp(`${escaped}$`);
4984
+ let regex = "";
4985
+ let i = 0;
4986
+ while (i < pattern.length) {
4987
+ const c = pattern[i];
4988
+ if (c === "*") {
4989
+ if (pattern[i + 1] === "*") {
4990
+ const beforeSlash = i > 0 && pattern[i - 1] === "/";
4991
+ const afterSlash = pattern[i + 2] === "/";
4992
+ if (beforeSlash && afterSlash) {
4993
+ regex = `${regex.slice(0, -1)}(?:.*\\/)?`;
4994
+ i += 3;
4995
+ } else if (afterSlash) {
4996
+ regex += "(?:.*\\/)?";
4997
+ i += 3;
4998
+ } else {
4999
+ regex += ".*";
5000
+ i += 2;
5001
+ }
5002
+ continue;
5003
+ }
5004
+ regex += "[^/]*";
5005
+ i++;
5006
+ continue;
5007
+ }
5008
+ if (c === "?") {
5009
+ regex += "[^/]";
5010
+ i++;
5011
+ continue;
5012
+ }
5013
+ if (".+^${}()|[]\\".includes(c)) {
5014
+ regex += `\\${c}`;
5015
+ } else {
5016
+ regex += c;
5017
+ }
5018
+ i++;
5019
+ }
5020
+ return new RegExp(`(?:^|/)${regex}$`);
4988
5021
  }
4989
5022
  function globsToTestRegex(patterns) {
4990
5023
  const regexes = [];
4991
5024
  const seen = new Set;
4992
5025
  for (const pattern of patterns) {
4993
- const re = suffixRegex(pattern);
5026
+ const re = globToRegex(pattern);
4994
5027
  if (re && !seen.has(re.source)) {
4995
5028
  regexes.push(re);
4996
5029
  seen.add(re.source);
@@ -5294,68 +5327,101 @@ var init_file_scan = __esm(() => {
5294
5327
  };
5295
5328
  });
5296
5329
 
5297
- // src/test-runners/detect/framework-configs.ts
5298
- function filterExcluded(patterns) {
5299
- return patterns.filter((p) => !EXCLUDE_DIRS.some((d) => p.includes(`/${d}/`) || p.startsWith(`${d}/`)));
5300
- }
5301
- async function parseVitestConfig(workdir) {
5302
- const candidates = ["vitest.config.ts", "vitest.config.mts", "vitest.config.js", "vitest.config.mjs"];
5303
- for (const name of candidates) {
5304
- const path = `${workdir}/${name}`;
5305
- const text = await _frameworkConfigDeps.readText(path);
5306
- if (!text)
5307
- continue;
5308
- const includeMatch = text.match(/include\s*:\s*\[([^\]]+)\]/s);
5309
- if (includeMatch) {
5310
- const patterns = extractStringLiterals(includeMatch[1]);
5311
- if (patterns.length > 0) {
5312
- return { type: "framework-config", path, patterns: filterExcluded(patterns) };
5313
- }
5330
+ // src/test-runners/detect/extglob.ts
5331
+ function hasCharRange(pattern) {
5332
+ return /\[[^\]]*-[^\]]*\]/.test(pattern);
5333
+ }
5334
+ function hasUnsupported(pattern) {
5335
+ return pattern.includes("!(") || hasCharRange(pattern);
5336
+ }
5337
+ function expandLeftmost(pattern) {
5338
+ let earliest = null;
5339
+ for (const spec of CONSTRUCTS) {
5340
+ const m = pattern.match(spec.re);
5341
+ if (m?.index !== undefined) {
5342
+ if (earliest === null || m.index < (earliest.match.index ?? Number.POSITIVE_INFINITY)) {
5343
+ earliest = { match: m, spec };
5344
+ }
5345
+ }
5346
+ }
5347
+ if (!earliest)
5348
+ return [pattern];
5349
+ const full = earliest.match[0];
5350
+ const body = earliest.match[1];
5351
+ const start = earliest.match.index;
5352
+ const before = pattern.slice(0, start);
5353
+ const after = pattern.slice(start + full.length);
5354
+ let alternatives = earliest.spec.charClass ? [...body] : body.split(earliest.spec.separator);
5355
+ if (earliest.spec.withEmpty)
5356
+ alternatives = ["", ...alternatives];
5357
+ return alternatives.map((alt) => `${before}${alt}${after}`);
5358
+ }
5359
+ function expandExtglob(pattern) {
5360
+ if (hasUnsupported(pattern))
5361
+ return [pattern];
5362
+ let variants = [pattern];
5363
+ for (let pass = 0;pass < MAX_PASSES; pass++) {
5364
+ const next = [];
5365
+ let changed = false;
5366
+ for (const v of variants) {
5367
+ const expanded = expandLeftmost(v);
5368
+ if (expanded.length > 1 || expanded[0] !== v)
5369
+ changed = true;
5370
+ next.push(...expanded);
5371
+ if (next.length > MAX_VARIANTS)
5372
+ break;
5314
5373
  }
5315
- return { type: "framework-config", path, patterns: [] };
5374
+ variants = [...new Set(next)];
5375
+ if (!changed)
5376
+ break;
5377
+ if (variants.length > MAX_VARIANTS)
5378
+ break;
5316
5379
  }
5317
- return null;
5380
+ return variants;
5318
5381
  }
5319
- async function parseJestConfig(workdir) {
5320
- const candidates = ["jest.config.ts", "jest.config.js", "jest.config.cjs", "jest.config.mjs", "jest.config.json"];
5321
- for (const name of candidates) {
5322
- const path = `${workdir}/${name}`;
5323
- const text = await _frameworkConfigDeps.readText(path);
5324
- if (!text)
5325
- continue;
5326
- const patterns = extractJestPatterns(text);
5327
- return { type: "framework-config", path, patterns: filterExcluded(patterns) };
5328
- }
5329
- const pkgPath = `${workdir}/package.json`;
5330
- const pkgText = await _frameworkConfigDeps.readText(pkgPath);
5331
- if (pkgText) {
5332
- try {
5333
- const pkg = JSON.parse(pkgText);
5334
- const jestConfig = pkg.jest;
5335
- if (jestConfig) {
5336
- const patterns = extractJestPatternsFromObject(jestConfig);
5337
- if (patterns.length > 0) {
5338
- return { type: "framework-config", path: `${pkgPath}#jest`, patterns: filterExcluded(patterns) };
5339
- }
5382
+ function expandExtglobAll(patterns) {
5383
+ const seen = new Set;
5384
+ const result = [];
5385
+ for (const p of patterns) {
5386
+ for (const expanded of expandExtglob(p)) {
5387
+ if (!seen.has(expanded)) {
5388
+ seen.add(expanded);
5389
+ result.push(expanded);
5340
5390
  }
5341
- } catch {}
5342
- }
5343
- return null;
5344
- }
5345
- function extractJestPatterns(text) {
5346
- const matchMatch = text.match(/testMatch\s*:\s*\[([^\]]+)\]/s);
5347
- if (matchMatch) {
5348
- const patterns = extractStringLiterals(matchMatch[1]);
5349
- if (patterns.length > 0)
5350
- return patterns;
5391
+ }
5351
5392
  }
5352
- return [];
5393
+ return result;
5353
5394
  }
5354
- function extractJestPatternsFromObject(config) {
5355
- const testMatch = config.testMatch;
5356
- if (Array.isArray(testMatch))
5357
- return testMatch.filter((p) => typeof p === "string");
5358
- return [];
5395
+ var MAX_VARIANTS = 64, MAX_PASSES = 10, CONSTRUCTS;
5396
+ var init_extglob = __esm(() => {
5397
+ CONSTRUCTS = [
5398
+ { re: /\{([^{}]+)\}/, withEmpty: false, charClass: false, separator: "," },
5399
+ { re: /\?\(([^()]*)\)/, withEmpty: true, charClass: false, separator: "|" },
5400
+ { re: /\*\(([^()]*)\)/, withEmpty: true, charClass: false, separator: "|" },
5401
+ { re: /\+\(([^()]*)\)/, withEmpty: false, charClass: false, separator: "|" },
5402
+ { re: /@\(([^()]*)\)/, withEmpty: false, charClass: false, separator: "|" },
5403
+ { re: /\[([^\]]+)\]/, withEmpty: false, charClass: true, separator: "" }
5404
+ ];
5405
+ });
5406
+
5407
+ // src/test-runners/detect/framework-configs-deps.ts
5408
+ var _frameworkConfigDeps;
5409
+ var init_framework_configs_deps = __esm(() => {
5410
+ _frameworkConfigDeps = {
5411
+ readText: async (path) => {
5412
+ const f = Bun.file(path);
5413
+ if (!await f.exists())
5414
+ return null;
5415
+ return f.text();
5416
+ },
5417
+ parseToml: (text) => Bun.TOML.parse(text),
5418
+ parseYaml: (text) => Bun.YAML.parse(text)
5419
+ };
5420
+ });
5421
+
5422
+ // src/test-runners/detect/framework-configs-python.ts
5423
+ function filterExcluded(patterns) {
5424
+ return patterns.filter((p) => !EXCLUDE_DIRS.some((d) => p.includes(`/${d}/`) || p.startsWith(`${d}/`)));
5359
5425
  }
5360
5426
  async function parsePyprojectToml(workdir) {
5361
5427
  const path = `${workdir}/pyproject.toml`;
@@ -5390,7 +5456,7 @@ async function parsePyprojectToml(workdir) {
5390
5456
  if (patterns.length === 0) {
5391
5457
  patterns.push("test_*.py", "*_test.py");
5392
5458
  }
5393
- return { type: "framework-config", path, patterns: filterExcluded(patterns) };
5459
+ return { type: "framework-config", framework: "pytest", path, patterns: filterExcluded(patterns) };
5394
5460
  } catch {
5395
5461
  return null;
5396
5462
  }
@@ -5421,10 +5487,94 @@ async function parsePytestIni(workdir) {
5421
5487
  }
5422
5488
  if (patterns.length === 0)
5423
5489
  patterns.push("test_*.py", "*_test.py");
5424
- return { type: "framework-config", path, patterns: filterExcluded(patterns) };
5490
+ return { type: "framework-config", framework: "pytest", path, patterns: filterExcluded(patterns) };
5425
5491
  }
5426
5492
  return null;
5427
5493
  }
5494
+ var EXCLUDE_DIRS;
5495
+ var init_framework_configs_python = __esm(() => {
5496
+ init_framework_configs_deps();
5497
+ EXCLUDE_DIRS = ["node_modules", "dist", "build", ".nax", "coverage", ".git"];
5498
+ });
5499
+
5500
+ // src/test-runners/detect/framework-configs.ts
5501
+ function filterExcluded2(patterns) {
5502
+ return patterns.filter((p) => !EXCLUDE_DIRS2.some((d) => p.includes(`/${d}/`) || p.startsWith(`${d}/`)));
5503
+ }
5504
+ function normalize(patterns) {
5505
+ return filterExcluded2(expandExtglobAll(patterns));
5506
+ }
5507
+ async function parseVitestConfig(workdir) {
5508
+ const candidates = ["vitest.config.ts", "vitest.config.mts", "vitest.config.js", "vitest.config.mjs"];
5509
+ for (const name of candidates) {
5510
+ const path = `${workdir}/${name}`;
5511
+ const text = await _frameworkConfigDeps.readText(path);
5512
+ if (!text)
5513
+ continue;
5514
+ const includeMatch = text.match(/include\s*:\s*\[([^\]]+)\]/s);
5515
+ if (includeMatch) {
5516
+ const patterns = extractStringLiterals(includeMatch[1]);
5517
+ if (patterns.length > 0) {
5518
+ return { type: "framework-config", framework: "vitest", path, patterns: normalize(patterns) };
5519
+ }
5520
+ }
5521
+ return { type: "framework-config", framework: "vitest", path, patterns: [] };
5522
+ }
5523
+ return null;
5524
+ }
5525
+ async function parseJestConfig(workdir) {
5526
+ const candidates = ["jest.config.ts", "jest.config.js", "jest.config.cjs", "jest.config.mjs", "jest.config.json"];
5527
+ for (const name of candidates) {
5528
+ const path = `${workdir}/${name}`;
5529
+ const text = await _frameworkConfigDeps.readText(path);
5530
+ if (!text)
5531
+ continue;
5532
+ if (name.endsWith(".json")) {
5533
+ try {
5534
+ const config = JSON.parse(text);
5535
+ const patterns2 = extractJestPatternsFromObject(config);
5536
+ return { type: "framework-config", framework: "jest", path, patterns: normalize(patterns2) };
5537
+ } catch {}
5538
+ }
5539
+ const patterns = extractJestPatterns(text);
5540
+ return { type: "framework-config", framework: "jest", path, patterns: normalize(patterns) };
5541
+ }
5542
+ const pkgPath = `${workdir}/package.json`;
5543
+ const pkgText = await _frameworkConfigDeps.readText(pkgPath);
5544
+ if (pkgText) {
5545
+ try {
5546
+ const pkg = JSON.parse(pkgText);
5547
+ const jestConfig = pkg.jest;
5548
+ if (jestConfig) {
5549
+ const patterns = extractJestPatternsFromObject(jestConfig);
5550
+ if (patterns.length > 0) {
5551
+ return {
5552
+ type: "framework-config",
5553
+ framework: "jest",
5554
+ path: `${pkgPath}#jest`,
5555
+ patterns: normalize(patterns)
5556
+ };
5557
+ }
5558
+ }
5559
+ } catch {}
5560
+ }
5561
+ return null;
5562
+ }
5563
+ function extractJestPatterns(text) {
5564
+ const matchMatch = text.match(/testMatch\s*:\s*\[([^\]]+)\]/s);
5565
+ if (matchMatch) {
5566
+ const patterns = extractStringLiterals(matchMatch[1]);
5567
+ if (patterns.length > 0)
5568
+ return patterns;
5569
+ }
5570
+ return [];
5571
+ }
5572
+ function extractJestPatternsFromObject(config) {
5573
+ const testMatch = config.testMatch;
5574
+ if (Array.isArray(testMatch))
5575
+ return testMatch.filter((p) => typeof p === "string");
5576
+ return [];
5577
+ }
5428
5578
  async function parseMochaConfig(workdir) {
5429
5579
  const candidates = [".mocharc.js", ".mocharc.cjs", ".mocharc.yaml", ".mocharc.yml", ".mocharc.json"];
5430
5580
  for (const name of candidates) {
@@ -5441,15 +5591,16 @@ async function parseMochaConfig(workdir) {
5441
5591
  } else {
5442
5592
  const specMatch = text.match(/spec\s*:\s*['"]([^'"]+)['"]/);
5443
5593
  if (specMatch) {
5444
- return { type: "framework-config", path, patterns: [specMatch[1]] };
5594
+ return { type: "framework-config", framework: "mocha", path, patterns: normalize([specMatch[1]]) };
5445
5595
  }
5446
- continue;
5596
+ return { type: "framework-config", framework: "mocha", path, patterns: [] };
5447
5597
  }
5448
5598
  const spec = config.spec;
5449
5599
  const patterns = Array.isArray(spec) ? spec.filter((p) => typeof p === "string") : typeof spec === "string" ? [spec] : [];
5450
5600
  if (patterns.length > 0) {
5451
- return { type: "framework-config", path, patterns: filterExcluded(patterns) };
5601
+ return { type: "framework-config", framework: "mocha", path, patterns: normalize(patterns) };
5452
5602
  }
5603
+ return { type: "framework-config", framework: "mocha", path, patterns: [] };
5453
5604
  } catch {}
5454
5605
  }
5455
5606
  return null;
@@ -5471,9 +5622,9 @@ async function parsePlaywrightConfig(workdir) {
5471
5622
  patterns.push(...extracted);
5472
5623
  }
5473
5624
  if (patterns.length > 0) {
5474
- return { type: "framework-config", path, patterns: filterExcluded(patterns) };
5625
+ return { type: "framework-config", framework: "playwright", path, patterns: normalize(patterns) };
5475
5626
  }
5476
- return { type: "framework-config", path, patterns: ["**/*.spec.ts", "**/*.spec.js"] };
5627
+ return { type: "framework-config", framework: "playwright", path, patterns: [] };
5477
5628
  }
5478
5629
  return null;
5479
5630
  }
@@ -5486,9 +5637,14 @@ async function parseCypressConfig(workdir) {
5486
5637
  continue;
5487
5638
  const specMatch = text.match(/specPattern\s*:\s*['"]([^'"]+)['"]/);
5488
5639
  if (specMatch) {
5489
- return { type: "framework-config", path, patterns: [specMatch[1]] };
5640
+ return { type: "framework-config", framework: "cypress", path, patterns: normalize([specMatch[1]]) };
5490
5641
  }
5491
- return { type: "framework-config", path, patterns: ["cypress/e2e/**/*.cy.{js,ts}"] };
5642
+ return {
5643
+ type: "framework-config",
5644
+ framework: "cypress",
5645
+ path,
5646
+ patterns: normalize(["cypress/e2e/**/*.cy.{js,ts}"])
5647
+ };
5492
5648
  }
5493
5649
  return null;
5494
5650
  }
@@ -5503,10 +5659,79 @@ function extractStringLiterals(body) {
5503
5659
  }
5504
5660
  return patterns;
5505
5661
  }
5662
+ async function parseViteConfig(workdir) {
5663
+ const candidates = ["vite.config.ts", "vite.config.mts", "vite.config.js", "vite.config.mjs"];
5664
+ for (const name of candidates) {
5665
+ const path = `${workdir}/${name}`;
5666
+ const text = await _frameworkConfigDeps.readText(path);
5667
+ if (!text)
5668
+ continue;
5669
+ if (!/\btest\s*:\s*\{/.test(text))
5670
+ continue;
5671
+ const testBlock = extractBalancedBlock(text, /\btest\s*:\s*\{/);
5672
+ if (testBlock) {
5673
+ const includeMatch = testBlock.match(/include\s*:\s*\[([^\]]+)\]/s);
5674
+ if (includeMatch) {
5675
+ const patterns = extractStringLiterals(includeMatch[1]);
5676
+ if (patterns.length > 0) {
5677
+ return { type: "framework-config", framework: "vitest", path, patterns: normalize(patterns) };
5678
+ }
5679
+ }
5680
+ }
5681
+ return { type: "framework-config", framework: "vitest", path, patterns: [] };
5682
+ }
5683
+ return null;
5684
+ }
5685
+ async function parseBunfig(workdir) {
5686
+ const path = `${workdir}/bunfig.toml`;
5687
+ const text = await _frameworkConfigDeps.readText(path);
5688
+ if (!text)
5689
+ return null;
5690
+ try {
5691
+ const parsed = _frameworkConfigDeps.parseToml(text);
5692
+ if (!parsed?.test || typeof parsed.test !== "object")
5693
+ return null;
5694
+ } catch {
5695
+ return null;
5696
+ }
5697
+ return {
5698
+ type: "framework-config",
5699
+ framework: "bun",
5700
+ path,
5701
+ patterns: normalize([
5702
+ "**/*.test.{ts,tsx,js,jsx,mjs,cjs}",
5703
+ "**/*_test.{ts,tsx,js,jsx,mjs,cjs}",
5704
+ "**/*.spec.{ts,tsx,js,jsx,mjs,cjs}",
5705
+ "**/*_spec.{ts,tsx,js,jsx,mjs,cjs}"
5706
+ ])
5707
+ };
5708
+ }
5709
+ function extractBalancedBlock(text, anchor) {
5710
+ const m = text.match(anchor);
5711
+ if (!m || m.index === undefined)
5712
+ return null;
5713
+ const openIdx = text.indexOf("{", m.index);
5714
+ if (openIdx === -1)
5715
+ return null;
5716
+ let depth = 0;
5717
+ for (let i = openIdx;i < text.length; i++) {
5718
+ const c = text[i];
5719
+ if (c === "{")
5720
+ depth++;
5721
+ else if (c === "}") {
5722
+ depth--;
5723
+ if (depth === 0)
5724
+ return text.slice(openIdx + 1, i);
5725
+ }
5726
+ }
5727
+ return null;
5728
+ }
5506
5729
  async function detectFromFrameworkConfigs(workdir) {
5507
5730
  const results = await Promise.all([
5508
5731
  parseVitestConfig(workdir),
5732
+ parseViteConfig(workdir),
5509
5733
  parseJestConfig(workdir),
5734
+ parseBunfig(workdir),
5510
5735
  parsePyprojectToml(workdir),
5511
5736
  parsePytestIni(workdir),
5512
5737
  parseMochaConfig(workdir),
@@ -5515,19 +5740,13 @@ async function detectFromFrameworkConfigs(workdir) {
5515
5740
  ]);
5516
5741
  return results.filter((r) => r !== null);
5517
5742
  }
5518
- var EXCLUDE_DIRS, _frameworkConfigDeps;
5743
+ var EXCLUDE_DIRS2;
5519
5744
  var init_framework_configs = __esm(() => {
5520
- EXCLUDE_DIRS = ["node_modules", "dist", "build", ".nax", "coverage", ".git"];
5521
- _frameworkConfigDeps = {
5522
- readText: async (path) => {
5523
- const f = Bun.file(path);
5524
- if (!await f.exists())
5525
- return null;
5526
- return f.text();
5527
- },
5528
- parseToml: (text) => Bun.TOML.parse(text),
5529
- parseYaml: (text) => Bun.YAML.parse(text)
5530
- };
5745
+ init_extglob();
5746
+ init_framework_configs_deps();
5747
+ init_framework_configs_python();
5748
+ init_framework_configs_deps();
5749
+ EXCLUDE_DIRS2 = ["node_modules", "dist", "build", ".nax", "coverage", ".git"];
5531
5750
  });
5532
5751
 
5533
5752
  // src/test-runners/detect/framework-defaults.ts
@@ -5535,61 +5754,75 @@ async function detectFromPackageJson(workdir) {
5535
5754
  const path = `${workdir}/package.json`;
5536
5755
  const text = await _frameworkDefaultsDeps.readText(path);
5537
5756
  if (!text)
5538
- return null;
5757
+ return [];
5539
5758
  let pkg;
5540
5759
  try {
5541
5760
  pkg = JSON.parse(text);
5542
5761
  } catch {
5543
- return null;
5762
+ return [];
5544
5763
  }
5545
5764
  const devDeps = pkg.devDependencies ?? {};
5546
5765
  const deps = pkg.dependencies ?? {};
5547
5766
  const allDeps = { ...deps, ...devDeps };
5548
- for (const [framework, patterns] of Object.entries(JS_FRAMEWORK_DEFAULTS)) {
5549
- if (framework in allDeps) {
5550
- return { type: "manifest", path, patterns };
5767
+ const results = [];
5768
+ for (const { depKey, framework, patterns } of JS_FRAMEWORK_DEFAULTS) {
5769
+ if (depKey in allDeps) {
5770
+ results.push({ type: "manifest", framework, path, patterns: expandExtglobAll(patterns) });
5551
5771
  }
5552
5772
  }
5773
+ if (results.length > 0)
5774
+ return results;
5553
5775
  const scripts = pkg.scripts;
5554
5776
  const testScript = typeof scripts?.test === "string" ? scripts.test : "";
5555
5777
  if (testScript.includes("bun test")) {
5556
- return { type: "manifest", path, patterns: BUN_TEST_DEFAULTS };
5778
+ return [{ type: "manifest", framework: "bun", path, patterns: expandExtglobAll(BUN_TEST_DEFAULTS) }];
5557
5779
  }
5558
- return null;
5780
+ return [];
5559
5781
  }
5560
5782
  async function detectFromGoMod(workdir) {
5561
5783
  const path = `${workdir}/go.mod`;
5562
5784
  if (!await _frameworkDefaultsDeps.fileExists(path))
5563
5785
  return null;
5564
- return { type: "manifest", path, patterns: ["**/*_test.go"] };
5786
+ return { type: "manifest", framework: "go", path, patterns: ["**/*_test.go"] };
5565
5787
  }
5566
5788
  async function detectFromCargoToml(workdir) {
5567
5789
  const path = `${workdir}/Cargo.toml`;
5568
5790
  if (!await _frameworkDefaultsDeps.fileExists(path))
5569
5791
  return null;
5570
- return { type: "manifest", path, patterns: ["tests/**/*.rs", "src/**/*.rs"] };
5792
+ return { type: "manifest", framework: "rust", path, patterns: ["tests/**/*.rs", "src/**/*.rs"] };
5571
5793
  }
5572
5794
  async function detectFromPyprojectDeps(workdir) {
5573
5795
  const path = `${workdir}/pyproject.toml`;
5574
5796
  const text = await _frameworkDefaultsDeps.readText(path);
5575
5797
  if (!text)
5576
5798
  return null;
5577
- if (text.includes("pytest")) {
5578
- return { type: "manifest", path, patterns: ["test_*.py", "*_test.py", "tests/**/*.py"] };
5579
- }
5580
- return null;
5799
+ const PYTEST_DEP_RE = /(?:^[ \t]*["']?pytest(?![-\w]))|(?:["']pytest(?![-\w])(?:[>=~!<][^"']*)?["'])/m;
5800
+ if (!PYTEST_DEP_RE.test(text))
5801
+ return null;
5802
+ return {
5803
+ type: "manifest",
5804
+ framework: "pytest",
5805
+ path,
5806
+ patterns: ["test_*.py", "*_test.py", "tests/**/*.py"]
5807
+ };
5581
5808
  }
5582
5809
  async function detectFromFrameworkDefaults(workdir) {
5583
- const results = await Promise.all([
5810
+ const [pkgJsonSources, goSource, cargoSource, pyprojectSource] = await Promise.all([
5584
5811
  detectFromPackageJson(workdir),
5585
5812
  detectFromGoMod(workdir),
5586
5813
  detectFromCargoToml(workdir),
5587
5814
  detectFromPyprojectDeps(workdir)
5588
5815
  ]);
5589
- return results.filter((r) => r !== null);
5816
+ return [
5817
+ ...pkgJsonSources,
5818
+ ...goSource ? [goSource] : [],
5819
+ ...cargoSource ? [cargoSource] : [],
5820
+ ...pyprojectSource ? [pyprojectSource] : []
5821
+ ];
5590
5822
  }
5591
5823
  var _frameworkDefaultsDeps, JS_FRAMEWORK_DEFAULTS, BUN_TEST_DEFAULTS;
5592
5824
  var init_framework_defaults = __esm(() => {
5825
+ init_extglob();
5593
5826
  _frameworkDefaultsDeps = {
5594
5827
  readText: async (path) => {
5595
5828
  const f = Bun.file(path);
@@ -5599,15 +5832,24 @@ var init_framework_defaults = __esm(() => {
5599
5832
  },
5600
5833
  fileExists: async (path) => Bun.file(path).exists()
5601
5834
  };
5602
- JS_FRAMEWORK_DEFAULTS = {
5603
- vitest: ["**/*.{test,spec}.?(c|m)[jt]s?(x)"],
5604
- jest: ["**/__tests__/**/*.[jt]s?(x)", "**/?(*.)+(spec|test).[jt]s?(x)"],
5605
- mocha: ["test/**/*.{js,mjs,cjs}", "**/*.spec.{js,ts}"],
5606
- jasmine: ["spec/**/*.js"],
5607
- "@playwright/test": ["**/*.spec.ts", "**/*.spec.js"],
5608
- cypress: ["cypress/e2e/**/*.cy.{js,jsx,ts,tsx}"]
5609
- };
5610
- BUN_TEST_DEFAULTS = ["**/*.test.{ts,tsx,js,jsx}"];
5835
+ JS_FRAMEWORK_DEFAULTS = [
5836
+ { depKey: "vitest", framework: "vitest", patterns: ["**/*.{test,spec}.?(c|m)[jt]s?(x)"] },
5837
+ {
5838
+ depKey: "jest",
5839
+ framework: "jest",
5840
+ patterns: ["**/__tests__/**/*.[jt]s?(x)", "**/?(*.)+(spec|test).[jt]s?(x)"]
5841
+ },
5842
+ { depKey: "mocha", framework: "mocha", patterns: ["test/**/*.{js,mjs,cjs}", "**/*.spec.{js,ts}"] },
5843
+ { depKey: "jasmine", framework: "jasmine", patterns: ["spec/**/*.js"] },
5844
+ { depKey: "@playwright/test", framework: "playwright", patterns: ["**/*.spec.ts", "**/*.spec.js"] },
5845
+ { depKey: "cypress", framework: "cypress", patterns: ["cypress/e2e/**/*.cy.{js,jsx,ts,tsx}"] }
5846
+ ];
5847
+ BUN_TEST_DEFAULTS = [
5848
+ "**/*.test.{ts,tsx,js,jsx,mjs,cjs}",
5849
+ "**/*_test.{ts,tsx,js,jsx,mjs,cjs}",
5850
+ "**/*.spec.{ts,tsx,js,jsx,mjs,cjs}",
5851
+ "**/*_spec.{ts,tsx,js,jsx,mjs,cjs}"
5852
+ ];
5611
5853
  });
5612
5854
 
5613
5855
  // src/test-runners/detect/index.ts
@@ -5653,7 +5895,9 @@ async function detectForDirectory(workdir) {
5653
5895
  const logger = getSafeLogger();
5654
5896
  const tier1Sources = await detectFromFrameworkConfigs(workdir);
5655
5897
  const tier1Patterns = tier1Sources.flatMap((s) => [...s.patterns]);
5656
- const tier2Sources = await detectFromFrameworkDefaults(workdir);
5898
+ const tier1FrameworksWithPatterns = new Set(tier1Sources.filter((s) => s.patterns.length > 0 && s.framework !== undefined).map((s) => s.framework));
5899
+ const tier2SourcesAll = await detectFromFrameworkDefaults(workdir);
5900
+ const tier2Sources = tier2SourcesAll.filter((s) => !s.framework || !tier1FrameworksWithPatterns.has(s.framework));
5657
5901
  const tier2Patterns = tier2Sources.flatMap((s) => [...s.patterns]);
5658
5902
  const tier3Source = await detectFromFileScan(workdir);
5659
5903
  const tier3Patterns = tier3Source ? [...tier3Source.patterns] : [];
@@ -7191,6 +7435,7 @@ function buildAuditContent(entry, epochMs) {
7191
7435
  const lines = [
7192
7436
  `Timestamp: ${ts}`,
7193
7437
  `Session: ${entry.sessionName}`,
7438
+ ...entry.recordId ? [`RecordId: ${entry.recordId}`] : [],
7194
7439
  ...entry.sessionId ? [`SessionId: ${entry.sessionId}`] : [],
7195
7440
  `Type: ${typeLabel}`,
7196
7441
  `StoryId: ${entry.storyId ?? "(none)"}`,
@@ -7445,6 +7690,7 @@ class SpawnAcpSession {
7445
7690
  pidRegistry;
7446
7691
  activeProc = null;
7447
7692
  id;
7693
+ recordId;
7448
7694
  constructor(opts) {
7449
7695
  this.agentName = opts.agentName;
7450
7696
  this.sessionName = opts.sessionName;
@@ -7455,6 +7701,7 @@ class SpawnAcpSession {
7455
7701
  this.env = opts.env;
7456
7702
  this.pidRegistry = opts.pidRegistry;
7457
7703
  this.id = opts.id;
7704
+ this.recordId = opts.recordId;
7458
7705
  }
7459
7706
  async prompt(text) {
7460
7707
  const cmd = [
@@ -7602,7 +7849,7 @@ class SpawnAcpSession {
7602
7849
  await this.trackedSpawn(cmd);
7603
7850
  }
7604
7851
  }
7605
- function parseSessionId(stdout) {
7852
+ function parseSessionIds(stdout) {
7606
7853
  for (const line of stdout.split(`
7607
7854
  `).reverse()) {
7608
7855
  const trimmed = line.trim();
@@ -7610,12 +7857,17 @@ function parseSessionId(stdout) {
7610
7857
  continue;
7611
7858
  try {
7612
7859
  const parsed = JSON.parse(trimmed);
7613
- const id = parsed.acpxSessionId;
7614
- if (typeof id === "string" && id.length > 0)
7615
- return id;
7860
+ const sessionId = parsed.acpxSessionId;
7861
+ const recordId = parsed.acpxRecordId;
7862
+ if (typeof sessionId === "string" && sessionId.length > 0) {
7863
+ return {
7864
+ sessionId,
7865
+ recordId: typeof recordId === "string" && recordId.length > 0 ? recordId : undefined
7866
+ };
7867
+ }
7616
7868
  } catch {}
7617
7869
  }
7618
- return;
7870
+ return { sessionId: undefined, recordId: undefined };
7619
7871
  }
7620
7872
 
7621
7873
  class SpawnAcpClient {
@@ -7672,6 +7924,7 @@ class SpawnAcpClient {
7672
7924
  if (exitCode !== 0) {
7673
7925
  throw new Error(`[acp-adapter] Failed to create session: ${stderr || `exit code ${exitCode}`}`);
7674
7926
  }
7927
+ const { sessionId, recordId } = parseSessionIds(stdout);
7675
7928
  return new SpawnAcpSession({
7676
7929
  agentName: opts.agentName,
7677
7930
  sessionName,
@@ -7681,7 +7934,8 @@ class SpawnAcpClient {
7681
7934
  permissionMode: opts.permissionMode,
7682
7935
  env: this.env,
7683
7936
  pidRegistry: this.pidRegistry,
7684
- id: parseSessionId(stdout)
7937
+ id: sessionId,
7938
+ recordId
7685
7939
  });
7686
7940
  }
7687
7941
  async loadSession(sessionName, agentName, permissionMode) {
@@ -7690,6 +7944,7 @@ class SpawnAcpClient {
7690
7944
  if (exitCode !== 0) {
7691
7945
  return null;
7692
7946
  }
7947
+ const { sessionId, recordId } = parseSessionIds(stdout);
7693
7948
  return new SpawnAcpSession({
7694
7949
  agentName,
7695
7950
  sessionName,
@@ -7699,7 +7954,8 @@ class SpawnAcpClient {
7699
7954
  permissionMode,
7700
7955
  env: this.env,
7701
7956
  pidRegistry: this.pidRegistry,
7702
- id: parseSessionId(stdout)
7957
+ id: sessionId,
7958
+ recordId
7703
7959
  });
7704
7960
  }
7705
7961
  async closeSession(sessionName, agentName) {
@@ -22981,6 +23237,7 @@ class AcpAgentAdapter {
22981
23237
  writePromptAudit({
22982
23238
  prompt: currentPrompt,
22983
23239
  sessionName,
23240
+ recordId: session.recordId,
22984
23241
  sessionId: session.id,
22985
23242
  workdir: options.workdir,
22986
23243
  projectDir: options.projectDir,
@@ -23135,6 +23392,7 @@ class AcpAgentAdapter {
23135
23392
  writePromptAudit({
23136
23393
  prompt,
23137
23394
  sessionName: completeSessionName,
23395
+ recordId: session.recordId,
23138
23396
  sessionId: session.id,
23139
23397
  workdir: workdir ?? process.cwd(),
23140
23398
  auditDir: _completeAuditConfig.agent.promptAudit.dir,
@@ -23711,7 +23969,7 @@ function migrateLegacyTestPattern(raw, logger) {
23711
23969
 
23712
23970
  // src/config/path-security.ts
23713
23971
  import { existsSync as existsSync3, lstatSync, realpathSync } from "fs";
23714
- import { basename, isAbsolute as isAbsolute3, normalize, resolve as resolve2 } from "path";
23972
+ import { basename, isAbsolute as isAbsolute3, normalize as normalize2, resolve as resolve2 } from "path";
23715
23973
  function validateDirectory(dirPath, baseDir) {
23716
23974
  const resolved = resolve2(dirPath);
23717
23975
  if (!existsSync3(resolved)) {
@@ -23741,8 +23999,8 @@ function validateDirectory(dirPath, baseDir) {
23741
23999
  return realPath;
23742
24000
  }
23743
24001
  function isWithinDirectory(targetPath, basePath) {
23744
- const normalizedTarget = normalize(targetPath);
23745
- const normalizedBase = normalize(basePath);
24002
+ const normalizedTarget = normalize2(targetPath);
24003
+ const normalizedBase = normalize2(basePath);
23746
24004
  if (!isAbsolute3(normalizedTarget) || !isAbsolute3(normalizedBase)) {
23747
24005
  return false;
23748
24006
  }
@@ -38362,14 +38620,14 @@ var init_init_context = __esm(() => {
38362
38620
 
38363
38621
  // src/utils/path-security.ts
38364
38622
  import { realpathSync as realpathSync3 } from "fs";
38365
- import { dirname as dirname4, isAbsolute as isAbsolute5, join as join28, normalize as normalize2, resolve as resolve7 } from "path";
38623
+ import { dirname as dirname4, isAbsolute as isAbsolute5, join as join28, normalize as normalize3, resolve as resolve7 } from "path";
38366
38624
  function safeRealpathForComparison(p) {
38367
38625
  try {
38368
38626
  return realpathSync3(p);
38369
38627
  } catch {
38370
38628
  const parent = dirname4(p);
38371
38629
  if (parent === p)
38372
- return normalize2(p);
38630
+ return normalize3(p);
38373
38631
  const resolvedParent = safeRealpathForComparison(parent);
38374
38632
  return join28(resolvedParent, p.split("/").pop() ?? "");
38375
38633
  }
@@ -38380,7 +38638,7 @@ function validateModulePath(modulePath, allowedRoots) {
38380
38638
  }
38381
38639
  const resolvedRoots = allowedRoots.map((r) => safeRealpathForComparison(resolve7(r)));
38382
38640
  if (isAbsolute5(modulePath)) {
38383
- const normalized = normalize2(modulePath);
38641
+ const normalized = normalize3(modulePath);
38384
38642
  const resolved = safeRealpathForComparison(normalized);
38385
38643
  const isWithin = resolvedRoots.some((root) => resolved.startsWith(`${root}/`) || resolved === root);
38386
38644
  if (isWithin) {
@@ -39142,7 +39400,7 @@ var package_default;
39142
39400
  var init_package = __esm(() => {
39143
39401
  package_default = {
39144
39402
  name: "@nathapp/nax",
39145
- version: "0.62.0-canary.7",
39403
+ version: "0.62.0",
39146
39404
  description: "AI Coding Agent Orchestrator \u2014 loops until done",
39147
39405
  type: "module",
39148
39406
  bin: {
@@ -39222,8 +39480,8 @@ var init_version = __esm(() => {
39222
39480
  NAX_VERSION = package_default.version;
39223
39481
  NAX_COMMIT = (() => {
39224
39482
  try {
39225
- if (/^[0-9a-f]{6,10}$/.test("bea17955"))
39226
- return "bea17955";
39483
+ if (/^[0-9a-f]{6,10}$/.test("2aa5324d"))
39484
+ return "2aa5324d";
39227
39485
  } catch {}
39228
39486
  try {
39229
39487
  const result = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"], {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nathapp/nax",
3
- "version": "0.62.0-canary.7",
3
+ "version": "0.62.0",
4
4
  "description": "AI Coding Agent Orchestrator — loops until done",
5
5
  "type": "module",
6
6
  "bin": {