@wrongstack/techstack 0.292.1 → 0.295.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 (70) hide show
  1. package/dist/adapters/cpp.d.ts +1 -1
  2. package/dist/adapters/cpp.d.ts.map +1 -1
  3. package/dist/adapters/dart.d.ts +1 -2
  4. package/dist/adapters/dart.d.ts.map +1 -1
  5. package/dist/adapters/dotnet.d.ts +1 -1
  6. package/dist/adapters/dotnet.d.ts.map +1 -1
  7. package/dist/adapters/elixir.d.ts +1 -1
  8. package/dist/adapters/elixir.d.ts.map +1 -1
  9. package/dist/adapters/go.d.ts +1 -2
  10. package/dist/adapters/go.d.ts.map +1 -1
  11. package/dist/adapters/gradle.d.ts +9 -0
  12. package/dist/adapters/gradle.d.ts.map +1 -0
  13. package/dist/adapters/interface.d.ts +1 -0
  14. package/dist/adapters/interface.d.ts.map +1 -1
  15. package/dist/adapters/maven.d.ts +1 -1
  16. package/dist/adapters/maven.d.ts.map +1 -1
  17. package/dist/adapters/npm.d.ts +1 -1
  18. package/dist/adapters/npm.d.ts.map +1 -1
  19. package/dist/adapters/parse-utils.d.ts +15 -0
  20. package/dist/adapters/parse-utils.d.ts.map +1 -0
  21. package/dist/adapters/paths.d.ts +8 -1
  22. package/dist/adapters/paths.d.ts.map +1 -1
  23. package/dist/adapters/php.d.ts +1 -2
  24. package/dist/adapters/php.d.ts.map +1 -1
  25. package/dist/adapters/python.d.ts +1 -2
  26. package/dist/adapters/python.d.ts.map +1 -1
  27. package/dist/adapters/ruby.d.ts +1 -1
  28. package/dist/adapters/ruby.d.ts.map +1 -1
  29. package/dist/adapters/rust.d.ts +1 -2
  30. package/dist/adapters/rust.d.ts.map +1 -1
  31. package/dist/adapters/swift.d.ts +9 -0
  32. package/dist/adapters/swift.d.ts.map +1 -0
  33. package/dist/advisory/native-audit.d.ts +20 -8
  34. package/dist/advisory/native-audit.d.ts.map +1 -1
  35. package/dist/advisory/osv.d.ts.map +1 -1
  36. package/dist/discovery/index.d.ts +3 -13
  37. package/dist/discovery/index.d.ts.map +1 -1
  38. package/dist/index.d.ts +10 -6
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +1396 -929
  41. package/dist/index.js.map +4 -4
  42. package/dist/registry/client.d.ts +23 -0
  43. package/dist/registry/client.d.ts.map +1 -1
  44. package/dist/registry/http-fetch.d.ts +22 -0
  45. package/dist/registry/http-fetch.d.ts.map +1 -0
  46. package/dist/remediation.d.ts +35 -0
  47. package/dist/remediation.d.ts.map +1 -1
  48. package/dist/research/llm.d.ts +1 -1
  49. package/dist/research/llm.d.ts.map +1 -1
  50. package/dist/research/triage.d.ts +2 -15
  51. package/dist/research/triage.d.ts.map +1 -1
  52. package/dist/service/enrich-phase.d.ts +8 -0
  53. package/dist/service/enrich-phase.d.ts.map +1 -0
  54. package/dist/service/finding-factory.d.ts +4 -0
  55. package/dist/service/finding-factory.d.ts.map +1 -0
  56. package/dist/service/inventory-phase.d.ts +9 -0
  57. package/dist/service/inventory-phase.d.ts.map +1 -0
  58. package/dist/service/report-generator.d.ts +3 -0
  59. package/dist/service/report-generator.d.ts.map +1 -0
  60. package/dist/service/research-phase.d.ts +10 -0
  61. package/dist/service/research-phase.d.ts.map +1 -0
  62. package/dist/service/techstack-engine.d.ts +33 -0
  63. package/dist/service/techstack-engine.d.ts.map +1 -0
  64. package/dist/service.d.ts +5 -106
  65. package/dist/service.d.ts.map +1 -1
  66. package/dist/store/sqlite.d.ts +4 -0
  67. package/dist/store/sqlite.d.ts.map +1 -1
  68. package/dist/trend.d.ts +36 -0
  69. package/dist/trend.d.ts.map +1 -0
  70. package/package.json +3 -3
package/dist/index.js CHANGED
@@ -265,10 +265,12 @@ function coverageForEcosystem(ecosystem) {
265
265
  }
266
266
 
267
267
  // src/adapters/npm.ts
268
- import { existsSync, readFileSync } from "node:fs";
268
+ import { access as access2, readFile } from "node:fs/promises";
269
269
  import { dirname, join, relative, resolve as resolve2 } from "node:path";
270
270
 
271
271
  // src/adapters/paths.ts
272
+ import { existsSync } from "node:fs";
273
+ import { access } from "node:fs/promises";
272
274
  import { resolve } from "node:path";
273
275
  function workspaceRoot(workspace, options) {
274
276
  const relative2 = workspace.relativeRoot || ".";
@@ -277,9 +279,26 @@ function workspaceRoot(workspace, options) {
277
279
  function resolveIn(root, candidate) {
278
280
  return resolve(root, candidate);
279
281
  }
282
+ function manifestEvidence(path) {
283
+ return { kind: "manifest", source: path, retrievedAt: (/* @__PURE__ */ new Date()).toISOString() };
284
+ }
285
+ function lockfileEvidence(path) {
286
+ return { kind: "lockfile", source: path, retrievedAt: (/* @__PURE__ */ new Date()).toISOString() };
287
+ }
288
+ function fileExists(filePath) {
289
+ return existsSync(filePath);
290
+ }
291
+ async function fileExistsAsync(filePath) {
292
+ try {
293
+ await access(filePath);
294
+ return true;
295
+ } catch {
296
+ return false;
297
+ }
298
+ }
280
299
 
281
300
  // src/adapters/npm.ts
282
- function detectLockfile(workspaceDir, stopAt) {
301
+ async function detectLockfile(workspaceDir, stopAt) {
283
302
  const candidates = [
284
303
  { file: "pnpm-lock.yaml", kind: "pnpm" },
285
304
  { file: "package-lock.json", kind: "npm" },
@@ -291,7 +310,11 @@ function detectLockfile(workspaceDir, stopAt) {
291
310
  for (; ; ) {
292
311
  for (const c of candidates) {
293
312
  const candidate = join(dir, c.file);
294
- if (existsSync(candidate)) return { kind: c.kind, path: candidate };
313
+ try {
314
+ await access2(candidate);
315
+ return { kind: c.kind, path: candidate };
316
+ } catch {
317
+ }
295
318
  }
296
319
  if (ceiling && dir === ceiling) break;
297
320
  const parent = dirname(dir);
@@ -379,19 +402,29 @@ function parseNpmLockVersions(lockContent) {
379
402
  }
380
403
  return versions;
381
404
  }
382
- function manifestEvidence(path) {
383
- return {
384
- kind: "manifest",
385
- source: path,
386
- retrievedAt: (/* @__PURE__ */ new Date()).toISOString()
387
- };
388
- }
389
- function lockfileEvidence(path) {
390
- return {
391
- kind: "lockfile",
392
- source: path,
393
- retrievedAt: (/* @__PURE__ */ new Date()).toISOString()
394
- };
405
+ function parsePnpmAllVersions(lockContent) {
406
+ const versions = /* @__PURE__ */ new Map();
407
+ let inPackages = false;
408
+ for (const raw of lockContent.split(/\r?\n/)) {
409
+ if (!/^\s/.test(raw)) {
410
+ inPackages = raw.startsWith("packages:") || raw.startsWith("snapshots:");
411
+ continue;
412
+ }
413
+ if (!inPackages) continue;
414
+ const indent = raw.length - raw.trimStart().length;
415
+ if (indent !== 2) continue;
416
+ const trimmed = raw.trim();
417
+ const separator = trimmed.indexOf(":");
418
+ if (separator < 0) continue;
419
+ const key = unquote(trimmed.slice(0, separator));
420
+ const clean = stripPeerSuffix(key);
421
+ const splitAt = clean.lastIndexOf("@");
422
+ if (splitAt <= 0) continue;
423
+ const name = clean.slice(0, splitAt);
424
+ const version = clean.slice(splitAt + 1);
425
+ if (name && /^\d/.test(version) && !versions.has(name)) versions.set(name, version);
426
+ }
427
+ return versions;
395
428
  }
396
429
  function scopeForSection(section) {
397
430
  switch (section) {
@@ -431,29 +464,32 @@ var NpmAdapter = class {
431
464
  let pkg;
432
465
  let manifestContent;
433
466
  try {
434
- manifestContent = readFileSync(manifestPath, "utf-8");
467
+ manifestContent = await readFile(manifestPath, "utf-8");
435
468
  pkg = JSON.parse(manifestContent);
436
469
  } catch {
437
470
  return [];
438
471
  }
439
472
  const manifestEv = manifestEvidence(manifestPath);
440
- const lockInfo = detectLockfile(root, options.projectRoot);
473
+ const lockInfo = await detectLockfile(root, options.projectRoot);
441
474
  const resolvedVersions = /* @__PURE__ */ new Map();
475
+ const allLockVersions = /* @__PURE__ */ new Map();
442
476
  let lockEv;
443
477
  if (lockInfo.kind === "pnpm") {
444
478
  try {
445
- const lockContent = readFileSync(lockInfo.path, "utf-8");
479
+ const lockContent = await readFile(lockInfo.path, "utf-8");
446
480
  const importerPath = relative(dirname(lockInfo.path), root).split(/[/\\]/).filter(Boolean).join("/") || ".";
447
481
  const parsed = parsePnpmImporterVersions(lockContent, importerPath);
448
482
  for (const [k, v] of parsed) resolvedVersions.set(k, v);
483
+ for (const [k, v] of parsePnpmAllVersions(lockContent)) allLockVersions.set(k, v);
449
484
  if (parsed.size > 0) lockEv = lockfileEvidence(lockInfo.path);
450
485
  } catch {
451
486
  }
452
487
  } else if (lockInfo.kind === "npm") {
453
488
  try {
454
- const lockContent = readFileSync(lockInfo.path, "utf-8");
489
+ const lockContent = await readFile(lockInfo.path, "utf-8");
455
490
  const parsed = parseNpmLockVersions(lockContent);
456
491
  for (const [k, v] of parsed) resolvedVersions.set(k, v);
492
+ for (const [k, v] of parsed) allLockVersions.set(k, v);
457
493
  lockEv = lockfileEvidence(lockInfo.path);
458
494
  } catch {
459
495
  }
@@ -494,20 +530,81 @@ var NpmAdapter = class {
494
530
  });
495
531
  }
496
532
  }
533
+ if (options.includeTransitive && lockEv) {
534
+ for (const [name, locked] of allLockVersions) {
535
+ if (seen.has(name)) continue;
536
+ seen.add(name);
537
+ observations.push({
538
+ id: `dep-${workspace.id}-${name}`,
539
+ workspaceId: workspace.id,
540
+ purl: buildPurl({ type: "npm", name, version: locked }),
541
+ ecosystem: "npm",
542
+ name,
543
+ sourceType: "registry",
544
+ direct: false,
545
+ scope: "transitive",
546
+ locked,
547
+ status: "current",
548
+ evidence: [lockEv]
549
+ });
550
+ }
551
+ }
497
552
  return observations;
498
553
  }
499
554
  };
500
555
  var npmAdapter = new NpmAdapter();
501
556
 
502
557
  // src/adapters/python.ts
503
- import { readFileSync as readFileSync2 } from "node:fs";
558
+ import { readFile as readFile2 } from "node:fs/promises";
504
559
  import { join as join2 } from "node:path";
505
- function manifestEvidence2(path) {
506
- return { kind: "manifest", source: path, retrievedAt: (/* @__PURE__ */ new Date()).toISOString() };
560
+
561
+ // src/adapters/parse-utils.ts
562
+ function stripInlineComment(line, marker = "#") {
563
+ let quote;
564
+ let escaped = false;
565
+ for (let index = 0; index < line.length; index++) {
566
+ const character = line.charAt(index);
567
+ if (escaped) {
568
+ escaped = false;
569
+ continue;
570
+ }
571
+ if (character === "\\" && quote === '"') {
572
+ escaped = true;
573
+ continue;
574
+ }
575
+ if (character === '"' || character === "'") {
576
+ quote = quote === character ? void 0 : quote ?? character;
577
+ continue;
578
+ }
579
+ if (!quote && line.startsWith(marker, index)) return line.slice(0, index).trimEnd();
580
+ }
581
+ return line;
507
582
  }
508
- function lockfileEvidence2(path) {
509
- return { kind: "lockfile", source: path, retrievedAt: (/* @__PURE__ */ new Date()).toISOString() };
583
+ function parseTomlKeyValue(line) {
584
+ const cleaned = stripInlineComment(line).trim();
585
+ const match = /^(?:"([^"]+)"|'([^']+)'|([A-Za-z0-9_.-]+))\s*=\s*(.+)$/.exec(cleaned);
586
+ if (!match) return void 0;
587
+ const key = match[1] ?? match[2] ?? match[3];
588
+ const value = match[4];
589
+ return key && value ? { key, value: value.trim() } : void 0;
510
590
  }
591
+ function parseXmlAttributes(source) {
592
+ const attributes = /* @__PURE__ */ new Map();
593
+ const regex = /([A-Za-z_:][\w:.-]*)\s*=\s*(["'])(.*?)\2/g;
594
+ let match;
595
+ while ((match = regex.exec(source)) !== null) {
596
+ const key = match[1];
597
+ const value = match[3];
598
+ if (key !== void 0 && value !== void 0) attributes.set(key, value);
599
+ }
600
+ return attributes;
601
+ }
602
+ function xmlTagValue(source, tag) {
603
+ const escaped = tag.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
604
+ return new RegExp(`<${escaped}(?:\\s[^>]*)?>\\s*([^<]+?)\\s*</${escaped}>`, "i").exec(source)?.[1]?.trim();
605
+ }
606
+
607
+ // src/adapters/python.ts
511
608
  function parseTomlSections(content) {
512
609
  const sections = [];
513
610
  let currentSection = "__header__";
@@ -595,6 +692,19 @@ function parsePyprojectDeps(content) {
595
692
  }
596
693
  }
597
694
  }
695
+ if (section.name === "tool.poetry.dependencies" || section.name.startsWith("tool.poetry.group.")) {
696
+ const scope = section.name === "tool.poetry.dependencies" ? "runtime" : "development";
697
+ for (const raw of section.lines) {
698
+ const entry = parseTomlKeyValue(raw);
699
+ if (!entry) continue;
700
+ const name = entry.key;
701
+ if (!name || name.toLowerCase() === "python") continue;
702
+ const rawValue = entry.value;
703
+ const inlineVersion = rawValue.match(/\bversion\s*=\s*["']([^"']+)["']/)?.[1];
704
+ const stringVersion = rawValue.match(/^["']([^"']+)["']/)?.[1];
705
+ deps.push({ name, constraint: inlineVersion ?? stringVersion, scope });
706
+ }
707
+ }
598
708
  }
599
709
  return deps;
600
710
  }
@@ -631,26 +741,60 @@ function parseRequirementsLockVersions(content) {
631
741
  const line = raw.trim();
632
742
  if (!line || line.startsWith("#") || line.startsWith("-")) continue;
633
743
  const match = line.match(/^([a-zA-Z0-9][a-zA-Z0-9._-]*)\s*==\s*([^\s;]+)/);
634
- if (match) versions.set(match[1], match[2]);
744
+ if (match?.[1] && match[2]) versions.set(normalizePkgName(match[1]), match[2]);
635
745
  }
636
746
  return versions;
637
747
  }
748
+ function parsePipfileLock(content) {
749
+ const versions = /* @__PURE__ */ new Map();
750
+ try {
751
+ const json = JSON.parse(content);
752
+ for (const section of ["default", "develop"]) {
753
+ for (const [name, entry] of Object.entries(json[section] ?? {})) {
754
+ if (entry.version?.startsWith("==")) versions.set(normalizePkgName(name), entry.version.slice(2));
755
+ }
756
+ }
757
+ } catch {
758
+ }
759
+ return versions;
760
+ }
761
+ function parsePoetryLock(content) {
762
+ const versions = /* @__PURE__ */ new Map();
763
+ let currentName;
764
+ for (const raw of content.split("\n")) {
765
+ const line = raw.trim();
766
+ const nameMatch = line.match(/^name\s*=\s*"([^"]+)"/);
767
+ if (nameMatch) {
768
+ currentName = nameMatch[1];
769
+ continue;
770
+ }
771
+ const versionMatch = line.match(/^version\s*=\s*"([^"]+)"/);
772
+ if (versionMatch && currentName) {
773
+ versions.set(normalizePkgName(currentName), versionMatch[1]);
774
+ currentName = void 0;
775
+ }
776
+ }
777
+ return versions;
778
+ }
779
+ function normalizePkgName(name) {
780
+ return name.toLowerCase().replace(/_/g, "-");
781
+ }
638
782
  var PythonAdapter = class {
639
783
  ecosystem = "python";
640
784
  async inventory(workspace, options) {
641
785
  const observations = [];
642
786
  const root = workspaceRoot(workspace, options);
643
787
  const seen = /* @__PURE__ */ new Set();
644
- const hasPyproject = workspace.manifests.some((m) => m.includes("pyproject.toml")) || this.fileExists(join2(root, "pyproject.toml"));
645
- const hasRequirements = workspace.manifests.some((m) => m.includes("requirements.txt")) || this.fileExists(join2(root, "requirements.txt"));
646
- const hasPipfile = workspace.manifests.some((m) => m.includes("Pipfile")) || this.fileExists(join2(root, "Pipfile"));
647
- const lockfilePath = this.detectLockfile(root);
788
+ const hasPyproject = workspace.manifests.some((m) => m.includes("pyproject.toml")) || await fileExistsAsync(join2(root, "pyproject.toml"));
789
+ const hasRequirements = workspace.manifests.some((m) => m.includes("requirements.txt")) || await fileExistsAsync(join2(root, "requirements.txt"));
790
+ const hasPipfile = workspace.manifests.some((m) => m.includes("Pipfile")) || await fileExistsAsync(join2(root, "Pipfile"));
791
+ const lockfilePath = await this.detectLockfile(root);
648
792
  let allDeps = [];
649
793
  let pyprojectEv;
650
794
  if (hasPyproject) {
651
795
  try {
652
- const content = readFileSync2(join2(root, "pyproject.toml"), "utf-8");
653
- pyprojectEv = manifestEvidence2(join2(root, "pyproject.toml"));
796
+ const content = await readFile2(join2(root, "pyproject.toml"), "utf-8");
797
+ pyprojectEv = manifestEvidence(join2(root, "pyproject.toml"));
654
798
  const parsed = parsePyprojectDeps(content);
655
799
  for (const d of parsed) allDeps.push({ ...d, source: "pyproject.toml" });
656
800
  } catch {
@@ -660,8 +804,8 @@ var PythonAdapter = class {
660
804
  let requirementsEv;
661
805
  if (hasRequirements) {
662
806
  try {
663
- const content = readFileSync2(join2(root, "requirements.txt"), "utf-8");
664
- requirementsEv = manifestEvidence2(join2(root, "requirements.txt"));
807
+ const content = await readFile2(join2(root, "requirements.txt"), "utf-8");
808
+ requirementsEv = manifestEvidence(join2(root, "requirements.txt"));
665
809
  const parsed = parseRequirementsTxt(content);
666
810
  for (const d of parsed) {
667
811
  if (!allDeps.some((existing) => existing.name === d.name)) {
@@ -674,8 +818,8 @@ var PythonAdapter = class {
674
818
  }
675
819
  if (hasPipfile) {
676
820
  try {
677
- const content = readFileSync2(join2(root, "Pipfile"), "utf-8");
678
- if (!pyprojectEv) pyprojectEv = manifestEvidence2(join2(root, "Pipfile"));
821
+ const content = await readFile2(join2(root, "Pipfile"), "utf-8");
822
+ if (!pyprojectEv) pyprojectEv = manifestEvidence(join2(root, "Pipfile"));
679
823
  const parsed = parsePipfileDeps(content);
680
824
  for (const d of parsed) {
681
825
  if (!allDeps.some((existing) => existing.name === d.name)) {
@@ -686,10 +830,13 @@ var PythonAdapter = class {
686
830
  }
687
831
  }
688
832
  let lockEv;
833
+ const lockVersions = new Map(reqLockVersions);
689
834
  if (lockfilePath) {
690
835
  try {
691
- readFileSync2(lockfilePath, "utf-8");
692
- lockEv = lockfileEvidence2(lockfilePath);
836
+ const lockContent = await readFile2(lockfilePath, "utf-8");
837
+ const parsed = lockfilePath.endsWith("poetry.lock") || lockfilePath.endsWith("uv.lock") ? parsePoetryLock(lockContent) : parsePipfileLock(lockContent);
838
+ for (const [name, version] of parsed) lockVersions.set(name, version);
839
+ lockEv = lockfileEvidence(lockfilePath);
693
840
  } catch {
694
841
  }
695
842
  }
@@ -697,7 +844,7 @@ var PythonAdapter = class {
697
844
  for (const dep of allDeps) {
698
845
  if (seen.has(dep.name)) continue;
699
846
  seen.add(dep.name);
700
- const locked = reqLockVersions.get(dep.name) || void 0;
847
+ const locked = lockVersions.get(normalizePkgName(dep.name));
701
848
  const isRegistry = !dep.constraint || !dep.constraint.startsWith("file:") && !dep.constraint.startsWith("git+") && !dep.constraint.startsWith("-e");
702
849
  const purl = isRegistry && locked ? buildPurl({ type: "python", name: dep.name, version: locked }) : isRegistry ? buildPurl({ type: "python", name: dep.name }) : void 0;
703
850
  const evidence = [];
@@ -722,23 +869,31 @@ var PythonAdapter = class {
722
869
  evidence
723
870
  });
724
871
  }
725
- return observations;
726
- }
727
- fileExists(filePath) {
728
- try {
729
- readFileSync2(filePath, "utf-8");
730
- return true;
731
- } catch {
732
- return false;
872
+ if (options.includeTransitive && lockEv) {
873
+ for (const [name, locked] of lockVersions) {
874
+ if (seen.has(name)) continue;
875
+ seen.add(name);
876
+ observations.push({
877
+ id: `dep-${workspace.id}-${name}`,
878
+ workspaceId: workspace.id,
879
+ purl: buildPurl({ type: "python", name, version: locked }),
880
+ ecosystem: "python",
881
+ name,
882
+ sourceType: "registry",
883
+ direct: false,
884
+ scope: "transitive",
885
+ locked,
886
+ status: "current",
887
+ evidence: [lockEv]
888
+ });
889
+ }
733
890
  }
891
+ return observations;
734
892
  }
735
- detectLockfile(workspaceRoot2) {
893
+ async detectLockfile(workspaceRoot2) {
736
894
  for (const file of ["Pipfile.lock", "poetry.lock", "uv.lock"]) {
737
- try {
738
- readFileSync2(join2(workspaceRoot2, file), "utf-8");
739
- return join2(workspaceRoot2, file);
740
- } catch {
741
- }
895
+ const candidate = join2(workspaceRoot2, file);
896
+ if (await fileExistsAsync(candidate)) return candidate;
742
897
  }
743
898
  return void 0;
744
899
  }
@@ -746,13 +901,7 @@ var PythonAdapter = class {
746
901
  var pythonAdapter = new PythonAdapter();
747
902
 
748
903
  // src/adapters/rust.ts
749
- import { readFileSync as readFileSync3 } from "node:fs";
750
- function manifestEvidence3(path) {
751
- return { kind: "manifest", source: path, retrievedAt: (/* @__PURE__ */ new Date()).toISOString() };
752
- }
753
- function lockfileEvidence3(path) {
754
- return { kind: "lockfile", source: path, retrievedAt: (/* @__PURE__ */ new Date()).toISOString() };
755
- }
904
+ import { readFileSync } from "node:fs";
756
905
  function parseTomlSections2(content) {
757
906
  const sections = [];
758
907
  let currentSection = "__header__";
@@ -772,33 +921,29 @@ function parseTomlSections2(content) {
772
921
  if (currentLines.length > 0) sections.push({ name: currentSection, lines: currentLines });
773
922
  return sections;
774
923
  }
775
- function parseTomlKeyValue(line) {
776
- const trimmed = line.trim();
777
- if (trimmed.startsWith("#")) return void 0;
778
- const match = trimmed.match(/^([a-zA-Z0-9_-]+)\s*=\s*(.+)$/);
779
- if (!match) return void 0;
780
- return { key: match[1], value: match[2].trim() };
781
- }
782
924
  function extractTomlDeps(sectionLines) {
783
925
  const deps = [];
784
926
  for (const raw of sectionLines) {
785
927
  const line = raw.trim();
786
928
  if (line.startsWith("#") || line === "") continue;
787
- const tableMatch = line.match(/^([a-zA-Z0-9_-]+)\s*=\s*\{\s*(.*?)\s*\}$/);
929
+ const entry = parseTomlKeyValue(line);
930
+ if (!entry) continue;
931
+ const tableMatch = entry.value.match(/^\{\s*(.*?)\s*\}$/);
788
932
  if (tableMatch) {
789
- const name = tableMatch[1];
790
- const inner = tableMatch[2];
933
+ const name = entry.key;
934
+ const inner = tableMatch[1];
935
+ if (inner === void 0) continue;
791
936
  const versionMatch = inner.match(/version\s*=\s*"([^"]+)"/);
792
- deps.push({ name, version: versionMatch ? versionMatch[1] : void 0 });
937
+ const sourceType = /\bgit\s*=/.test(inner) ? "git" : /\bpath\s*=/.test(inner) ? "path" : "registry";
938
+ deps.push({ name, version: versionMatch?.[1], sourceType });
793
939
  continue;
794
940
  }
795
- const simpleMatch = line.match(/^([a-zA-Z0-9_-]+)\s*=\s*"([^"]*)"$/);
941
+ const simpleMatch = entry.value.match(/^"([^"]*)"$/);
796
942
  if (simpleMatch) {
797
- deps.push({ name: simpleMatch[1], version: simpleMatch[2] || void 0 });
943
+ deps.push({ name: entry.key, version: simpleMatch[1] || void 0, sourceType: "registry" });
798
944
  continue;
799
945
  }
800
- const partialMatch = parseTomlKeyValue(line);
801
- if (partialMatch && !partialMatch.value.startsWith("{") && !partialMatch.value.startsWith('"')) {
946
+ if (!entry.value.startsWith("{") && !entry.value.startsWith('"')) {
802
947
  }
803
948
  }
804
949
  return deps;
@@ -854,23 +999,23 @@ var RustAdapter = class {
854
999
  const observations = [];
855
1000
  const root = workspaceRoot(workspace, options);
856
1001
  const seen = /* @__PURE__ */ new Set();
857
- const cargoTomlPath = workspace.manifests.find((m) => m.includes("Cargo.toml")) || (this.fileExists(resolveIn(root, "Cargo.toml")) ? "Cargo.toml" : void 0);
1002
+ const cargoTomlPath = workspace.manifests.find((m) => m.includes("Cargo.toml")) || (fileExists(resolveIn(root, "Cargo.toml")) ? "Cargo.toml" : void 0);
858
1003
  if (!cargoTomlPath) return [];
859
1004
  const fullManifestPath = resolveIn(root, cargoTomlPath);
860
1005
  let cargoContent;
861
1006
  try {
862
- cargoContent = readFileSync3(fullManifestPath, "utf-8");
1007
+ cargoContent = readFileSync(fullManifestPath, "utf-8");
863
1008
  } catch {
864
1009
  return [];
865
1010
  }
866
- const manifestEv = manifestEvidence3(fullManifestPath);
1011
+ const manifestEv = manifestEvidence(fullManifestPath);
867
1012
  const cargoLockPath = resolveIn(root, "Cargo.lock");
868
1013
  let lockVersions = /* @__PURE__ */ new Map();
869
1014
  let lockEv;
870
1015
  try {
871
- const lockContent = readFileSync3(cargoLockPath, "utf-8");
1016
+ const lockContent = readFileSync(cargoLockPath, "utf-8");
872
1017
  lockVersions = parseCargoLock(lockContent);
873
- lockEv = lockfileEvidence3(cargoLockPath);
1018
+ lockEv = lockfileEvidence(cargoLockPath);
874
1019
  } catch {
875
1020
  }
876
1021
  const sections = parseTomlSections2(cargoContent);
@@ -891,18 +1036,18 @@ var RustAdapter = class {
891
1036
  if (seen.has(dep.name)) continue;
892
1037
  seen.add(dep.name);
893
1038
  const locked = lockVersions.get(dep.name) || dep.version;
894
- const isRegistry = !dep.version || !dep.version.startsWith("path=") && !dep.version.startsWith("git=") && !dep.version.startsWith("../");
1039
+ const isRegistry = dep.sourceType === "registry";
895
1040
  const purl = isRegistry && locked ? buildPurl({ type: "rust", name: dep.name, version: locked }) : isRegistry ? buildPurl({ type: "rust", name: dep.name }) : void 0;
896
1041
  const evidence = [manifestEv];
897
1042
  if (lockEv && locked && lockVersions.has(dep.name)) evidence.push(lockEv);
898
- const status = dep.version && (dep.version.startsWith("path=") || dep.version.startsWith("git=")) ? dep.version.startsWith("git=") ? "git_dependency" : "local_path" : "current";
1043
+ const status = dep.sourceType === "git" ? "git_dependency" : dep.sourceType === "path" ? "local_path" : "current";
899
1044
  observations.push({
900
1045
  id: `dep-${workspace.id}-${dep.name}`,
901
1046
  workspaceId: workspace.id,
902
1047
  ...purl ? { purl } : {},
903
1048
  ecosystem: "rust",
904
1049
  name: dep.name,
905
- sourceType: isRegistry ? "registry" : status === "local_path" ? "path" : "git",
1050
+ sourceType: dep.sourceType,
906
1051
  direct: true,
907
1052
  scope,
908
1053
  ...dep.version ? { requested: dep.version } : {},
@@ -912,27 +1057,32 @@ var RustAdapter = class {
912
1057
  });
913
1058
  }
914
1059
  }
915
- return observations;
916
- }
917
- fileExists(filePath) {
918
- try {
919
- readFileSync3(filePath, "utf-8");
920
- return true;
921
- } catch {
922
- return false;
1060
+ if (options.includeTransitive && lockEv) {
1061
+ for (const [name, locked] of lockVersions) {
1062
+ if (seen.has(name)) continue;
1063
+ seen.add(name);
1064
+ observations.push({
1065
+ id: `dep-${workspace.id}-${name}`,
1066
+ workspaceId: workspace.id,
1067
+ purl: buildPurl({ type: "rust", name, version: locked }),
1068
+ ecosystem: "rust",
1069
+ name,
1070
+ sourceType: "registry",
1071
+ direct: false,
1072
+ scope: "transitive",
1073
+ locked,
1074
+ status: "current",
1075
+ evidence: [lockEv]
1076
+ });
1077
+ }
923
1078
  }
1079
+ return observations;
924
1080
  }
925
1081
  };
926
1082
  var rustAdapter = new RustAdapter();
927
1083
 
928
1084
  // src/adapters/go.ts
929
- import { readFileSync as readFileSync4 } from "node:fs";
930
- function manifestEvidence4(path) {
931
- return { kind: "manifest", source: path, retrievedAt: (/* @__PURE__ */ new Date()).toISOString() };
932
- }
933
- function lockfileEvidence4(path) {
934
- return { kind: "lockfile", source: path, retrievedAt: (/* @__PURE__ */ new Date()).toISOString() };
935
- }
1085
+ import { readFileSync as readFileSync2 } from "node:fs";
936
1086
  function cleanGoVersion(v) {
937
1087
  return v.replace(/^v/i, "");
938
1088
  }
@@ -973,6 +1123,30 @@ function parseGoMod(content) {
973
1123
  }
974
1124
  return deps;
975
1125
  }
1126
+ function parseGoReplacements(content) {
1127
+ const replacements = /* @__PURE__ */ new Map();
1128
+ let inBlock = false;
1129
+ for (const raw of content.split("\n")) {
1130
+ const line = raw.trim();
1131
+ if (line === "replace (") {
1132
+ inBlock = true;
1133
+ continue;
1134
+ }
1135
+ if (inBlock && line === ")") {
1136
+ inBlock = false;
1137
+ continue;
1138
+ }
1139
+ const candidate = inBlock ? line : line.startsWith("replace ") ? line.slice(8).trim() : "";
1140
+ const match = candidate.match(/^(\S+)(?:\s+v\S+)?\s+=>\s+(\S+)/);
1141
+ if (!match) continue;
1142
+ const modulePath = match[1];
1143
+ const target = match[2];
1144
+ if (!modulePath || !target) continue;
1145
+ const local = target.startsWith(".") || target.startsWith("/") || /^[A-Za-z]:[\\/]/.test(target);
1146
+ replacements.set(modulePath, local ? "path" : "git");
1147
+ }
1148
+ return replacements;
1149
+ }
976
1150
  function parseGoSum(content) {
977
1151
  const versions = /* @__PURE__ */ new Map();
978
1152
  for (const raw of content.split("\n")) {
@@ -1003,25 +1177,26 @@ var GoAdapter = class {
1003
1177
  const observations = [];
1004
1178
  const root = workspaceRoot(workspace, options);
1005
1179
  const seen = /* @__PURE__ */ new Set();
1006
- const goModPath = workspace.manifests.find((m) => m.includes("go.mod")) || (this.fileExists(resolveIn(root, "go.mod")) ? "go.mod" : void 0);
1180
+ const goModPath = workspace.manifests.find((m) => m.includes("go.mod")) || (fileExists(resolveIn(root, "go.mod")) ? "go.mod" : void 0);
1007
1181
  if (!goModPath) return [];
1008
1182
  const fullManifestPath = resolveIn(root, goModPath);
1009
1183
  let goModContent;
1010
1184
  try {
1011
- goModContent = readFileSync4(fullManifestPath, "utf-8");
1185
+ goModContent = readFileSync2(fullManifestPath, "utf-8");
1012
1186
  } catch {
1013
1187
  return [];
1014
1188
  }
1015
- const manifestEv = manifestEvidence4(fullManifestPath);
1189
+ const manifestEv = manifestEvidence(fullManifestPath);
1016
1190
  const requires = parseGoMod(goModContent);
1191
+ const replacements = parseGoReplacements(goModContent);
1017
1192
  const modName = parseGoModuleName(goModContent);
1018
1193
  const goSumPath = resolveIn(root, "go.sum");
1019
1194
  let lockVersions = /* @__PURE__ */ new Map();
1020
1195
  let lockEv;
1021
1196
  try {
1022
- const sumContent = readFileSync4(goSumPath, "utf-8");
1197
+ const sumContent = readFileSync2(goSumPath, "utf-8");
1023
1198
  lockVersions = parseGoSum(sumContent);
1024
- lockEv = lockfileEvidence4(goSumPath);
1199
+ lockEv = lockfileEvidence(goSumPath);
1025
1200
  } catch {
1026
1201
  }
1027
1202
  for (const req of requires) {
@@ -1031,53 +1206,44 @@ var GoAdapter = class {
1031
1206
  const scope = req.indirect ? "transitive" : "runtime";
1032
1207
  const direct = !req.indirect;
1033
1208
  const locked = lockVersions.get(req.modulePath) || req.version;
1034
- const purl = buildPurl({ type: "go", name: req.modulePath, version: locked });
1209
+ const replacement = replacements.get(req.modulePath);
1210
+ const purl = replacement ? void 0 : buildPurl({ type: "go", name: req.modulePath, version: locked });
1035
1211
  const evidence = [manifestEv];
1036
1212
  if (lockEv && lockVersions.has(req.modulePath)) evidence.push(lockEv);
1037
1213
  observations.push({
1038
1214
  id: `dep-${workspace.id}-${req.modulePath}`,
1039
1215
  workspaceId: workspace.id,
1040
- purl,
1216
+ ...purl ? { purl } : {},
1041
1217
  ecosystem: "go",
1042
1218
  name: req.modulePath,
1043
- sourceType: "registry",
1219
+ sourceType: replacement ?? "registry",
1044
1220
  direct,
1045
1221
  scope,
1046
1222
  requested: req.version,
1047
1223
  ...locked ? { locked } : {},
1048
- status: "current",
1224
+ status: replacement === "path" ? "local_path" : replacement === "git" ? "git_dependency" : "current",
1049
1225
  evidence
1050
1226
  });
1051
1227
  }
1052
1228
  return observations;
1053
1229
  }
1054
- fileExists(filePath) {
1055
- try {
1056
- readFileSync4(filePath, "utf-8");
1057
- return true;
1058
- } catch {
1059
- return false;
1060
- }
1061
- }
1062
1230
  };
1063
1231
  var goAdapter = new GoAdapter();
1064
1232
 
1065
1233
  // src/adapters/dotnet.ts
1066
- import { readFileSync as readFileSync5, readdirSync } from "node:fs";
1234
+ import { readFile as readFile3, readdir } from "node:fs/promises";
1067
1235
  import { join as join3 } from "node:path";
1068
- function manifestEvidence5(path) {
1069
- return { kind: "manifest", source: path, retrievedAt: (/* @__PURE__ */ new Date()).toISOString() };
1070
- }
1071
- function lockfileEvidence5(path) {
1072
- return { kind: "lockfile", source: path, retrievedAt: (/* @__PURE__ */ new Date()).toISOString() };
1073
- }
1074
1236
  function parseCsproj(content) {
1075
1237
  const refs = [];
1076
- const regex = /<PackageReference\s+Include\s*=\s*"([^"]+)"\s*(?:Version\s*=\s*"([^"]*)")?\s*\/?\s*>/g;
1238
+ const regex = /<PackageReference\b([^>]*?)(?:\/>|>([\s\S]*?)<\/PackageReference>)/gi;
1077
1239
  let match;
1078
1240
  while ((match = regex.exec(content)) !== null) {
1079
- const name = match[1];
1080
- const version = match[2] || void 0;
1241
+ const attributes = match[1] ?? "";
1242
+ const body = match[2] ?? "";
1243
+ const parsedAttributes = parseXmlAttributes(attributes);
1244
+ const name = parsedAttributes.get("Include");
1245
+ if (!name) continue;
1246
+ const version = parsedAttributes.get("Version") ?? xmlTagValue(body, "Version");
1081
1247
  refs.push({ name, version });
1082
1248
  }
1083
1249
  return refs;
@@ -1110,7 +1276,7 @@ var DotNetAdapter = class {
1110
1276
  const seen = /* @__PURE__ */ new Set();
1111
1277
  let csprojPath;
1112
1278
  try {
1113
- const files = readdirSync(root);
1279
+ const files = await readdir(root);
1114
1280
  const csproj = files.find((f) => f.endsWith(".csproj"));
1115
1281
  if (csproj) csprojPath = join3(root, csproj);
1116
1282
  } catch {
@@ -1118,19 +1284,19 @@ var DotNetAdapter = class {
1118
1284
  if (!csprojPath) return [];
1119
1285
  let csprojContent;
1120
1286
  try {
1121
- csprojContent = readFileSync5(csprojPath, "utf-8");
1287
+ csprojContent = await readFile3(csprojPath, "utf-8");
1122
1288
  } catch {
1123
1289
  return [];
1124
1290
  }
1125
- const manifestEv = manifestEvidence5(csprojPath);
1291
+ const manifestEv = manifestEvidence(csprojPath);
1126
1292
  const refs = parseCsproj(csprojContent);
1127
1293
  const assetsPath = join3(root, "project.assets.json");
1128
1294
  let lockVersions = /* @__PURE__ */ new Map();
1129
1295
  let lockEv;
1130
1296
  try {
1131
- const assetsContent = readFileSync5(assetsPath, "utf-8");
1297
+ const assetsContent = await readFile3(assetsPath, "utf-8");
1132
1298
  lockVersions = parseProjectAssetsJson(assetsContent);
1133
- lockEv = lockfileEvidence5(assetsPath);
1299
+ lockEv = lockfileEvidence(assetsPath);
1134
1300
  } catch {
1135
1301
  }
1136
1302
  for (const ref of refs) {
@@ -1161,14 +1327,8 @@ var DotNetAdapter = class {
1161
1327
  var dotNetAdapter = new DotNetAdapter();
1162
1328
 
1163
1329
  // src/adapters/php.ts
1164
- import { readFileSync as readFileSync6 } from "node:fs";
1330
+ import { readFileSync as readFileSync3 } from "node:fs";
1165
1331
  import { join as join4 } from "node:path";
1166
- function manifestEvidence6(path) {
1167
- return { kind: "manifest", source: path, retrievedAt: (/* @__PURE__ */ new Date()).toISOString() };
1168
- }
1169
- function lockfileEvidence6(path) {
1170
- return { kind: "lockfile", source: path, retrievedAt: (/* @__PURE__ */ new Date()).toISOString() };
1171
- }
1172
1332
  function parseComposerLock(content) {
1173
1333
  const versions = /* @__PURE__ */ new Map();
1174
1334
  try {
@@ -1196,15 +1356,15 @@ var PhpAdapter = class {
1196
1356
  const observations = [];
1197
1357
  const root = workspaceRoot(workspace, options);
1198
1358
  const seen = /* @__PURE__ */ new Set();
1199
- const composerJsonPath = workspace.manifests.find((m) => m.includes("composer.json")) || (this.fileExists(join4(root, "composer.json")) ? join4(root, "composer.json") : void 0);
1359
+ const composerJsonPath = workspace.manifests.find((m) => m.includes("composer.json")) || (fileExists(join4(root, "composer.json")) ? join4(root, "composer.json") : void 0);
1200
1360
  if (!composerJsonPath) return [];
1201
1361
  let content;
1202
1362
  try {
1203
- content = readFileSync6(composerJsonPath, "utf-8");
1363
+ content = readFileSync3(composerJsonPath, "utf-8");
1204
1364
  } catch {
1205
1365
  return [];
1206
1366
  }
1207
- const manifestEv = manifestEvidence6(composerJsonPath);
1367
+ const manifestEv = manifestEvidence(composerJsonPath);
1208
1368
  let composerJson;
1209
1369
  try {
1210
1370
  composerJson = JSON.parse(content);
@@ -1215,9 +1375,9 @@ var PhpAdapter = class {
1215
1375
  let lockVersions = /* @__PURE__ */ new Map();
1216
1376
  let lockEv;
1217
1377
  try {
1218
- const lockContent = readFileSync6(lockPath, "utf-8");
1378
+ const lockContent = readFileSync3(lockPath, "utf-8");
1219
1379
  lockVersions = parseComposerLock(lockContent);
1220
- lockEv = lockfileEvidence6(lockPath);
1380
+ lockEv = lockfileEvidence(lockPath);
1221
1381
  } catch {
1222
1382
  }
1223
1383
  const sections = [
@@ -1254,26 +1414,12 @@ var PhpAdapter = class {
1254
1414
  }
1255
1415
  return observations;
1256
1416
  }
1257
- fileExists(filePath) {
1258
- try {
1259
- readFileSync6(filePath, "utf-8");
1260
- return true;
1261
- } catch {
1262
- return false;
1263
- }
1264
- }
1265
1417
  };
1266
1418
  var phpAdapter = new PhpAdapter();
1267
1419
 
1268
1420
  // src/adapters/dart.ts
1269
- import { readFileSync as readFileSync7 } from "node:fs";
1421
+ import { readFileSync as readFileSync4 } from "node:fs";
1270
1422
  import { join as join5 } from "node:path";
1271
- function manifestEvidence7(path) {
1272
- return { kind: "manifest", source: path, retrievedAt: (/* @__PURE__ */ new Date()).toISOString() };
1273
- }
1274
- function lockfileEvidence7(path) {
1275
- return { kind: "lockfile", source: path, retrievedAt: (/* @__PURE__ */ new Date()).toISOString() };
1276
- }
1277
1423
  function parsePubspecYaml(content) {
1278
1424
  const sections = /* @__PURE__ */ new Map();
1279
1425
  let currentSection;
@@ -1301,6 +1447,14 @@ function parsePubspecYaml(content) {
1301
1447
  }
1302
1448
  const sec = sections.get(currentSection);
1303
1449
  sec.set(currentName, constraint);
1450
+ continue;
1451
+ }
1452
+ if (currentName && line.startsWith(" ")) {
1453
+ const sec = sections.get(currentSection);
1454
+ if (!sec) continue;
1455
+ if (/^sdk:\s*flutter\b/.test(trimmed)) sec.set(currentName, "sdk:flutter");
1456
+ else if (/^git:\s*/.test(trimmed)) sec.set(currentName, `git:${trimmed.slice(4).trim()}`);
1457
+ else if (/^path:\s*/.test(trimmed)) sec.set(currentName, `path:${trimmed.slice(5).trim()}`);
1304
1458
  }
1305
1459
  }
1306
1460
  return sections;
@@ -1339,23 +1493,23 @@ var DartAdapter = class {
1339
1493
  const observations = [];
1340
1494
  const root = workspaceRoot(workspace, options);
1341
1495
  const seen = /* @__PURE__ */ new Set();
1342
- const pubspecPath = workspace.manifests.find((m) => m.includes("pubspec.yaml")) || (this.fileExists(join5(root, "pubspec.yaml")) ? join5(root, "pubspec.yaml") : void 0);
1496
+ const pubspecPath = workspace.manifests.find((m) => m.includes("pubspec.yaml")) || (fileExists(join5(root, "pubspec.yaml")) ? join5(root, "pubspec.yaml") : void 0);
1343
1497
  if (!pubspecPath) return [];
1344
1498
  let content;
1345
1499
  try {
1346
- content = readFileSync7(pubspecPath, "utf-8");
1500
+ content = readFileSync4(pubspecPath, "utf-8");
1347
1501
  } catch {
1348
1502
  return [];
1349
1503
  }
1350
- const manifestEv = manifestEvidence7(pubspecPath);
1504
+ const manifestEv = manifestEvidence(pubspecPath);
1351
1505
  const sections = parsePubspecYaml(content);
1352
1506
  const lockPath = join5(root, "pubspec.lock");
1353
1507
  let lockVersions = /* @__PURE__ */ new Map();
1354
1508
  let lockEv;
1355
1509
  try {
1356
- const lockContent = readFileSync7(lockPath, "utf-8");
1510
+ const lockContent = readFileSync4(lockPath, "utf-8");
1357
1511
  lockVersions = parsePubspecLock(lockContent);
1358
- lockEv = lockfileEvidence7(lockPath);
1512
+ lockEv = lockfileEvidence(lockPath);
1359
1513
  } catch {
1360
1514
  }
1361
1515
  const sectionMapping = [
@@ -1369,7 +1523,7 @@ var DartAdapter = class {
1369
1523
  for (const [name, constraint] of deps) {
1370
1524
  if (seen.has(name)) continue;
1371
1525
  seen.add(name);
1372
- if (constraint === "*" || constraint.startsWith("{")) continue;
1526
+ if (constraint === "*" || constraint === "sdk:flutter" || constraint.startsWith("{")) continue;
1373
1527
  const locked = lockVersions.get(name);
1374
1528
  let status = "current";
1375
1529
  let sourceType = "registry";
@@ -1405,32 +1559,57 @@ var DartAdapter = class {
1405
1559
  }
1406
1560
  return observations;
1407
1561
  }
1408
- fileExists(filePath) {
1409
- try {
1410
- readFileSync7(filePath, "utf-8");
1411
- return true;
1412
- } catch {
1413
- return false;
1414
- }
1415
- }
1416
1562
  };
1417
1563
  var dartAdapter = new DartAdapter();
1418
1564
 
1419
1565
  // src/adapters/maven.ts
1420
- import { readFileSync as readFileSync8 } from "node:fs";
1421
- function manifestEvidence8(path) {
1422
- return { kind: "manifest", source: path, retrievedAt: (/* @__PURE__ */ new Date()).toISOString() };
1423
- }
1566
+ import { readFileSync as readFileSync5 } from "node:fs";
1424
1567
  function parsePomDependencies(xml) {
1425
1568
  const deps = [];
1569
+ const properties = /* @__PURE__ */ new Map();
1570
+ const propertiesBlock = /<properties>([\s\S]*?)<\/properties>/.exec(xml)?.[1] ?? "";
1571
+ const propertyRegex = /<([A-Za-z0-9_.-]+)>\s*([^<]+?)\s*<\/\1>/g;
1572
+ let propertyMatch;
1573
+ while ((propertyMatch = propertyRegex.exec(propertiesBlock)) !== null) {
1574
+ const key = propertyMatch[1];
1575
+ const value = propertyMatch[2];
1576
+ if (key && value) properties.set(key, value.trim());
1577
+ }
1578
+ const parentBlock = /<parent>([\s\S]*?)<\/parent>/.exec(xml)?.[1];
1579
+ if (parentBlock) {
1580
+ const parentVersion = xmlTagValue(parentBlock, "version");
1581
+ const parentGroup = xmlTagValue(parentBlock, "groupId");
1582
+ if (parentVersion) {
1583
+ properties.set("parent.version", parentVersion);
1584
+ properties.set("project.parent.version", parentVersion);
1585
+ }
1586
+ if (parentGroup) {
1587
+ properties.set("parent.groupId", parentGroup);
1588
+ properties.set("project.parent.groupId", parentGroup);
1589
+ }
1590
+ }
1591
+ const managed = /* @__PURE__ */ new Map();
1592
+ const managementBlock = /<dependencyManagement>([\s\S]*?)<\/dependencyManagement>/.exec(xml)?.[1] ?? "";
1593
+ const managementRegex = /<dependency>\s*([\s\S]*?)<\/dependency>/g;
1594
+ let managementMatch;
1595
+ while ((managementMatch = managementRegex.exec(managementBlock)) !== null) {
1596
+ const block = managementMatch[1];
1597
+ if (!block) continue;
1598
+ const groupId = xmlTagValue(block, "groupId");
1599
+ const artifactId = xmlTagValue(block, "artifactId");
1600
+ const version = xmlTagValue(block, "version");
1601
+ if (groupId && artifactId && version) managed.set(`${groupId}:${artifactId}`, version);
1602
+ }
1603
+ const directXml = xml.replace(/<dependencyManagement>[\s\S]*?<\/dependencyManagement>/g, "");
1426
1604
  const depRegex = /<dependency>\s*([\s\S]*?)<\/dependency>/g;
1427
1605
  let match;
1428
- while ((match = depRegex.exec(xml)) !== null) {
1606
+ while ((match = depRegex.exec(directXml)) !== null) {
1429
1607
  const block = match[1];
1430
- const groupId = block.match(/<groupId>([^<]+)<\/groupId>/)?.[1]?.trim();
1431
- const artifactId = block.match(/<artifactId>([^<]+)<\/artifactId>/)?.[1]?.trim();
1432
- const version = block.match(/<version>([^<]+)<\/version>/)?.[1]?.trim();
1433
- const scope = block.match(/<scope>([^<]+)<\/scope>/)?.[1]?.trim();
1608
+ const groupId = xmlTagValue(block, "groupId");
1609
+ const artifactId = xmlTagValue(block, "artifactId");
1610
+ const rawVersion = xmlTagValue(block, "version") ?? (groupId && artifactId ? managed.get(`${groupId}:${artifactId}`) : void 0);
1611
+ const version = rawVersion?.replace(/\$\{([^}]+)\}/g, (_whole, key) => properties.get(key) ?? `\${${key}}`);
1612
+ const scope = xmlTagValue(block, "scope");
1434
1613
  if (groupId && artifactId) {
1435
1614
  deps.push({ groupId, artifactId, version, scope });
1436
1615
  }
@@ -1459,11 +1638,11 @@ var MavenAdapter = class {
1459
1638
  if (!pomPath) return [];
1460
1639
  let content;
1461
1640
  try {
1462
- content = readFileSync8(pomPath, "utf-8");
1641
+ content = readFileSync5(pomPath, "utf-8");
1463
1642
  } catch {
1464
1643
  return [];
1465
1644
  }
1466
- const manifestEv = manifestEvidence8(pomPath);
1645
+ const manifestEv = manifestEvidence(pomPath);
1467
1646
  const deps = parsePomDependencies(content);
1468
1647
  const seen = /* @__PURE__ */ new Set();
1469
1648
  for (const dep of deps) {
@@ -1490,22 +1669,141 @@ var MavenAdapter = class {
1490
1669
  };
1491
1670
  var mavenAdapter = new MavenAdapter();
1492
1671
 
1493
- // src/adapters/ruby.ts
1494
- import { readFileSync as readFileSync9 } from "node:fs";
1495
- function manifestEvidence9(path) {
1496
- return { kind: "manifest", source: path, retrievedAt: (/* @__PURE__ */ new Date()).toISOString() };
1672
+ // src/adapters/gradle.ts
1673
+ import { readFile as readFile4 } from "node:fs/promises";
1674
+ import { join as join6 } from "node:path";
1675
+ function scopeForConfiguration(configuration) {
1676
+ if (/test/i.test(configuration)) return "development";
1677
+ if (/compileOnly|annotationProcessor/i.test(configuration)) return "build";
1678
+ if (/runtimeOnly/i.test(configuration)) return "runtime";
1679
+ return "runtime";
1497
1680
  }
1498
- function lockfileEvidence8(path) {
1499
- return { kind: "lockfile", source: path, retrievedAt: (/* @__PURE__ */ new Date()).toISOString() };
1681
+ function parseVersionCatalog(content) {
1682
+ const versions = /* @__PURE__ */ new Map();
1683
+ const libraries = /* @__PURE__ */ new Map();
1684
+ let section = "";
1685
+ for (const raw of content.split("\n")) {
1686
+ const line = raw.replace(/#.*$/, "").trim();
1687
+ const header = /^\[([^\]]+)\]$/.exec(line);
1688
+ if (header) {
1689
+ section = header[1] ?? "";
1690
+ continue;
1691
+ }
1692
+ const entry = /^(?:"([^"]+)"|([\w.-]+))\s*=\s*(.+)$/.exec(line);
1693
+ if (!entry) continue;
1694
+ const key = entry[1] ?? entry[2];
1695
+ const value = entry[3];
1696
+ if (!key || !value) continue;
1697
+ if (section === "versions") {
1698
+ const version = /^["']([^"']+)["']/.exec(value)?.[1];
1699
+ if (version) versions.set(key, version);
1700
+ } else if (section === "libraries") {
1701
+ const module = /\bmodule\s*=\s*["']([^"']+)["']/.exec(value)?.[1] ?? (() => {
1702
+ const group = /\bgroup\s*=\s*["']([^"']+)["']/.exec(value)?.[1];
1703
+ const name = /\bname\s*=\s*["']([^"']+)["']/.exec(value)?.[1];
1704
+ return group && name ? `${group}:${name}` : void 0;
1705
+ })();
1706
+ if (!module) continue;
1707
+ const version = /\bversion\s*=\s*["']([^"']+)["']/.exec(value)?.[1];
1708
+ const ref = /\bversion\.ref\s*=\s*["']([^"']+)["']/.exec(value)?.[1];
1709
+ libraries.set(key.replace(/-/g, "."), `${module}:${version ?? (ref ? versions.get(ref) ?? "" : "")}`.replace(/:$/, ""));
1710
+ }
1711
+ }
1712
+ return libraries;
1713
+ }
1714
+ function parseGradleManifest(content, catalog) {
1715
+ const deps = [];
1716
+ const coordinateRegex = /\b(implementation|api|compileOnly|runtimeOnly|testImplementation|testRuntimeOnly|annotationProcessor)\s*(?:\(|\s)\s*["']([^"']+)["']/g;
1717
+ let match;
1718
+ while ((match = coordinateRegex.exec(content)) !== null) {
1719
+ const configuration = match[1];
1720
+ const value = match[2];
1721
+ if (!configuration || !value) continue;
1722
+ const [group, artifact, version] = value.split(":");
1723
+ if (!group || !artifact) continue;
1724
+ deps.push({ name: `${group}:${artifact}`, requested: version, scope: scopeForConfiguration(configuration) });
1725
+ }
1726
+ const aliasRegex = /\b(implementation|api|compileOnly|runtimeOnly|testImplementation|testRuntimeOnly)\s*\(\s*libs\.([\w.]+)\s*\)/g;
1727
+ while ((match = aliasRegex.exec(content)) !== null) {
1728
+ const configuration = match[1];
1729
+ const alias = match[2];
1730
+ if (!configuration || !alias) continue;
1731
+ const coordinate = catalog.get(alias);
1732
+ if (!coordinate) continue;
1733
+ const [group, artifact, version] = coordinate.split(":");
1734
+ if (!group || !artifact) continue;
1735
+ deps.push({ name: `${group}:${artifact}`, requested: version, scope: scopeForConfiguration(configuration) });
1736
+ }
1737
+ return deps;
1500
1738
  }
1739
+ function parseGradleLock(content) {
1740
+ const locked = /* @__PURE__ */ new Map();
1741
+ for (const raw of content.split("\n")) {
1742
+ const line = raw.replace(/#.*/, "").trim();
1743
+ const coordinate = /^([^:\s=]+):([^:\s=]+):([^=\s]+)(?:=.*)?$/.exec(line);
1744
+ if (coordinate?.[1] && coordinate[2] && coordinate[3]) locked.set(`${coordinate[1]}:${coordinate[2]}`, coordinate[3]);
1745
+ }
1746
+ return locked;
1747
+ }
1748
+ var GradleAdapter = class {
1749
+ ecosystem = "gradle";
1750
+ async inventory(workspace, options) {
1751
+ const root = workspaceRoot(workspace, options);
1752
+ const manifestPath = workspace.manifests.find((path) => /build\.gradle(?:\.kts)?$/.test(path)) ?? (await fileExistsAsync(join6(root, "build.gradle.kts")) ? join6(root, "build.gradle.kts") : join6(root, "build.gradle"));
1753
+ if (!await fileExistsAsync(resolveIn(root, manifestPath))) return [];
1754
+ const catalogPath = join6(root, "gradle", "libs.versions.toml");
1755
+ const catalog = await fileExistsAsync(catalogPath) ? parseVersionCatalog(await readFile4(catalogPath, "utf8")) : /* @__PURE__ */ new Map();
1756
+ const direct = parseGradleManifest(await readFile4(resolveIn(root, manifestPath), "utf8"), catalog);
1757
+ const lockPath = workspace.lockfiles.find((path) => path.endsWith("gradle.lockfile")) ?? join6(root, "gradle.lockfile");
1758
+ const locked = await fileExistsAsync(resolveIn(root, lockPath)) ? parseGradleLock(await readFile4(resolveIn(root, lockPath), "utf8")) : /* @__PURE__ */ new Map();
1759
+ const manifestEv = manifestEvidence(resolveIn(root, manifestPath));
1760
+ const lockEv = locked.size > 0 ? lockfileEvidence(resolveIn(root, lockPath)) : void 0;
1761
+ const observations = [];
1762
+ const seen = /* @__PURE__ */ new Set();
1763
+ const add = (name, requested, scope, isDirect) => {
1764
+ if (seen.has(name)) return;
1765
+ seen.add(name);
1766
+ const version = locked.get(name) ?? requested;
1767
+ const lockedVersion = locked.get(name);
1768
+ const evidence = isDirect ? [manifestEv] : [];
1769
+ if (lockEv && locked.has(name)) evidence.push(lockEv);
1770
+ observations.push({
1771
+ id: `dep-${workspace.id}-${name}`,
1772
+ workspaceId: workspace.id,
1773
+ purl: buildPurl({ type: "maven", name, ...version ? { version } : {} }),
1774
+ ecosystem: "gradle",
1775
+ name,
1776
+ sourceType: "registry",
1777
+ direct: isDirect,
1778
+ scope: isDirect ? scope : "transitive",
1779
+ ...requested ? { requested } : {},
1780
+ ...lockedVersion ? { locked: lockedVersion } : {},
1781
+ status: "current",
1782
+ evidence
1783
+ });
1784
+ };
1785
+ for (const dep of direct) add(dep.name, dep.requested, dep.scope, true);
1786
+ if (options.includeTransitive) {
1787
+ for (const [name] of locked) add(name, void 0, "transitive", false);
1788
+ }
1789
+ return observations;
1790
+ }
1791
+ };
1792
+ var gradleAdapter = new GradleAdapter();
1793
+
1794
+ // src/adapters/ruby.ts
1795
+ import { readFileSync as readFileSync6 } from "node:fs";
1501
1796
  function parseGemfile(content) {
1502
1797
  const gems = [];
1503
- const gemRegex = /gem\s+['"]([^'"]+)['"](?:\s*,\s*['"]([^'"]+)['"])?/g;
1798
+ const gemRegex = /gem\s+['"]([^'"]+)['"]([^\n]*)/g;
1504
1799
  let match;
1505
1800
  while ((match = gemRegex.exec(content)) !== null) {
1506
1801
  const name = match[1];
1507
1802
  if (name === "rails" || name === "ruby") continue;
1508
- gems.push({ name, version: match[2] });
1803
+ const tail = match[2] ?? "";
1804
+ const version = /^\s*,\s*['"]([^'"]+)['"]/.exec(tail)?.[1];
1805
+ const sourceType = /\b(?:git|github):/.test(tail) ? "git" : /\bpath:/.test(tail) ? "path" : "registry";
1806
+ gems.push({ name, version, sourceType });
1509
1807
  }
1510
1808
  return gems;
1511
1809
  }
@@ -1514,7 +1812,11 @@ function parseGemfileLock(content) {
1514
1812
  const lines = content.split("\n");
1515
1813
  let inSpecs = false;
1516
1814
  for (const line of lines) {
1517
- if (line.startsWith("GEM")) {
1815
+ if (/^(?:GEM|GIT|PATH)$/.test(line.trim())) {
1816
+ inSpecs = false;
1817
+ continue;
1818
+ }
1819
+ if (/^\s{2}specs:\s*$/.test(line)) {
1518
1820
  inSpecs = true;
1519
1821
  continue;
1520
1822
  }
@@ -1539,11 +1841,11 @@ var RubyAdapter = class {
1539
1841
  if (!gemfilePath) return [];
1540
1842
  let content;
1541
1843
  try {
1542
- content = readFileSync9(gemfilePath, "utf-8");
1844
+ content = readFileSync6(gemfilePath, "utf-8");
1543
1845
  } catch {
1544
1846
  return [];
1545
1847
  }
1546
- const manifestEv = manifestEvidence9(gemfilePath);
1848
+ const manifestEv = manifestEvidence(gemfilePath);
1547
1849
  const gems = parseGemfile(content);
1548
1850
  const seen = /* @__PURE__ */ new Set();
1549
1851
  const lockfilePath = workspace.lockfiles.find((l) => l.includes("Gemfile.lock"));
@@ -1551,9 +1853,9 @@ var RubyAdapter = class {
1551
1853
  let lockEv;
1552
1854
  if (lockfilePath) {
1553
1855
  try {
1554
- const lockContent = readFileSync9(lockfilePath, "utf-8");
1856
+ const lockContent = readFileSync6(lockfilePath, "utf-8");
1555
1857
  lockVersions = parseGemfileLock(lockContent);
1556
- lockEv = lockfileEvidence8(lockfilePath);
1858
+ lockEv = lockfileEvidence(lockfilePath);
1557
1859
  } catch {
1558
1860
  }
1559
1861
  }
@@ -1562,21 +1864,21 @@ var RubyAdapter = class {
1562
1864
  seen.add(gem.name);
1563
1865
  const locked = lockVersions.get(gem.name);
1564
1866
  const version = locked ?? gem.version;
1565
- const purl = version ? buildPurl({ type: "gem", name: gem.name, version }) : buildPurl({ type: "gem", name: gem.name });
1867
+ const purl = gem.sourceType === "registry" && version ? buildPurl({ type: "gem", name: gem.name, version }) : gem.sourceType === "registry" ? buildPurl({ type: "gem", name: gem.name }) : void 0;
1566
1868
  const evidence = [manifestEv];
1567
1869
  if (lockEv && locked) evidence.push(lockEv);
1568
1870
  observations.push({
1569
1871
  id: `dep-${workspace.id}-${gem.name}`,
1570
1872
  workspaceId: workspace.id,
1571
- purl,
1873
+ ...purl ? { purl } : {},
1572
1874
  ecosystem: "ruby",
1573
1875
  name: gem.name,
1574
- sourceType: "registry",
1876
+ sourceType: gem.sourceType,
1575
1877
  direct: true,
1576
1878
  scope: "runtime",
1577
1879
  ...gem.version ? { requested: gem.version } : {},
1578
1880
  ...locked ? { locked } : {},
1579
- status: "current",
1881
+ status: gem.sourceType === "git" ? "git_dependency" : gem.sourceType === "path" ? "local_path" : "current",
1580
1882
  evidence
1581
1883
  });
1582
1884
  }
@@ -1585,26 +1887,102 @@ var RubyAdapter = class {
1585
1887
  };
1586
1888
  var rubyAdapter = new RubyAdapter();
1587
1889
 
1588
- // src/adapters/elixir.ts
1589
- import { readFileSync as readFileSync10 } from "node:fs";
1590
- function manifestEvidence10(path) {
1591
- return { kind: "manifest", source: path, retrievedAt: (/* @__PURE__ */ new Date()).toISOString() };
1890
+ // src/adapters/swift.ts
1891
+ import { readFileSync as readFileSync7 } from "node:fs";
1892
+ import { basename, join as join7 } from "node:path";
1893
+ function identityFromLocation(location) {
1894
+ return basename(location.replace(/[\\/]$/, "")).replace(/\.git$/i, "").toLowerCase();
1592
1895
  }
1593
- function lockfileEvidence9(path) {
1594
- return { kind: "lockfile", source: path, retrievedAt: (/* @__PURE__ */ new Date()).toISOString() };
1896
+ function parsePackageSwift(content) {
1897
+ const deps = [];
1898
+ const packageRegex = /\.package\s*\(\s*(?:name:\s*["'][^"']+["'],\s*)?(url|path):\s*["']([^"']+)["']\s*(?:,\s*(?:from|exact|branch|revision):\s*["']([^"']+)["'])?\s*\)/g;
1899
+ let match;
1900
+ while ((match = packageRegex.exec(content)) !== null) {
1901
+ const location = match[2];
1902
+ if (!location) continue;
1903
+ deps.push({
1904
+ identity: identityFromLocation(location),
1905
+ requested: match[3],
1906
+ sourceType: match[1] === "path" ? "path" : "git"
1907
+ });
1908
+ }
1909
+ return deps;
1595
1910
  }
1911
+ function parsePackageResolved(content) {
1912
+ const pins = /* @__PURE__ */ new Map();
1913
+ try {
1914
+ const json = JSON.parse(content);
1915
+ for (const pin of json.pins ?? json.object?.pins ?? []) {
1916
+ const identity = (pin.identity ?? pin.package ?? (pin.location ? identityFromLocation(pin.location) : void 0) ?? (pin.repositoryURL ? identityFromLocation(pin.repositoryURL) : void 0))?.toLowerCase();
1917
+ if (identity) pins.set(identity, { ...pin.state?.version ? { version: pin.state.version } : {}, ...pin.state?.revision ? { revision: pin.state.revision } : {} });
1918
+ }
1919
+ } catch {
1920
+ }
1921
+ return pins;
1922
+ }
1923
+ var SwiftAdapter = class {
1924
+ ecosystem = "swift";
1925
+ async inventory(workspace, options) {
1926
+ const root = workspaceRoot(workspace, options);
1927
+ const manifestPath = workspace.manifests.find((path) => path.endsWith("Package.swift")) ?? join7(root, "Package.swift");
1928
+ if (!fileExists(manifestPath)) return [];
1929
+ const direct = parsePackageSwift(readFileSync7(manifestPath, "utf8"));
1930
+ const resolvedPath = workspace.lockfiles.find((path) => path.endsWith("Package.resolved")) ?? join7(root, "Package.resolved");
1931
+ const pins = fileExists(resolvedPath) ? parsePackageResolved(readFileSync7(resolvedPath, "utf8")) : /* @__PURE__ */ new Map();
1932
+ const manifestEv = manifestEvidence(manifestPath);
1933
+ const lockEv = pins.size > 0 ? lockfileEvidence(resolvedPath) : void 0;
1934
+ const observations = [];
1935
+ const seen = /* @__PURE__ */ new Set();
1936
+ const add = (identity, requested, sourceType, isDirect) => {
1937
+ if (seen.has(identity)) return;
1938
+ seen.add(identity);
1939
+ const pin = pins.get(identity);
1940
+ const version = pin?.version ?? pin?.revision ?? requested;
1941
+ const evidence = isDirect ? [manifestEv] : [];
1942
+ if (lockEv && pin) evidence.push(lockEv);
1943
+ observations.push({
1944
+ id: `dep-${workspace.id}-${identity}`,
1945
+ workspaceId: workspace.id,
1946
+ ...sourceType === "git" ? { purl: buildPurl({ type: "swift", name: identity, ...version ? { version } : {} }) } : {},
1947
+ ecosystem: "swift",
1948
+ name: identity,
1949
+ sourceType,
1950
+ direct: isDirect,
1951
+ scope: isDirect ? "runtime" : "transitive",
1952
+ ...requested ? { requested } : {},
1953
+ ...pin?.version ? { locked: pin.version } : pin?.revision ? { locked: pin.revision } : {},
1954
+ status: sourceType === "path" ? "local_path" : "git_dependency",
1955
+ evidence
1956
+ });
1957
+ };
1958
+ for (const dep of direct) add(dep.identity, dep.requested, dep.sourceType, true);
1959
+ if (options.includeTransitive) {
1960
+ for (const [identity] of pins) add(identity, void 0, "git", false);
1961
+ }
1962
+ return observations;
1963
+ }
1964
+ };
1965
+ var swiftAdapter = new SwiftAdapter();
1966
+
1967
+ // src/adapters/elixir.ts
1968
+ import { readFileSync as readFileSync8 } from "node:fs";
1596
1969
  function parseMixExsDeps(content) {
1597
1970
  const deps = [];
1598
- const depRegex = /\{:(\w+),\s*["']([^"']+)["']\}/g;
1971
+ const depRegex = /\{:(\w+),\s*([^}]+)\}/g;
1599
1972
  let match;
1600
1973
  while ((match = depRegex.exec(content)) !== null) {
1601
- deps.push({ name: match[1], version: match[2] });
1974
+ const name = match[1];
1975
+ const value = match[2];
1976
+ if (!name || !value) continue;
1977
+ const version = /^\s*["']([^"']+)["']/.exec(value)?.[1];
1978
+ const sourceType = /\bgit:/.test(value) ? "git" : /\bpath:/.test(value) ? "path" : "registry";
1979
+ deps.push({ name, version, sourceType });
1602
1980
  }
1603
1981
  return deps;
1604
1982
  }
1605
1983
  function parseMixLock(content) {
1606
1984
  const versions = /* @__PURE__ */ new Map();
1607
- const lockRegex = /\{:"(\w+)",\s*hex: "[^"]*",\s*"([^"]+)"/g;
1985
+ const lockRegex = /["']([\w-]+)["']\s*=>\s*\{:hex,\s*:[\w-]+,\s*["']([^"']+)["']/g;
1608
1986
  let match;
1609
1987
  while ((match = lockRegex.exec(content)) !== null) {
1610
1988
  versions.set(match[1], match[2]);
@@ -1619,11 +1997,11 @@ var ElixirAdapter = class {
1619
1997
  if (!mixExsPath) return [];
1620
1998
  let content;
1621
1999
  try {
1622
- content = readFileSync10(mixExsPath, "utf-8");
2000
+ content = readFileSync8(mixExsPath, "utf-8");
1623
2001
  } catch {
1624
2002
  return [];
1625
2003
  }
1626
- const manifestEv = manifestEvidence10(mixExsPath);
2004
+ const manifestEv = manifestEvidence(mixExsPath);
1627
2005
  const deps = parseMixExsDeps(content);
1628
2006
  const seen = /* @__PURE__ */ new Set();
1629
2007
  const lockfilePath = workspace.lockfiles.find((l) => l.includes("mix.lock"));
@@ -1631,9 +2009,9 @@ var ElixirAdapter = class {
1631
2009
  let lockEv;
1632
2010
  if (lockfilePath) {
1633
2011
  try {
1634
- const lockContent = readFileSync10(lockfilePath, "utf-8");
2012
+ const lockContent = readFileSync8(lockfilePath, "utf-8");
1635
2013
  lockVersions = parseMixLock(lockContent);
1636
- lockEv = lockfileEvidence9(lockfilePath);
2014
+ lockEv = lockfileEvidence(lockfilePath);
1637
2015
  } catch {
1638
2016
  }
1639
2017
  }
@@ -1642,21 +2020,21 @@ var ElixirAdapter = class {
1642
2020
  seen.add(dep.name);
1643
2021
  const locked = lockVersions.get(dep.name);
1644
2022
  const version = locked ?? dep.version;
1645
- const purl = version ? buildPurl({ type: "hex", name: dep.name, version }) : buildPurl({ type: "hex", name: dep.name });
2023
+ const purl = dep.sourceType === "registry" && version ? buildPurl({ type: "hex", name: dep.name, version }) : dep.sourceType === "registry" ? buildPurl({ type: "hex", name: dep.name }) : void 0;
1646
2024
  const evidence = [manifestEv];
1647
2025
  if (lockEv && locked) evidence.push(lockEv);
1648
2026
  observations.push({
1649
2027
  id: `dep-${workspace.id}-${dep.name}`,
1650
2028
  workspaceId: workspace.id,
1651
- purl,
2029
+ ...purl ? { purl } : {},
1652
2030
  ecosystem: "elixir",
1653
2031
  name: dep.name,
1654
- sourceType: "registry",
2032
+ sourceType: dep.sourceType,
1655
2033
  direct: true,
1656
2034
  scope: "runtime",
1657
2035
  ...dep.version ? { requested: dep.version } : {},
1658
2036
  ...locked ? { locked } : {},
1659
- status: "current",
2037
+ status: dep.sourceType === "git" ? "git_dependency" : dep.sourceType === "path" ? "local_path" : "current",
1660
2038
  evidence
1661
2039
  });
1662
2040
  }
@@ -1666,10 +2044,7 @@ var ElixirAdapter = class {
1666
2044
  var elixirAdapter = new ElixirAdapter();
1667
2045
 
1668
2046
  // src/adapters/cpp.ts
1669
- import { readFileSync as readFileSync11 } from "node:fs";
1670
- function manifestEvidence11(path) {
1671
- return { kind: "manifest", source: path, retrievedAt: (/* @__PURE__ */ new Date()).toISOString() };
1672
- }
2047
+ import { readFileSync as readFileSync9 } from "node:fs";
1673
2048
  function parseConanTxt(content) {
1674
2049
  const deps = [];
1675
2050
  const requiresMatch = /\[requires\]\s*\n([\s\S]*?)(?:\[|$)/;
@@ -1710,11 +2085,11 @@ var CppAdapter = class {
1710
2085
  for (const manifestPath of workspace.manifests) {
1711
2086
  let content;
1712
2087
  try {
1713
- content = readFileSync11(manifestPath, "utf-8");
2088
+ content = readFileSync9(manifestPath, "utf-8");
1714
2089
  } catch {
1715
2090
  continue;
1716
2091
  }
1717
- const manifestEv = manifestEvidence11(manifestPath);
2092
+ const manifestEv = manifestEvidence(manifestPath);
1718
2093
  let deps = [];
1719
2094
  if (manifestPath.includes("conanfile")) {
1720
2095
  deps = parseConanTxt(content);
@@ -1790,6 +2165,108 @@ function diffSnapshots(oldSnapshot, newSnapshot) {
1790
2165
  return { added, removed, changed };
1791
2166
  }
1792
2167
 
2168
+ // src/trend.ts
2169
+ function median(values) {
2170
+ if (!values.length) return void 0;
2171
+ const sorted = [...values].sort((a, b) => a - b);
2172
+ const middle = Math.floor(sorted.length / 2);
2173
+ const upper = sorted[middle];
2174
+ if (upper === void 0) return void 0;
2175
+ if (sorted.length % 2) return upper;
2176
+ const lower = sorted[middle - 1];
2177
+ return lower === void 0 ? upper : (lower + upper) / 2;
2178
+ }
2179
+ var TrendStore = class {
2180
+ constructor(source) {
2181
+ this.source = source;
2182
+ }
2183
+ source;
2184
+ analyze(projectId, limit = 100) {
2185
+ const snapshots = this.source.listSnapshots(projectId, limit).sort((a, b) => Date.parse(a.createdAt) - Date.parse(b.createdAt));
2186
+ const versionHistory = /* @__PURE__ */ new Map();
2187
+ const vulnerabilityDurations = [];
2188
+ const openVulnerabilities = /* @__PURE__ */ new Map();
2189
+ for (const snapshot of snapshots) {
2190
+ const at = Date.parse(snapshot.createdAt);
2191
+ const active = /* @__PURE__ */ new Set();
2192
+ for (const dependency of snapshot.dependencies) {
2193
+ const key = `${dependency.ecosystem}:${dependency.name}`;
2194
+ active.add(key);
2195
+ const vulnerable = dependency.status === "vulnerable";
2196
+ const history = versionHistory.get(key) ?? [];
2197
+ history.push({ at, version: dependency.locked ?? dependency.installed ?? dependency.requested, vulnerable, name: dependency.name, ecosystem: dependency.ecosystem });
2198
+ versionHistory.set(key, history);
2199
+ if (vulnerable && !openVulnerabilities.has(key)) openVulnerabilities.set(key, at);
2200
+ if (!vulnerable && openVulnerabilities.has(key)) {
2201
+ const openedAt = openVulnerabilities.get(key);
2202
+ if (openedAt !== void 0) vulnerabilityDurations.push(Math.max(0, at - openedAt));
2203
+ openVulnerabilities.delete(key);
2204
+ }
2205
+ }
2206
+ for (const key of openVulnerabilities.keys()) {
2207
+ if (!active.has(key)) {
2208
+ const openedAt = openVulnerabilities.get(key);
2209
+ if (openedAt !== void 0) vulnerabilityDurations.push(Math.max(0, at - openedAt));
2210
+ openVulnerabilities.delete(key);
2211
+ }
2212
+ }
2213
+ }
2214
+ const dependencies = [];
2215
+ for (const [key, history] of versionHistory) {
2216
+ const first = history[0];
2217
+ const latest = history.at(-1);
2218
+ if (!first || !latest) continue;
2219
+ let versionChanges = 0;
2220
+ let sameVersionSince = first.at;
2221
+ for (let index = 1; index < history.length; index++) {
2222
+ const current = history[index];
2223
+ const previous = history[index - 1];
2224
+ if (current && previous && current.version !== previous.version) {
2225
+ versionChanges++;
2226
+ sameVersionSince = current.at;
2227
+ }
2228
+ }
2229
+ const spanDays = Math.max(1, (latest.at - first.at) / 864e5);
2230
+ dependencies.push({
2231
+ key,
2232
+ name: latest.name,
2233
+ ecosystem: latest.ecosystem,
2234
+ ...latest.version ? { currentVersion: latest.version } : {},
2235
+ lockedVersionAgeMs: Math.max(0, latest.at - sameVersionSince),
2236
+ versionChanges,
2237
+ upgradeVelocityPerDay: versionChanges / spanDays
2238
+ });
2239
+ }
2240
+ dependencies.sort((a, b) => a.key.localeCompare(b.key));
2241
+ const halfLife = median(vulnerabilityDurations);
2242
+ const firstSnapshot = snapshots[0];
2243
+ const lastSnapshot = snapshots.at(-1);
2244
+ return {
2245
+ projectId,
2246
+ ...firstSnapshot && lastSnapshot ? { from: firstSnapshot.createdAt, to: lastSnapshot.createdAt } : {},
2247
+ snapshots: snapshots.length,
2248
+ dependencies,
2249
+ ...halfLife !== void 0 ? { vulnerabilityHalfLifeMs: halfLife } : {},
2250
+ points: snapshots.map((snapshot) => ({
2251
+ snapshotId: snapshot.id,
2252
+ createdAt: snapshot.createdAt,
2253
+ dependencies: snapshot.dependencies.length,
2254
+ outdated: snapshot.dependencies.filter((dependency) => dependency.status.startsWith("update_available")).length,
2255
+ vulnerable: snapshot.dependencies.filter((dependency) => dependency.status === "vulnerable").length
2256
+ }))
2257
+ };
2258
+ }
2259
+ };
2260
+ function renderTrendMarkdown(report) {
2261
+ const lines = ["# TechStack Trend", "", `**Snapshots:** ${report.snapshots}`];
2262
+ if (report.vulnerabilityHalfLifeMs !== void 0) lines.push(`**Vulnerability half-life:** ${(report.vulnerabilityHalfLifeMs / 864e5).toFixed(1)} days`);
2263
+ lines.push("", "| Dependency | Version | Age (days) | Changes | Velocity/day |", "|---|---|---:|---:|---:|");
2264
+ for (const dependency of report.dependencies) {
2265
+ lines.push(`| ${dependency.ecosystem}:${dependency.name} | ${dependency.currentVersion ?? "\u2014"} | ${(dependency.lockedVersionAgeMs / 864e5).toFixed(1)} | ${dependency.versionChanges} | ${dependency.upgradeVelocityPerDay.toFixed(3)} |`);
2266
+ }
2267
+ return lines.join("\n");
2268
+ }
2269
+
1793
2270
  // src/sbom.ts
1794
2271
  function toSpdx(snapshot) {
1795
2272
  const created = snapshot.createdAt;
@@ -1832,6 +2309,47 @@ function toCycloneDX(snapshot) {
1832
2309
  }
1833
2310
 
1834
2311
  // src/remediation.ts
2312
+ var LANGUAGE_BY_ECOSYSTEM = {
2313
+ python: "python",
2314
+ rust: "rust",
2315
+ go: "go",
2316
+ php: "php",
2317
+ dotnet: "csharp"
2318
+ };
2319
+ var EXECUTABLE_ECOSYSTEMS = /* @__PURE__ */ new Set(["npm", ...Object.keys(LANGUAGE_BY_ECOSYSTEM)]);
2320
+ function versionedPackageName(operation) {
2321
+ const version = operation.targetVersion;
2322
+ if (!version) return operation.dependencyName;
2323
+ switch (operation.ecosystem) {
2324
+ case "python":
2325
+ return `${operation.dependencyName}==${version}`;
2326
+ case "php":
2327
+ return `${operation.dependencyName}:${version}`;
2328
+ default:
2329
+ return `${operation.dependencyName}@${version}`;
2330
+ }
2331
+ }
2332
+ function toLanguagePackageInput(operation, workspace) {
2333
+ if (operation.action === "replace" || operation.action === "investigate") {
2334
+ throw new Error(`${operation.action} requires a manual package choice`);
2335
+ }
2336
+ if (!EXECUTABLE_ECOSYSTEMS.has(operation.ecosystem)) {
2337
+ throw new Error(`Automated remediation is not supported for ${operation.ecosystem}`);
2338
+ }
2339
+ const language = LANGUAGE_BY_ECOSYSTEM[operation.ecosystem];
2340
+ const base = {
2341
+ ...workspace ? { workspace } : {},
2342
+ ...language ? { language } : {},
2343
+ allowScripts: false
2344
+ };
2345
+ if (operation.action === "remove") {
2346
+ if (operation.ecosystem === "go") {
2347
+ return { ...base, operation: "add", names: [`${operation.dependencyName}@none`] };
2348
+ }
2349
+ return { ...base, operation: "remove", names: [operation.dependencyName] };
2350
+ }
2351
+ return { ...base, operation: "add", names: [versionedPackageName(operation)] };
2352
+ }
1835
2353
  function suggestCommand(ecosystem, name, action, targetVersion) {
1836
2354
  const ver = targetVersion ? `@${targetVersion}` : "@latest";
1837
2355
  switch (ecosystem) {
@@ -1951,14 +2469,138 @@ function renderPlanMarkdown(plan) {
1951
2469
  }
1952
2470
  return lines.join("\n");
1953
2471
  }
2472
+ async function applyPlan(plan, options = {}) {
2473
+ const dryRun = options.dryRun !== false;
2474
+ const execute = options.execute;
2475
+ if (!dryRun && !execute) throw new Error("An execute strategy is required when dryRun is false");
2476
+ const results = [];
2477
+ for (const [index, item] of plan.items.entries()) {
2478
+ if (options.signal?.aborted) throw new DOMException("Remediation cancelled", "AbortError");
2479
+ if (dryRun) {
2480
+ results.push({ dependencyName: item.dependencyName, status: "planned" });
2481
+ continue;
2482
+ }
2483
+ const approved = options.approve ? await options.approve(item, index) : false;
2484
+ if (!approved) {
2485
+ results.push({
2486
+ dependencyName: item.dependencyName,
2487
+ status: "skipped",
2488
+ detail: "Not approved"
2489
+ });
2490
+ continue;
2491
+ }
2492
+ try {
2493
+ if (!execute) throw new Error("An execute strategy is required when dryRun is false");
2494
+ const response = await execute({
2495
+ ecosystem: item.ecosystem,
2496
+ workspaceId: item.workspaceId,
2497
+ dependencyName: item.dependencyName,
2498
+ action: item.action,
2499
+ ...item.targetVersion ? { targetVersion: item.targetVersion } : {}
2500
+ });
2501
+ results.push({
2502
+ dependencyName: item.dependencyName,
2503
+ status: "applied",
2504
+ ...response?.detail ? { detail: response.detail } : {}
2505
+ });
2506
+ } catch (error) {
2507
+ results.push({
2508
+ dependencyName: item.dependencyName,
2509
+ status: "failed",
2510
+ detail: error instanceof Error ? error.message : String(error)
2511
+ });
2512
+ }
2513
+ }
2514
+ return { dryRun, items: results };
2515
+ }
2516
+
2517
+ // src/registry/http-fetch.ts
2518
+ import { get as httpGet, request as httpRequest } from "node:http";
2519
+ import { get as httpsGet, request as httpsRequest } from "node:https";
2520
+ function retryAfterMs(headers, now = Date.now()) {
2521
+ const value = Array.isArray(headers["retry-after"]) ? headers["retry-after"][0] : headers["retry-after"];
2522
+ if (!value) return void 0;
2523
+ const seconds = Number(value);
2524
+ if (Number.isFinite(seconds) && seconds >= 0) return seconds * 1e3;
2525
+ const date = Date.parse(value);
2526
+ return Number.isFinite(date) ? Math.max(0, date - now) : void 0;
2527
+ }
2528
+ function delay(ms, signal) {
2529
+ if (signal?.aborted) return Promise.reject(new DOMException("Request aborted", "AbortError"));
2530
+ return new Promise((resolve3, reject) => {
2531
+ const timer = setTimeout(resolve3, ms);
2532
+ signal?.addEventListener("abort", () => {
2533
+ clearTimeout(timer);
2534
+ reject(new DOMException("Request aborted", "AbortError"));
2535
+ }, { once: true });
2536
+ });
2537
+ }
2538
+ function requestOnce(options) {
2539
+ return new Promise((resolve3, reject) => {
2540
+ const method = options.method ?? "GET";
2541
+ const requestOptions = {
2542
+ hostname: options.hostname,
2543
+ path: options.path,
2544
+ method,
2545
+ headers: options.headers,
2546
+ signal: options.signal,
2547
+ timeout: options.timeoutMs ?? 15e3
2548
+ };
2549
+ const isHttp = options.hostname === "localhost" || options.hostname === "127.0.0.1";
2550
+ const callback = (response) => {
2551
+ let body = "";
2552
+ response.setEncoding?.("utf8");
2553
+ response.on("data", (chunk) => {
2554
+ body += chunk.toString();
2555
+ });
2556
+ response.on("end", () => resolve3({
2557
+ statusCode: response.statusCode ?? 0,
2558
+ headers: response.headers,
2559
+ body
2560
+ }));
2561
+ };
2562
+ const request = method === "GET" ? (isHttp ? httpGet : httpsGet)(requestOptions, callback) : (isHttp ? httpRequest : httpsRequest)(requestOptions, callback);
2563
+ request.on("error", reject);
2564
+ request.on("timeout", () => {
2565
+ request.destroy();
2566
+ reject(new Error(`Request timeout for ${options.hostname}${options.path}`));
2567
+ });
2568
+ if (options.body) request.write(options.body);
2569
+ request.end();
2570
+ });
2571
+ }
2572
+ async function requestWithRetry(options) {
2573
+ const maxAttempts = Math.max(1, options.maxAttempts ?? 3);
2574
+ let lastError;
2575
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
2576
+ try {
2577
+ const response = await requestOnce(options);
2578
+ const retryable = response.statusCode === 429 || response.statusCode >= 500;
2579
+ if (!retryable || attempt === maxAttempts - 1) return response;
2580
+ const serverDelay = retryAfterMs(response.headers);
2581
+ const exponential = (options.baseBackoffMs ?? 1e3) * 2 ** attempt;
2582
+ await delay(serverDelay ?? exponential, options.signal);
2583
+ } catch (error) {
2584
+ if (options.signal?.aborted || error instanceof DOMException && error.name === "AbortError") throw error;
2585
+ lastError = error instanceof Error ? error : new Error(String(error));
2586
+ if (attempt < maxAttempts - 1) {
2587
+ await delay((options.baseBackoffMs ?? 1e3) * 2 ** attempt, options.signal);
2588
+ }
2589
+ }
2590
+ }
2591
+ throw lastError ?? new Error(`Request failed for ${options.hostname}${options.path}`);
2592
+ }
2593
+ function parseJsonResponse(response, source) {
2594
+ try {
2595
+ return JSON.parse(response.body);
2596
+ } catch {
2597
+ throw new Error(`Invalid JSON response from ${source}`);
2598
+ }
2599
+ }
1954
2600
 
1955
2601
  // src/registry/client.ts
1956
- import { get as httpsGet } from "node:https";
1957
- import { get as httpGet } from "node:http";
1958
2602
  var DEFAULT_TTL_MS = 10 * 60 * 1e3;
1959
2603
  var MAX_CONCURRENCY_PER_HOST = 3;
1960
- var MAX_RETRIES = 3;
1961
- var BASE_BACKOFF_MS = 1e3;
1962
2604
  var registryCache = /* @__PURE__ */ new Map();
1963
2605
  var hostConcurrency = /* @__PURE__ */ new Map();
1964
2606
  function getCacheKey(host, path) {
@@ -2006,54 +2648,6 @@ function releaseHostSlot(host) {
2006
2648
  }
2007
2649
  }
2008
2650
  }
2009
- function sleep(ms) {
2010
- return new Promise((resolve3) => setTimeout(resolve3, ms));
2011
- }
2012
- function computeBackoff(attempt, statusCode) {
2013
- const base = statusCode === 429 ? BASE_BACKOFF_MS * 2 : BASE_BACKOFF_MS;
2014
- return base * Math.pow(2, attempt) + Math.random() * 500;
2015
- }
2016
- function httpsFetch(hostname, path, etag, signal) {
2017
- return new Promise((resolve3, reject) => {
2018
- const options = {
2019
- hostname,
2020
- path,
2021
- method: "GET",
2022
- headers: {
2023
- Accept: "application/json",
2024
- "User-Agent": "WrongStack-TechStack/1.0",
2025
- ...etag ? { "If-None-Match": etag } : {}
2026
- },
2027
- signal,
2028
- timeout: 15e3
2029
- };
2030
- const mod = hostname === "localhost" || hostname === "127.0.0.1" ? httpGet : httpsGet;
2031
- const req = mod(options, (res) => {
2032
- const statusCode = res.statusCode ?? 0;
2033
- const responseHeaders = res.headers;
2034
- let body = "";
2035
- res.on("data", (chunk) => {
2036
- body += chunk;
2037
- });
2038
- res.on("end", () => {
2039
- resolve3({
2040
- statusCode,
2041
- headers: responseHeaders,
2042
- body,
2043
- isFromCache: false
2044
- });
2045
- });
2046
- });
2047
- req.on("error", (err) => {
2048
- reject(err);
2049
- });
2050
- req.on("timeout", () => {
2051
- req.destroy();
2052
- reject(new Error(`Request timeout for ${hostname}${path}`));
2053
- });
2054
- req.end();
2055
- });
2056
- }
2057
2651
  function parseNpmPackument(json, name) {
2058
2652
  const latestVersion = json["dist-tags"]?.["latest"];
2059
2653
  let deprecated;
@@ -2208,6 +2802,44 @@ var ECOSYSTEM_FETCHERS = {
2208
2802
  }
2209
2803
  }
2210
2804
  };
2805
+ var RegistryNotFoundError = class extends Error {
2806
+ constructor(statusCode, packageName) {
2807
+ super(`Registry package not found or inaccessible (${statusCode}): ${packageName}`);
2808
+ this.statusCode = statusCode;
2809
+ this.packageName = packageName;
2810
+ this.name = "RegistryNotFoundError";
2811
+ }
2812
+ statusCode;
2813
+ packageName;
2814
+ };
2815
+ var RegistryAuthError = class extends Error {
2816
+ constructor(statusCode, packageName) {
2817
+ super(`Registry authorization failed (${statusCode}): ${packageName}`);
2818
+ this.statusCode = statusCode;
2819
+ this.packageName = packageName;
2820
+ this.name = "RegistryAuthError";
2821
+ }
2822
+ statusCode;
2823
+ packageName;
2824
+ };
2825
+ var RegistryRateLimitError = class extends Error {
2826
+ constructor(statusCode, host) {
2827
+ super(`Registry ${host} returned ${statusCode} after 3 attempts`);
2828
+ this.statusCode = statusCode;
2829
+ this.host = host;
2830
+ this.name = "RegistryRateLimitError";
2831
+ }
2832
+ statusCode;
2833
+ host;
2834
+ };
2835
+ var RegistryNetworkError = class extends Error {
2836
+ constructor(message, cause) {
2837
+ super(message);
2838
+ this.cause = cause;
2839
+ this.name = "RegistryNetworkError";
2840
+ }
2841
+ cause;
2842
+ };
2211
2843
  async function lookupRegistry(ecosystem, name, options = {}) {
2212
2844
  const fetcher = ECOSYSTEM_FETCHERS[ecosystem];
2213
2845
  if (!fetcher) {
@@ -2223,48 +2855,52 @@ async function lookupRegistry(ecosystem, name, options = {}) {
2223
2855
  try {
2224
2856
  const existingEntry = registryCache.get(cacheKey);
2225
2857
  const etag = existingEntry?.etag;
2226
- let lastError;
2227
- for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
2228
- try {
2229
- const response = await httpsFetch(fetcher.host, path, etag, options.signal);
2230
- if (response.statusCode === 304 && existingEntry) {
2231
- setCache(cacheKey, existingEntry.data, existingEntry.etag, DEFAULT_TTL_MS);
2232
- return existingEntry.data;
2233
- }
2234
- if (response.statusCode === 401 || response.statusCode === 403 || response.statusCode === 404) {
2235
- return void 0;
2236
- }
2237
- if (response.statusCode === 429 || response.statusCode >= 500) {
2238
- if (attempt < MAX_RETRIES - 1) {
2239
- const backoff = computeBackoff(attempt, response.statusCode);
2240
- await sleep(backoff);
2241
- continue;
2242
- }
2243
- throw new Error(`Registry ${fetcher.host} returned ${response.statusCode} after ${MAX_RETRIES} attempts`);
2244
- }
2245
- let json;
2246
- try {
2247
- json = JSON.parse(response.body);
2248
- } catch {
2249
- throw new Error(`Invalid JSON response from ${fetcher.host}${path}`);
2250
- }
2251
- const parsed = fetcher.parser(json, name, ecosystem);
2252
- if (!parsed) {
2253
- return void 0;
2254
- }
2255
- const responseEtag = response.headers["etag"];
2256
- setCache(cacheKey, parsed, responseEtag, DEFAULT_TTL_MS);
2257
- return parsed;
2258
- } catch (err) {
2259
- lastError = err instanceof Error ? err : new Error(String(err));
2260
- if (attempt < MAX_RETRIES - 1) {
2261
- const isRateLimit = err instanceof Error && err.message.includes("429");
2262
- const backoff = computeBackoff(attempt, isRateLimit ? 429 : 500);
2263
- await sleep(backoff);
2264
- }
2265
- }
2858
+ let response;
2859
+ try {
2860
+ response = await requestWithRetry({
2861
+ hostname: fetcher.host,
2862
+ path,
2863
+ headers: {
2864
+ Accept: "application/json",
2865
+ "User-Agent": "WrongStack-TechStack/1.0",
2866
+ ...etag ? { "If-None-Match": etag } : {}
2867
+ },
2868
+ signal: options.signal,
2869
+ timeoutMs: 15e3,
2870
+ maxAttempts: 3
2871
+ });
2872
+ } catch (error) {
2873
+ if (options.signal?.aborted) throw error;
2874
+ const cause = error instanceof Error ? error : new Error(String(error));
2875
+ throw new RegistryNetworkError(cause.message, cause);
2876
+ }
2877
+ if (response.statusCode === 304 && existingEntry) {
2878
+ setCache(cacheKey, existingEntry.data, existingEntry.etag, DEFAULT_TTL_MS);
2879
+ return existingEntry.data;
2880
+ }
2881
+ if (response.statusCode === 401 || response.statusCode === 404) {
2882
+ if (options.strictErrors) throw new RegistryNotFoundError(response.statusCode, name);
2883
+ return void 0;
2884
+ }
2885
+ if (response.statusCode === 403) {
2886
+ if (options.strictErrors) throw new RegistryAuthError(response.statusCode, name);
2887
+ return void 0;
2888
+ }
2889
+ if (response.statusCode === 429) {
2890
+ throw new RegistryRateLimitError(response.statusCode, fetcher.host);
2891
+ }
2892
+ if (response.statusCode >= 500) {
2893
+ throw new RegistryNetworkError(`Registry ${fetcher.host} returned ${response.statusCode} after 3 attempts`);
2894
+ }
2895
+ if (response.statusCode < 200 || response.statusCode >= 300) {
2896
+ throw new RegistryNetworkError(`Unexpected registry response ${response.statusCode} from ${fetcher.host}`);
2266
2897
  }
2267
- throw lastError ?? new Error(`Failed to look up ${ecosystem}:${name}`);
2898
+ const json = parseJsonResponse(response, `${fetcher.host}${path}`);
2899
+ const parsed = fetcher.parser(json, name, ecosystem);
2900
+ if (!parsed) return void 0;
2901
+ const responseEtag = Array.isArray(response.headers.etag) ? response.headers.etag[0] : response.headers.etag;
2902
+ setCache(cacheKey, parsed, responseEtag, DEFAULT_TTL_MS);
2903
+ return parsed;
2268
2904
  } finally {
2269
2905
  releaseHostSlot(fetcher.host);
2270
2906
  }
@@ -2293,14 +2929,21 @@ function clearRegistryCache() {
2293
2929
  registryCache.clear();
2294
2930
  hostConcurrency.clear();
2295
2931
  }
2932
+ function invalidateRegistryCache(ecosystem, names) {
2933
+ const fetcher = ECOSYSTEM_FETCHERS[ecosystem];
2934
+ if (!fetcher) return 0;
2935
+ const keys = names ? names.map((name) => getCacheKey(fetcher.host, fetcher.path(name))) : [...registryCache.keys()].filter((key) => key.startsWith(fetcher.host));
2936
+ let removed = 0;
2937
+ for (const key of keys) {
2938
+ if (registryCache.delete(key)) removed++;
2939
+ }
2940
+ return removed;
2941
+ }
2296
2942
 
2297
2943
  // src/advisory/osv.ts
2298
- import { get as httpsGet2 } from "node:https";
2299
2944
  var OSV_API_BASE = "api.osv.dev";
2300
2945
  var OSV_QUERY_BATCH_PATH = "/v1/querybatch";
2301
2946
  var MAX_BATCH_SIZE = 500;
2302
- var MAX_RETRIES2 = 3;
2303
- var BASE_BACKOFF_MS2 = 1e3;
2304
2947
  function mapSeverity(osvSeverity, databaseSeverity) {
2305
2948
  if (osvSeverity && osvSeverity.length > 0) {
2306
2949
  for (const s of osvSeverity) {
@@ -2322,44 +2965,6 @@ function mapSeverity(osvSeverity, databaseSeverity) {
2322
2965
  }
2323
2966
  return "info";
2324
2967
  }
2325
- function osvPostRequest(body, signal) {
2326
- return new Promise((resolve3, reject) => {
2327
- const options = {
2328
- hostname: OSV_API_BASE,
2329
- path: OSV_QUERY_BATCH_PATH,
2330
- method: "POST",
2331
- headers: {
2332
- "Content-Type": "application/json",
2333
- "Content-Length": Buffer.byteLength(body).toString(),
2334
- "User-Agent": "WrongStack-TechStack/1.0"
2335
- },
2336
- signal,
2337
- timeout: 3e4
2338
- };
2339
- const req = httpsGet2(options, (res) => {
2340
- const statusCode = res.statusCode ?? 0;
2341
- let responseBody = "";
2342
- res.on("data", (chunk) => {
2343
- responseBody += chunk;
2344
- });
2345
- res.on("end", () => {
2346
- resolve3({ statusCode, body: responseBody });
2347
- });
2348
- });
2349
- req.on("error", (err) => {
2350
- reject(err);
2351
- });
2352
- req.on("timeout", () => {
2353
- req.destroy();
2354
- reject(new Error("OSV API request timeout"));
2355
- });
2356
- req.write(body);
2357
- req.end();
2358
- });
2359
- }
2360
- function sleep2(ms) {
2361
- return new Promise((resolve3) => setTimeout(resolve3, ms));
2362
- }
2363
2968
  async function queryOsvBatch(purls, options = {}) {
2364
2969
  const advisories = /* @__PURE__ */ new Map();
2365
2970
  for (const purl of purls) {
@@ -2369,7 +2974,6 @@ async function queryOsvBatch(purls, options = {}) {
2369
2974
  for (let i = 0; i < purls.length; i += MAX_BATCH_SIZE) {
2370
2975
  batches.push(purls.slice(i, i + MAX_BATCH_SIZE));
2371
2976
  }
2372
- let lastError;
2373
2977
  for (const batch of batches) {
2374
2978
  const requestBody = {
2375
2979
  queries: batch.map((purl) => ({
@@ -2377,54 +2981,38 @@ async function queryOsvBatch(purls, options = {}) {
2377
2981
  }))
2378
2982
  };
2379
2983
  const jsonBody = JSON.stringify(requestBody);
2380
- let success = false;
2381
- for (let attempt = 0; attempt < MAX_RETRIES2 && !success; attempt++) {
2382
- try {
2383
- const response = await osvPostRequest(jsonBody, options.signal);
2384
- if (response.statusCode === 200) {
2385
- const result = JSON.parse(response.body);
2386
- if (result.results && Array.isArray(result.results)) {
2387
- for (let i = 0; i < result.results.length; i++) {
2388
- const purl = batch[i];
2389
- if (!purl) continue;
2390
- const vulns = result.results[i]?.vulns;
2391
- if (!vulns || vulns.length === 0) continue;
2392
- const parsed = [];
2393
- for (const vuln of vulns) {
2394
- const dbSpecific = vuln.database_specific;
2395
- const affectedDbSpecific = vuln.affected?.[0]?.database_specific;
2396
- const severitySource = dbSpecific?.severity ?? affectedDbSpecific?.severity;
2397
- parsed.push({
2398
- id: vuln.id,
2399
- summary: vuln.summary ?? vuln.details ?? "No summary available",
2400
- severity: mapSeverity(vuln.severity, severitySource),
2401
- aliases: vuln.aliases ?? []
2402
- });
2403
- }
2404
- advisories.set(purl, parsed);
2405
- }
2406
- }
2407
- success = true;
2408
- } else if (response.statusCode === 429 || response.statusCode >= 500) {
2409
- if (attempt < MAX_RETRIES2 - 1) {
2410
- const backoff = BASE_BACKOFF_MS2 * Math.pow(2, attempt) + Math.random() * 500;
2411
- await sleep2(backoff);
2412
- } else {
2413
- throw new Error(`OSV API returned ${response.statusCode} after ${MAX_RETRIES2} attempts: ${response.body}`);
2414
- }
2415
- } else {
2416
- throw new Error(`OSV API returned ${response.statusCode}: ${response.body}`);
2417
- }
2418
- } catch (err) {
2419
- lastError = err instanceof Error ? err : new Error(String(err));
2420
- if (attempt < MAX_RETRIES2 - 1) {
2421
- const backoff = BASE_BACKOFF_MS2 * Math.pow(2, attempt) + Math.random() * 500;
2422
- await sleep2(backoff);
2423
- }
2424
- }
2425
- }
2426
- if (!success && lastError) {
2427
- throw lastError;
2984
+ const response = await requestWithRetry({
2985
+ hostname: OSV_API_BASE,
2986
+ path: OSV_QUERY_BATCH_PATH,
2987
+ method: "POST",
2988
+ headers: {
2989
+ "Content-Type": "application/json",
2990
+ "Content-Length": Buffer.byteLength(jsonBody).toString(),
2991
+ "User-Agent": "WrongStack-TechStack/1.0"
2992
+ },
2993
+ body: jsonBody,
2994
+ signal: options.signal,
2995
+ timeoutMs: 3e4,
2996
+ maxAttempts: 3
2997
+ });
2998
+ if (response.statusCode !== 200) {
2999
+ throw new Error(`OSV API returned ${response.statusCode}: ${response.body}`);
3000
+ }
3001
+ const result = parseJsonResponse(response, "api.osv.dev/v1/querybatch");
3002
+ for (let i = 0; i < result.results.length; i++) {
3003
+ const purl = batch[i];
3004
+ if (!purl) continue;
3005
+ const vulns = result.results[i]?.vulns;
3006
+ if (!vulns || vulns.length === 0) continue;
3007
+ advisories.set(purl, vulns.map((vuln) => ({
3008
+ id: vuln.id,
3009
+ summary: vuln.summary ?? vuln.details ?? "No summary available",
3010
+ severity: mapSeverity(
3011
+ vuln.severity,
3012
+ vuln.database_specific?.severity ?? vuln.affected?.[0]?.database_specific?.severity
3013
+ ),
3014
+ aliases: vuln.aliases ?? []
3015
+ })));
2428
3016
  }
2429
3017
  }
2430
3018
  const evidence = {
@@ -2441,9 +3029,9 @@ async function queryOsvSingle(purl, options = {}) {
2441
3029
  }
2442
3030
 
2443
3031
  // src/advisory/native-audit.ts
2444
- import { spawnSync } from "node:child_process";
2445
- import { existsSync as existsSync2 } from "node:fs";
2446
- import { join as join6 } from "node:path";
3032
+ import { execFile } from "node:child_process";
3033
+ import { access as access3 } from "node:fs/promises";
3034
+ import { join as join8 } from "node:path";
2447
3035
  function npmSeverity(s) {
2448
3036
  switch (s.toLowerCase()) {
2449
3037
  case "critical":
@@ -2473,8 +3061,8 @@ function cargoSeverity(s) {
2473
3061
  return "info";
2474
3062
  }
2475
3063
  }
2476
- function runNpmAudit(workspaceRoot2) {
2477
- const result = runAuditCommand("npm", ["audit", "--json"], workspaceRoot2);
3064
+ async function runNpmAudit(workspaceRoot2, runner = defaultAuditCommandRunner) {
3065
+ const result = await runner("npm", ["audit", "--json"], workspaceRoot2);
2478
3066
  const advisories = [];
2479
3067
  let detailLines = [];
2480
3068
  if (result.status === 0 || result.status === 1) {
@@ -2523,20 +3111,22 @@ function runNpmAudit(workspaceRoot2) {
2523
3111
  };
2524
3112
  return { advisories, evidence };
2525
3113
  }
2526
- function runPipAudit(workspaceRoot2) {
3114
+ async function runPipAudit(workspaceRoot2, runner = defaultAuditCommandRunner) {
2527
3115
  const reqFiles = ["requirements.txt", "requirements-dev.txt"];
2528
3116
  let reqFlag = "";
2529
3117
  for (const f of reqFiles) {
2530
- if (existsSync2(join6(workspaceRoot2, f))) {
3118
+ try {
3119
+ await access3(join8(workspaceRoot2, f));
2531
3120
  reqFlag = `--requirement ${f}`;
2532
3121
  break;
3122
+ } catch {
2533
3123
  }
2534
3124
  }
2535
3125
  const args = ["audit", "--format", "json"];
2536
3126
  if (reqFlag) {
2537
3127
  args.push(...reqFlag.split(" "));
2538
3128
  }
2539
- const result = runAuditCommand("pip-audit" in process.env ? "pip-audit" : "pip-audit", args, workspaceRoot2);
3129
+ const result = await runner("pip-audit", args, workspaceRoot2);
2540
3130
  return parsePipAuditOutput(result);
2541
3131
  }
2542
3132
  function parsePipAuditOutput(result) {
@@ -2570,8 +3160,8 @@ function parsePipAuditOutput(result) {
2570
3160
  };
2571
3161
  return { advisories, evidence };
2572
3162
  }
2573
- function runCargoAudit(workspaceRoot2) {
2574
- const result = runAuditCommand("cargo", ["audit", "--json"], workspaceRoot2);
3163
+ async function runCargoAudit(workspaceRoot2, runner = defaultAuditCommandRunner) {
3164
+ const result = await runner("cargo", ["audit", "--json"], workspaceRoot2);
2575
3165
  return parseCargoAuditOutput(result);
2576
3166
  }
2577
3167
  function parseCargoAuditOutput(result) {
@@ -2612,8 +3202,8 @@ function parseCargoAuditOutput(result) {
2612
3202
  };
2613
3203
  return { advisories, evidence };
2614
3204
  }
2615
- function runGoVulncheck(workspaceRoot2) {
2616
- const result = runAuditCommand("govulncheck", ["-json"], workspaceRoot2);
3205
+ async function runGoVulncheck(workspaceRoot2, runner = defaultAuditCommandRunner) {
3206
+ const result = await runner("govulncheck", ["-json"], workspaceRoot2);
2617
3207
  const advisories = [];
2618
3208
  let detailLines = [];
2619
3209
  if (result.status === 0 || result.status === 3) {
@@ -2651,8 +3241,8 @@ function runGoVulncheck(workspaceRoot2) {
2651
3241
  };
2652
3242
  return { advisories, evidence };
2653
3243
  }
2654
- function runComposerAudit(workspaceRoot2) {
2655
- const result = runAuditCommand("composer", ["audit", "--format=json"], workspaceRoot2);
3244
+ async function runComposerAudit(workspaceRoot2, runner = defaultAuditCommandRunner) {
3245
+ const result = await runner("composer", ["audit", "--format=json"], workspaceRoot2);
2656
3246
  const advisories = [];
2657
3247
  let detailLines = [];
2658
3248
  if (result.status === 0) {
@@ -2688,8 +3278,8 @@ function runComposerAudit(workspaceRoot2) {
2688
3278
  };
2689
3279
  return { advisories, evidence };
2690
3280
  }
2691
- function runDotnetAudit(workspaceRoot2) {
2692
- const result = runAuditCommand("dotnet", ["package", "audit", "--format", "json"], workspaceRoot2);
3281
+ async function runDotnetAudit(workspaceRoot2, runner = defaultAuditCommandRunner) {
3282
+ const result = await runner("dotnet", ["package", "audit", "--format", "json"], workspaceRoot2);
2693
3283
  const advisories = [];
2694
3284
  let detailLines = [];
2695
3285
  if (result.status === 0) {
@@ -2739,44 +3329,42 @@ function runDotnetAudit(workspaceRoot2) {
2739
3329
  };
2740
3330
  return { advisories, evidence };
2741
3331
  }
2742
- function runAuditCommand(command, args, cwd) {
2743
- try {
2744
- const options = {
3332
+ var defaultAuditCommandRunner = (command, args, cwd) => new Promise((resolve3) => {
3333
+ execFile(
3334
+ command,
3335
+ [...args],
3336
+ {
2745
3337
  cwd,
2746
- encoding: "utf-8",
3338
+ encoding: "utf8",
2747
3339
  timeout: 6e4,
2748
3340
  maxBuffer: 10 * 1024 * 1024,
2749
- // 10MB
2750
3341
  windowsHide: true
2751
- };
2752
- const result = spawnSync(command, args, options);
2753
- return {
2754
- status: result.status,
2755
- stdout: result.stdout?.toString() ?? "",
2756
- stderr: result.stderr?.toString() ?? ""
2757
- };
2758
- } catch (err) {
2759
- return {
2760
- status: null,
2761
- stdout: "",
2762
- stderr: err instanceof Error ? err.message : String(err)
2763
- };
2764
- }
2765
- }
2766
- function runNativeAudit(ecosystem, workspaceRoot2) {
3342
+ },
3343
+ (error, stdout, stderr) => {
3344
+ const exitCode = error?.code;
3345
+ const status = error ? typeof exitCode === "number" ? exitCode : null : 0;
3346
+ resolve3({
3347
+ status,
3348
+ stdout,
3349
+ stderr: stderr || (status === null && error ? error.message : "")
3350
+ });
3351
+ }
3352
+ );
3353
+ });
3354
+ async function runNativeAudit(ecosystem, workspaceRoot2, runner = defaultAuditCommandRunner) {
2767
3355
  switch (ecosystem) {
2768
3356
  case "npm":
2769
- return runNpmAudit(workspaceRoot2);
3357
+ return runNpmAudit(workspaceRoot2, runner);
2770
3358
  case "python":
2771
- return runPipAudit(workspaceRoot2);
3359
+ return runPipAudit(workspaceRoot2, runner);
2772
3360
  case "rust":
2773
- return runCargoAudit(workspaceRoot2);
3361
+ return runCargoAudit(workspaceRoot2, runner);
2774
3362
  case "go":
2775
- return runGoVulncheck(workspaceRoot2);
3363
+ return runGoVulncheck(workspaceRoot2, runner);
2776
3364
  case "php":
2777
- return runComposerAudit(workspaceRoot2);
3365
+ return runComposerAudit(workspaceRoot2, runner);
2778
3366
  case "dotnet":
2779
- return runDotnetAudit(workspaceRoot2);
3367
+ return runDotnetAudit(workspaceRoot2, runner);
2780
3368
  // dart/pub doesn't have a standard audit command — use OSV instead
2781
3369
  default:
2782
3370
  return {
@@ -2790,14 +3378,41 @@ function runNativeAudit(ecosystem, workspaceRoot2) {
2790
3378
  };
2791
3379
  }
2792
3380
  }
2793
- function isNativeAuditAvailable(ecosystem) {
2794
- const result = runAuditCommand(
3381
+ async function isNativeAuditAvailable(ecosystem, runner = defaultAuditCommandRunner) {
3382
+ const result = await runner(
2795
3383
  ecosystem === "npm" ? "npm" : ecosystem === "python" ? "pip-audit" : ecosystem === "rust" ? "cargo" : ecosystem === "go" ? "govulncheck" : ecosystem === "php" ? "composer" : ecosystem === "dotnet" ? "dotnet" : "",
2796
3384
  ["--version"],
2797
3385
  process.cwd()
2798
3386
  );
2799
3387
  return result.status === 0;
2800
3388
  }
3389
+ function createAuditRunner(commandRunner = defaultAuditCommandRunner) {
3390
+ const commandFor = (ecosystem) => {
3391
+ switch (ecosystem) {
3392
+ case "npm":
3393
+ return "npm";
3394
+ case "python":
3395
+ return "pip-audit";
3396
+ case "rust":
3397
+ return "cargo";
3398
+ case "go":
3399
+ return "govulncheck";
3400
+ case "php":
3401
+ return "composer";
3402
+ case "dotnet":
3403
+ return "dotnet";
3404
+ default:
3405
+ return void 0;
3406
+ }
3407
+ };
3408
+ return {
3409
+ run: (ecosystem, workspaceRoot2) => runNativeAudit(ecosystem, workspaceRoot2, commandRunner),
3410
+ isAvailable: async (ecosystem, cwd = process.cwd()) => {
3411
+ const command = commandFor(ecosystem);
3412
+ return command ? (await commandRunner(command, ["--version"], cwd)).status === 0 : false;
3413
+ }
3414
+ };
3415
+ }
2801
3416
 
2802
3417
  // src/policy/status.ts
2803
3418
  function isValidSemver(version) {
@@ -2940,8 +3555,206 @@ function failedLookupStatus(source, error) {
2940
3555
  };
2941
3556
  }
2942
3557
 
2943
- // src/service.ts
3558
+ // src/service/techstack-engine.ts
3559
+ import { randomUUID as randomUUID2 } from "node:crypto";
3560
+
3561
+ // src/service/finding-factory.ts
3562
+ function computeDependencyFingerprint(dependencies) {
3563
+ const parts = dependencies.map((dependency) => `${dependency.name}@${dependency.locked ?? dependency.requested ?? "unknown"}`).sort().join(",");
3564
+ let hash = 0;
3565
+ for (let index = 0; index < parts.length; index++) {
3566
+ hash = (hash << 5) - hash + parts.charCodeAt(index);
3567
+ hash |= 0;
3568
+ }
3569
+ return `ts-${Math.abs(hash).toString(36)}`;
3570
+ }
3571
+ function createFindingForStatus(dependencyId, status) {
3572
+ const common = { dependencyId, confidence: 1, evidence: [] };
3573
+ switch (status) {
3574
+ case "vulnerable":
3575
+ return { ...common, id: `finding-${dependencyId}-vuln`, type: "vulnerability", severity: "high", action: "upgrade_patch", rationale: "Known security advisory found for this package" };
3576
+ case "deprecated":
3577
+ return { ...common, id: `finding-${dependencyId}-dep`, type: "deprecated", severity: "medium", action: "replace", rationale: "Package is deprecated in the registry" };
3578
+ case "yanked":
3579
+ return { ...common, id: `finding-${dependencyId}-yank`, type: "deprecated", severity: "high", action: "replace", rationale: "Package version has been yanked from the registry" };
3580
+ case "update_available_safe":
3581
+ return { ...common, id: `finding-${dependencyId}-update`, type: "upgrade", severity: "info", action: "upgrade_minor", rationale: "A newer compatible version is available" };
3582
+ case "update_available_breaking":
3583
+ return { ...common, id: `finding-${dependencyId}-major`, type: "upgrade", severity: "low", action: "upgrade_major", rationale: "A newer version is available that may require breaking changes" };
3584
+ default:
3585
+ return { ...common, confidence: 0.5, id: `finding-${dependencyId}-investigate`, type: "investigate", severity: "info", action: "investigate", rationale: `Package status is "${status}" \u2014 may need investigation` };
3586
+ }
3587
+ }
3588
+
3589
+ // src/service/enrich-phase.ts
3590
+ async function runEnrichPhase(snapshot, options = {}) {
3591
+ if (options.online === false || options.signal?.aborted) return snapshot;
3592
+ const grouped = /* @__PURE__ */ new Map();
3593
+ for (const dependency of snapshot.dependencies) {
3594
+ if (!dependency.purl || dependency.sourceType === "path" || dependency.sourceType === "git") continue;
3595
+ grouped.set(dependency.ecosystem, [...grouped.get(dependency.ecosystem) ?? [], dependency]);
3596
+ }
3597
+ const enriched = /* @__PURE__ */ new Map();
3598
+ const findings = [...snapshot.findings];
3599
+ for (const [ecosystem, dependencies] of grouped) {
3600
+ for (const name of new Set(dependencies.map((dependency) => dependency.name))) {
3601
+ if (options.signal?.aborted) throw new DOMException("TechStack job cancelled", "AbortError");
3602
+ let registryEntry;
3603
+ let registryStatus;
3604
+ try {
3605
+ registryEntry = await lookupRegistry(ecosystem, name, {
3606
+ signal: options.signal,
3607
+ force: options.forceRegistryRefresh,
3608
+ strictErrors: true
3609
+ });
3610
+ registryStatus = registryEntry ? {
3611
+ latestStable: registryEntry.latestStable,
3612
+ deprecated: registryEntry.deprecated,
3613
+ yanked: registryEntry.yanked,
3614
+ evidence: [{ kind: "registry", source: registryEntry.source, retrievedAt: registryEntry.retrievedAt, detail: `latestStable: ${registryEntry.latestStable ?? "N/A"}, license: ${registryEntry.license ?? "N/A"}` }]
3615
+ } : { privateOrUnresolved: true };
3616
+ } catch (error) {
3617
+ const unresolved = error instanceof RegistryNotFoundError || error instanceof RegistryAuthError;
3618
+ registryStatus = unresolved ? {
3619
+ privateOrUnresolved: true,
3620
+ evidence: [{ kind: "registry", source: `${ecosystem} registry for ${name}`, retrievedAt: (/* @__PURE__ */ new Date()).toISOString(), detail: error.message }]
3621
+ } : {
3622
+ lookupFailed: true,
3623
+ evidence: [{ kind: "registry", source: `${ecosystem} registry for ${name}`, retrievedAt: (/* @__PURE__ */ new Date()).toISOString(), detail: error instanceof Error ? error.message : "Registry lookup failed" }]
3624
+ };
3625
+ }
3626
+ let advisoryStatus;
3627
+ try {
3628
+ const purls = dependencies.filter((dependency) => dependency.name === name).flatMap((dependency) => dependency.purl ? [dependency.purl] : []);
3629
+ if (purls.length > 0) {
3630
+ const result = await queryOsvBatch(purls, { signal: options.signal });
3631
+ if ([...result.advisories.values()].some((items) => items.length > 0)) {
3632
+ advisoryStatus = { hasAdvisory: true, evidence: [result.evidence] };
3633
+ }
3634
+ }
3635
+ } catch {
3636
+ }
3637
+ for (const dependency of dependencies) {
3638
+ if (dependency.name !== name) continue;
3639
+ const status = classifyStatus(dependency, registryStatus, advisoryStatus);
3640
+ const evidence = [...dependency.evidence, ...registryStatus?.evidence ?? [], ...advisoryStatus?.evidence ?? []];
3641
+ enriched.set(dependency.id, {
3642
+ ...dependency,
3643
+ latestStable: registryEntry?.latestStable ?? dependency.latestStable,
3644
+ license: registryEntry?.license ?? dependency.license,
3645
+ deprecated: registryEntry?.deprecated ?? dependency.deprecated,
3646
+ yanked: registryEntry?.yanked ?? dependency.yanked,
3647
+ status,
3648
+ evidence
3649
+ });
3650
+ if (status !== "current" && status !== "local_path" && status !== "git_dependency") findings.push(createFindingForStatus(dependency.id, status));
3651
+ }
3652
+ }
3653
+ }
3654
+ return { ...snapshot, dependencies: snapshot.dependencies.map((dependency) => enriched.get(dependency.id) ?? dependency), findings };
3655
+ }
3656
+
3657
+ // src/service/inventory-phase.ts
2944
3658
  import { randomUUID } from "node:crypto";
3659
+ var ADAPTER_VERSION = "0.1.0";
3660
+ var ADAPTERS = {
3661
+ npm: npmAdapter,
3662
+ python: pythonAdapter,
3663
+ rust: rustAdapter,
3664
+ go: goAdapter,
3665
+ dotnet: dotNetAdapter,
3666
+ php: phpAdapter,
3667
+ dart: dartAdapter,
3668
+ maven: mavenAdapter,
3669
+ gradle: gradleAdapter,
3670
+ ruby: rubyAdapter,
3671
+ swift: swiftAdapter,
3672
+ elixir: elixirAdapter,
3673
+ cpp: cppAdapter
3674
+ };
3675
+ async function runInventoryPhase(store, projectId, targetRoot, options = {}) {
3676
+ options.onProgress?.("discovering", 0, 1);
3677
+ const workspaces = await discoverWorkspaces(targetRoot, { signal: options.signal });
3678
+ options.onProgress?.("inventorying", 0, workspaces.length);
3679
+ const dependencies = [];
3680
+ let coverage = "full";
3681
+ let completed = 0;
3682
+ for (const workspace of workspaces) {
3683
+ if (options.signal?.aborted) throw new DOMException("TechStack job cancelled", "AbortError");
3684
+ try {
3685
+ dependencies.push(...await ADAPTERS[workspace.ecosystem].inventory(workspace, {
3686
+ projectRoot: targetRoot,
3687
+ includeTransitive: options.includeTransitive,
3688
+ signal: options.signal
3689
+ }));
3690
+ } catch {
3691
+ }
3692
+ if (workspace.coverage === "unsupported") coverage = "partial";
3693
+ completed++;
3694
+ options.onProgress?.("inventorying", completed, workspaces.length);
3695
+ }
3696
+ const snapshot = {
3697
+ id: randomUUID(),
3698
+ projectId,
3699
+ targetRoot,
3700
+ fingerprint: computeDependencyFingerprint(dependencies),
3701
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
3702
+ workspaces,
3703
+ dependencies,
3704
+ findings: [],
3705
+ coverage,
3706
+ adapterVersion: ADAPTER_VERSION
3707
+ };
3708
+ store.saveSnapshot(snapshot);
3709
+ return snapshot;
3710
+ }
3711
+
3712
+ // src/service/report-generator.ts
3713
+ function generateReport(snapshot, format = "md") {
3714
+ if (format === "json") return JSON.stringify(snapshot, null, 2);
3715
+ const lines = [
3716
+ "# TechStack Report",
3717
+ "",
3718
+ `**Generated:** ${snapshot.createdAt}`,
3719
+ `**Target:** ${snapshot.targetRoot}`,
3720
+ `**Fingerprint:** ${snapshot.fingerprint}`,
3721
+ `**Workspaces:** ${snapshot.workspaces.length}`,
3722
+ `**Dependencies:** ${snapshot.dependencies.length}`,
3723
+ `**Findings:** ${snapshot.findings.length}`,
3724
+ `**Coverage:** ${snapshot.coverage}`,
3725
+ ""
3726
+ ];
3727
+ if (snapshot.workspaces.length > 0) {
3728
+ lines.push("## Workspaces", "", "| Workspace | Ecosystem | Coverage | Deps |", "|---|---|---|---|");
3729
+ for (const workspace of snapshot.workspaces) {
3730
+ const count = snapshot.dependencies.filter((dependency) => dependency.workspaceId === workspace.id).length;
3731
+ lines.push(`| ${workspace.relativeRoot} | ${workspace.ecosystem} | ${workspace.coverage} | ${count} |`);
3732
+ }
3733
+ lines.push("");
3734
+ }
3735
+ if (snapshot.findings.length > 0) {
3736
+ lines.push("## Findings", "");
3737
+ const bySeverity = /* @__PURE__ */ new Map();
3738
+ for (const finding of snapshot.findings) bySeverity.set(finding.severity, [...bySeverity.get(finding.severity) ?? [], finding]);
3739
+ for (const severity of ["critical", "high", "medium", "low", "info"]) {
3740
+ const findings = bySeverity.get(severity);
3741
+ if (!findings?.length) continue;
3742
+ lines.push(`### ${severity.charAt(0).toUpperCase() + severity.slice(1)} (${findings.length})`, "");
3743
+ for (const finding of findings) {
3744
+ const dependency = snapshot.dependencies.find((candidate) => candidate.id === finding.dependencyId);
3745
+ lines.push(`- **${dependency?.name ?? finding.dependencyId}** \u2014 ${finding.type} \u2014 ${finding.rationale}`);
3746
+ }
3747
+ lines.push("");
3748
+ }
3749
+ }
3750
+ if (snapshot.dependencies.length > 0) {
3751
+ lines.push("## Dependencies", "", "| Name | Ecosystem | Status | Locked | Latest |", "|---|---|---|---|---|");
3752
+ for (const dependency of snapshot.dependencies) {
3753
+ lines.push(`| ${dependency.name} | ${dependency.ecosystem} | ${dependency.status} | ${dependency.locked ?? "\u2014"} | ${dependency.latestStable ?? "\u2014"} |`);
3754
+ }
3755
+ }
3756
+ return lines.join("\n");
3757
+ }
2945
3758
 
2946
3759
  // src/research/triage.ts
2947
3760
  var DEFAULT_TRIAGE_LIMIT = 40;
@@ -3003,285 +3816,45 @@ function clusterCandidates(candidates) {
3003
3816
  return out;
3004
3817
  }
3005
3818
 
3006
- // src/service.ts
3007
- function getAdapter(ecosystem) {
3008
- switch (ecosystem) {
3009
- case "npm":
3010
- return npmAdapter;
3011
- case "python":
3012
- return pythonAdapter;
3013
- case "rust":
3014
- return rustAdapter;
3015
- case "go":
3016
- return goAdapter;
3017
- case "dotnet":
3018
- return dotNetAdapter;
3019
- case "php":
3020
- return phpAdapter;
3021
- case "dart":
3022
- return dartAdapter;
3023
- // Tier B — partial support
3024
- case "maven":
3025
- return mavenAdapter;
3026
- case "gradle":
3027
- return mavenAdapter;
3028
- // reuse Maven adapter (same manifest family)
3029
- case "ruby":
3030
- return rubyAdapter;
3031
- case "swift":
3032
- return void 0;
3033
- // no adapter yet
3034
- case "elixir":
3035
- return elixirAdapter;
3036
- // Tier C — best-effort
3037
- case "cpp":
3038
- return cppAdapter;
3039
- default:
3040
- return void 0;
3819
+ // src/service/research-phase.ts
3820
+ async function runResearchPhase(snapshot, options = {}) {
3821
+ if (!options.researcher || options.signal?.aborted) return snapshot;
3822
+ const candidates = triageCandidates(snapshot.dependencies, { limit: options.researchLimit });
3823
+ if (candidates.length === 0) return snapshot;
3824
+ options.onProgress?.("researching", 0, candidates.length);
3825
+ let findings;
3826
+ try {
3827
+ findings = await options.researcher.research(candidates, {
3828
+ signal: options.signal,
3829
+ onProgress: (completed, total) => options.onProgress?.("researching", completed, total)
3830
+ });
3831
+ } catch {
3832
+ return snapshot;
3041
3833
  }
3834
+ options.onProgress?.("synthesizing", 1, 1);
3835
+ return findings.length ? { ...snapshot, findings: [...snapshot.findings, ...findings] } : snapshot;
3042
3836
  }
3043
- var ADAPTER_VERSION = "0.1.0";
3837
+
3838
+ // src/service/techstack-engine.ts
3044
3839
  var TechStackEngine = class {
3045
- store;
3046
3840
  constructor(store) {
3047
3841
  this.store = store;
3048
3842
  }
3049
- // ── Inventory ─────────────────────────────────────────────────────────
3050
- /**
3051
- * Run an offline inventory: discover workspaces and parse dependencies
3052
- * from manifests and lockfiles. No network calls.
3053
- *
3054
- * Returns a Snapshot with workspace and dependency data but no
3055
- * registry/advisory enrichment.
3056
- */
3057
- async inventory(projectId, targetRoot, _jobId, onProgress) {
3058
- const snapshotId = randomUUID();
3059
- onProgress?.("discovering", 0, 1);
3060
- const rawWorkspaces = await discoverWorkspaces(targetRoot);
3061
- const workspaces = rawWorkspaces.map((w) => ({
3062
- id: w.id,
3063
- relativeRoot: w.relativeRoot,
3064
- ecosystem: w.ecosystem,
3065
- packageManager: w.packageManager,
3066
- manifests: [...w.manifests],
3067
- lockfiles: [...w.lockfiles],
3068
- confidence: w.confidence,
3069
- coverage: w.coverage
3070
- }));
3071
- onProgress?.("inventorying", 0, workspaces.length);
3072
- const allDependencies = [];
3073
- let totalCoverage = "full";
3074
- for (let i = 0; i < workspaces.length; i++) {
3075
- const ws = workspaces[i];
3076
- const adapter = getAdapter(ws.ecosystem);
3077
- let deps = [];
3078
- if (adapter) {
3079
- try {
3080
- deps = await adapter.inventory(ws, { projectRoot: targetRoot });
3081
- } catch {
3082
- deps = [];
3083
- }
3084
- }
3085
- if (ws.coverage === "unsupported") {
3086
- totalCoverage = "partial";
3087
- }
3088
- allDependencies.push(...deps);
3089
- onProgress?.("inventorying", i + 1, workspaces.length);
3090
- }
3091
- const fingerprint = computeFingerprint(allDependencies);
3092
- const snapshot = {
3093
- id: snapshotId,
3094
- projectId,
3095
- targetRoot,
3096
- fingerprint,
3097
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
3098
- workspaces,
3099
- dependencies: allDependencies,
3100
- findings: [],
3101
- coverage: totalCoverage,
3102
- adapterVersion: ADAPTER_VERSION
3103
- };
3104
- this.store.saveSnapshot(snapshot);
3105
- return snapshot;
3843
+ store;
3844
+ inventory(projectId, targetRoot, _jobId, onProgress, options = {}) {
3845
+ return runInventoryPhase(this.store, projectId, targetRoot, { ...options, onProgress });
3106
3846
  }
3107
- // ── Enrichment ─────────────────────────────────────────────────────────
3108
- /**
3109
- * Enrich a snapshot with registry metadata and advisory data.
3110
- *
3111
- * This is the online pass: fetches latest versions, licenses, deprecation
3112
- * status from registries, and queries OSV for advisories.
3113
- *
3114
- * Key contracts:
3115
- * - 404/401 → status `private_or_unresolved` (never `dead` or `deprecated`)
3116
- * - Network failure → status `unknown` with evidence detail (never `current`)
3117
- */
3118
- async enrich(snapshot, options = {}) {
3119
- const isOnline = options.online !== false;
3120
- if (!isOnline || options.signal?.aborted) {
3121
- return snapshot;
3122
- }
3123
- const byEcosystem = /* @__PURE__ */ new Map();
3124
- for (const dep of snapshot.dependencies) {
3125
- if (dep.sourceType === "path" || dep.sourceType === "git") continue;
3126
- if (!dep.purl) continue;
3127
- const list = byEcosystem.get(dep.ecosystem);
3128
- if (list) {
3129
- list.push(dep);
3130
- } else {
3131
- byEcosystem.set(dep.ecosystem, [dep]);
3132
- }
3133
- }
3134
- const enrichedDeps = /* @__PURE__ */ new Map();
3135
- const allFindings = [...snapshot.findings];
3136
- for (const [ecosystem, deps] of byEcosystem) {
3137
- const names = [...new Set(deps.map((d) => d.name))];
3138
- for (const name of names) {
3139
- const lookupOpts = {};
3140
- if (options.signal) lookupOpts.signal = options.signal;
3141
- if (options.forceRegistryRefresh) lookupOpts.force = true;
3142
- let registryEntry;
3143
- let registryStatus;
3144
- try {
3145
- registryEntry = await lookupRegistry(ecosystem, name, lookupOpts);
3146
- if (registryEntry) {
3147
- registryStatus = {
3148
- latestStable: registryEntry.latestStable,
3149
- deprecated: registryEntry.deprecated,
3150
- yanked: registryEntry.yanked,
3151
- evidence: [
3152
- {
3153
- kind: "registry",
3154
- source: registryEntry.source,
3155
- retrievedAt: registryEntry.retrievedAt,
3156
- detail: `latestStable: ${registryEntry.latestStable ?? "N/A"}, license: ${registryEntry.license ?? "N/A"}`
3157
- }
3158
- ]
3159
- };
3160
- } else {
3161
- registryStatus = {
3162
- privateOrUnresolved: true,
3163
- evidence: [
3164
- {
3165
- kind: "registry",
3166
- source: `${ecosystem} registry for ${name}`,
3167
- retrievedAt: (/* @__PURE__ */ new Date()).toISOString(),
3168
- detail: "Package returned 404/401 \u2014 private or unresolved"
3169
- }
3170
- ]
3171
- };
3172
- }
3173
- } catch (err) {
3174
- registryStatus = {
3175
- lookupFailed: true,
3176
- evidence: [
3177
- {
3178
- kind: "registry",
3179
- source: `${ecosystem} registry for ${name}`,
3180
- retrievedAt: (/* @__PURE__ */ new Date()).toISOString(),
3181
- detail: err instanceof Error ? err.message : "Registry lookup failed"
3182
- }
3183
- ]
3184
- };
3185
- }
3186
- let advisoryStatus;
3187
- try {
3188
- const depList = deps.filter((d) => d.name === name);
3189
- const purls = depList.map((d) => d.purl).filter((p) => !!p);
3190
- if (purls.length > 0) {
3191
- const osvResult = await queryOsvBatch(purls, { signal: options.signal });
3192
- const hasAdvisory = [...osvResult.advisories.values()].some(
3193
- (advisories) => advisories.length > 0
3194
- );
3195
- if (hasAdvisory) {
3196
- advisoryStatus = {
3197
- hasAdvisory: true
3198
- };
3199
- }
3200
- }
3201
- } catch {
3202
- }
3203
- for (const dep of deps) {
3204
- if (dep.name !== name) continue;
3205
- const newStatus = classifyStatus(dep, registryStatus, advisoryStatus);
3206
- const newEvidence = [
3207
- ...dep.evidence,
3208
- ...registryStatus?.evidence ?? [],
3209
- ...advisoryStatus?.evidence ?? []
3210
- ];
3211
- enrichedDeps.set(dep.id, {
3212
- ...dep,
3213
- latestStable: registryEntry?.latestStable ?? dep.latestStable,
3214
- license: registryEntry?.license ?? dep.license,
3215
- deprecated: registryEntry?.deprecated ?? dep.deprecated,
3216
- yanked: registryEntry?.yanked ?? dep.yanked,
3217
- status: newStatus,
3218
- evidence: newEvidence
3219
- });
3220
- if (newStatus !== "current" && newStatus !== "local_path" && newStatus !== "git_dependency") {
3221
- allFindings.push(
3222
- createFindingForStatus(dep.id, newStatus, registryEntry?.license)
3223
- );
3224
- }
3225
- }
3226
- }
3227
- }
3228
- const finalDependencies = snapshot.dependencies.map(
3229
- (dep) => enrichedDeps.get(dep.id) ?? dep
3230
- );
3231
- return {
3232
- ...snapshot,
3233
- dependencies: finalDependencies,
3234
- findings: allFindings
3235
- };
3847
+ enrich(snapshot, options = {}) {
3848
+ return runEnrichPhase(snapshot, options);
3236
3849
  }
3237
- // ── Research ───────────────────────────────────────────────────────────
3238
- /**
3239
- * Interpret an enriched snapshot with the LLM: triage the problem cases,
3240
- * research them, and append the resulting findings.
3241
- *
3242
- * Additive by construction. Two invariants hold here, and they are the whole
3243
- * reason this is a separate pass rather than part of `enrich()`:
3244
- *
3245
- * 1. **`snapshot.dependencies` is returned untouched.** Version facts come
3246
- * only from registry evidence — the LLM cannot fabricate a `latestStable`
3247
- * because `Finding` has nowhere to put one (SDD §472).
3248
- * 2. **Failure is not fatal.** No researcher, no candidates, a provider
3249
- * outage, a dry web search — every one of them returns the input snapshot
3250
- * unchanged. A deterministic report is the floor, never a casualty of the
3251
- * optional stage above it.
3252
- *
3253
- * @see docs/specs/techstack-sdd.md §31, §472
3254
- */
3255
- async research(snapshot, options = {}) {
3256
- if (!options.researcher || options.signal?.aborted) return snapshot;
3257
- const candidates = triageCandidates(snapshot.dependencies, {
3258
- limit: options.researchLimit
3259
- });
3260
- if (candidates.length === 0) return snapshot;
3261
- options.onProgress?.("researching", 0, candidates.length);
3262
- let findings;
3263
- try {
3264
- findings = await options.researcher.research(candidates, {
3265
- signal: options.signal,
3266
- onProgress: (completed, total) => {
3267
- options.onProgress?.("researching", completed, total);
3268
- }
3269
- });
3270
- } catch {
3271
- return snapshot;
3272
- }
3273
- options.onProgress?.("synthesizing", 1, 1);
3274
- if (findings.length === 0) return snapshot;
3275
- return { ...snapshot, findings: [...snapshot.findings, ...findings] };
3850
+ research(snapshot, options = {}) {
3851
+ return runResearchPhase(snapshot, options);
3852
+ }
3853
+ generateReport(snapshot, format = "md") {
3854
+ return generateReport(snapshot, format);
3276
3855
  }
3277
- // ── Analyze ───────────────────────────────────────────────────────────
3278
- /**
3279
- * Run a full analysis: inventory + enrich + research + persist.
3280
- * This is the main entry point for the analyze job flow.
3281
- */
3282
3856
  async analyze(projectId, options) {
3283
- const jobId = options.jobId ?? randomUUID();
3284
- const requestedBy = options.requestedBy ?? "system";
3857
+ const jobId = options.jobId ?? randomUUID2();
3285
3858
  const job = {
3286
3859
  id: jobId,
3287
3860
  projectId,
@@ -3289,7 +3862,7 @@ var TechStackEngine = class {
3289
3862
  kind: "analyze",
3290
3863
  status: "queued",
3291
3864
  fingerprint: "",
3292
- requestedBy,
3865
+ requestedBy: options.requestedBy ?? "system",
3293
3866
  sessionId: options.sessionId,
3294
3867
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
3295
3868
  progress: { phase: "queued", completed: 0, total: 0 }
@@ -3305,199 +3878,32 @@ var TechStackEngine = class {
3305
3878
  try {
3306
3879
  throwIfAborted();
3307
3880
  updateJob("discovering", { phase: "discovering", completed: 0, total: 1 });
3308
- const snapshot = await this.inventory(
3309
- projectId,
3310
- options.targetRoot,
3311
- jobId,
3312
- (phase, completed, total) => {
3313
- throwIfAborted();
3314
- updateJob(phase, { phase, completed, total });
3315
- }
3316
- );
3881
+ let snapshot = await this.inventory(projectId, options.targetRoot, jobId, (phase, completed, total) => {
3882
+ throwIfAborted();
3883
+ updateJob(phase, { phase, completed, total });
3884
+ }, { includeTransitive: options.includeTransitive, signal: options.signal });
3317
3885
  throwIfAborted();
3318
- const isOnline = options.online !== false;
3319
- if (isOnline) {
3886
+ if (options.online !== false) {
3320
3887
  updateJob("enriching", { phase: "enriching", completed: 0, total: 1 });
3321
- const enriched = await this.enrich(snapshot, {
3322
- online: true,
3323
- signal: options.signal
3324
- });
3888
+ snapshot = await this.enrich(snapshot, { online: true, signal: options.signal });
3325
3889
  throwIfAborted();
3326
- const researched = await this.research(enriched, {
3890
+ snapshot = await this.research(snapshot, {
3327
3891
  researcher: options.researcher,
3328
3892
  researchLimit: options.researchLimit,
3329
3893
  signal: options.signal,
3330
- onProgress: (phase, completed, total) => {
3331
- updateJob(phase, { phase, completed, total });
3332
- }
3894
+ onProgress: (phase, completed, total) => updateJob(phase, { phase, completed, total })
3333
3895
  });
3334
3896
  throwIfAborted();
3335
- this.store.saveSnapshot(researched);
3336
- updateJob("completed", { phase: "completed", completed: 1, total: 1 });
3337
- return {
3338
- snapshot: researched,
3339
- job: { ...job, status: "completed", completedAt: (/* @__PURE__ */ new Date()).toISOString() }
3340
- };
3341
3897
  }
3342
3898
  this.store.saveSnapshot(snapshot);
3343
3899
  updateJob("completed", { phase: "completed", completed: 1, total: 1 });
3344
- return {
3345
- snapshot,
3346
- job: { ...job, status: "completed", completedAt: (/* @__PURE__ */ new Date()).toISOString() }
3347
- };
3348
- } catch (err) {
3349
- if (options.signal?.aborted || err instanceof DOMException && err.name === "AbortError") {
3350
- updateJob("cancelled");
3351
- } else {
3352
- updateJob("failed");
3353
- }
3354
- throw err;
3355
- }
3356
- }
3357
- // ── Report generation ─────────────────────────────────────────────────
3358
- /**
3359
- * Generate a human-readable report from a snapshot.
3360
- *
3361
- * @param format 'md' for Markdown, 'json' for raw JSON.
3362
- * @returns The report as a string.
3363
- */
3364
- generateReport(snapshot, format = "md") {
3365
- if (format === "json") return JSON.stringify(snapshot, null, 2);
3366
- const lines = [
3367
- "# TechStack Report",
3368
- "",
3369
- `**Generated:** ${snapshot.createdAt}`,
3370
- `**Target:** ${snapshot.targetRoot}`,
3371
- `**Fingerprint:** ${snapshot.fingerprint}`,
3372
- `**Workspaces:** ${snapshot.workspaces.length}`,
3373
- `**Dependencies:** ${snapshot.dependencies.length}`,
3374
- `**Findings:** ${snapshot.findings.length}`,
3375
- `**Coverage:** ${snapshot.coverage}`,
3376
- ""
3377
- ];
3378
- if (snapshot.workspaces.length > 0) {
3379
- lines.push("## Workspaces", "");
3380
- lines.push("| Workspace | Ecosystem | Coverage | Deps |");
3381
- lines.push("|---|---|---|---|");
3382
- for (const ws of snapshot.workspaces) {
3383
- const depCount = snapshot.dependencies.filter((d) => d.workspaceId === ws.id).length;
3384
- lines.push(`| ${ws.relativeRoot} | ${ws.ecosystem} | ${ws.coverage} | ${depCount} |`);
3385
- }
3386
- lines.push("");
3387
- }
3388
- const findings = snapshot.findings;
3389
- if (findings.length > 0) {
3390
- lines.push("## Findings", "");
3391
- const bySeverity = /* @__PURE__ */ new Map();
3392
- for (const f of findings) {
3393
- const list = bySeverity.get(f.severity) ?? [];
3394
- list.push(f);
3395
- bySeverity.set(f.severity, list);
3396
- }
3397
- for (const sev of ["critical", "high", "medium", "low", "info"]) {
3398
- const items = bySeverity.get(sev);
3399
- if (!items || items.length === 0) continue;
3400
- lines.push(`### ${sev.charAt(0).toUpperCase() + sev.slice(1)} (${items.length})`, "");
3401
- for (const f of items) {
3402
- const dep = snapshot.dependencies.find((d) => d.id === f.dependencyId);
3403
- lines.push(`- **${dep?.name ?? f.dependencyId}** \u2014 ${f.type} \u2014 ${f.rationale}`);
3404
- }
3405
- lines.push("");
3406
- }
3407
- }
3408
- if (snapshot.dependencies.length > 0) {
3409
- lines.push("## Dependencies", "");
3410
- lines.push("| Name | Ecosystem | Status | Locked | Latest |");
3411
- lines.push("|---|---|---|---|---|");
3412
- for (const dep of snapshot.dependencies) {
3413
- lines.push(
3414
- `| ${dep.name} | ${dep.ecosystem} | ${dep.status} | ${dep.locked ?? "\u2014"} | ${dep.latestStable ?? "\u2014"} |`
3415
- );
3416
- }
3900
+ return { snapshot, job: { ...job, status: "completed", completedAt: (/* @__PURE__ */ new Date()).toISOString() } };
3901
+ } catch (error) {
3902
+ updateJob(options.signal?.aborted || error instanceof DOMException && error.name === "AbortError" ? "cancelled" : "failed");
3903
+ throw error;
3417
3904
  }
3418
- return lines.join("\n");
3419
3905
  }
3420
3906
  };
3421
- function computeFingerprint(dependencies) {
3422
- const parts = dependencies.map((d) => `${d.name}@${d.locked ?? d.requested ?? "unknown"}`).sort().join(",");
3423
- let hash = 0;
3424
- for (let i = 0; i < parts.length; i++) {
3425
- const char = parts.charCodeAt(i);
3426
- hash = (hash << 5) - hash + char;
3427
- hash |= 0;
3428
- }
3429
- return `ts-${Math.abs(hash).toString(36)}`;
3430
- }
3431
- function createFindingForStatus(dependencyId, status, _license) {
3432
- switch (status) {
3433
- case "vulnerable":
3434
- return {
3435
- id: `finding-${dependencyId}-vuln`,
3436
- dependencyId,
3437
- type: "vulnerability",
3438
- severity: "high",
3439
- action: "upgrade_patch",
3440
- confidence: 1,
3441
- rationale: "Known security advisory found for this package",
3442
- evidence: []
3443
- };
3444
- case "deprecated":
3445
- return {
3446
- id: `finding-${dependencyId}-dep`,
3447
- dependencyId,
3448
- type: "deprecated",
3449
- severity: "medium",
3450
- action: "replace",
3451
- confidence: 1,
3452
- rationale: "Package is deprecated in the registry",
3453
- evidence: []
3454
- };
3455
- case "yanked":
3456
- return {
3457
- id: `finding-${dependencyId}-yank`,
3458
- dependencyId,
3459
- type: "deprecated",
3460
- severity: "high",
3461
- action: "replace",
3462
- confidence: 1,
3463
- rationale: "Package version has been yanked from the registry",
3464
- evidence: []
3465
- };
3466
- case "update_available_safe":
3467
- return {
3468
- id: `finding-${dependencyId}-update`,
3469
- dependencyId,
3470
- type: "upgrade",
3471
- severity: "info",
3472
- action: "upgrade_minor",
3473
- confidence: 1,
3474
- rationale: "A newer compatible version is available",
3475
- evidence: []
3476
- };
3477
- case "update_available_breaking":
3478
- return {
3479
- id: `finding-${dependencyId}-major`,
3480
- dependencyId,
3481
- type: "upgrade",
3482
- severity: "low",
3483
- action: "upgrade_major",
3484
- confidence: 1,
3485
- rationale: "A newer version is available that may require breaking changes",
3486
- evidence: []
3487
- };
3488
- default:
3489
- return {
3490
- id: `finding-${dependencyId}-investigate`,
3491
- dependencyId,
3492
- type: "investigate",
3493
- severity: "info",
3494
- action: "investigate",
3495
- confidence: 0.5,
3496
- rationale: `Package status is "${status}" \u2014 may need investigation`,
3497
- evidence: []
3498
- };
3499
- }
3500
- }
3501
3907
 
3502
3908
  // src/research/llm.ts
3503
3909
  var DEFAULT_TIMEOUT_MS = 45e3;
@@ -3857,10 +4263,10 @@ function createToolSearch(options = {}) {
3857
4263
  }
3858
4264
 
3859
4265
  // src/store/sqlite.ts
3860
- import { DatabaseSync } from "node:sqlite";
3861
- import { mkdirSync, existsSync as existsSync3 } from "node:fs";
3862
- import { join as join7 } from "node:path";
3863
- import { homedir } from "node:os";
4266
+ import { createRequire } from "node:module";
4267
+ import { mkdirSync, existsSync as existsSync2 } from "node:fs";
4268
+ import { dirname as dirname2, join as join9 } from "node:path";
4269
+ import { wstackGlobalRoot } from "@wrongstack/core/utils";
3864
4270
 
3865
4271
  // src/store/schema.ts
3866
4272
  var SCHEMA_VERSION = 1;
@@ -3930,30 +4336,77 @@ function applySchema(db) {
3930
4336
  }
3931
4337
 
3932
4338
  // src/store/sqlite.ts
4339
+ var DatabaseSyncCtor;
4340
+ var SQLITE_EXPERIMENTAL_WARNING = "SQLite is an experimental feature and might change at any time";
4341
+ function loadDatabaseSync() {
4342
+ if (DatabaseSyncCtor) return DatabaseSyncCtor;
4343
+ const originalEmitWarning = process.emitWarning;
4344
+ const forwardWarning = originalEmitWarning.bind(process);
4345
+ process.emitWarning = ((warning, ...rest) => {
4346
+ const message = typeof warning === "string" ? warning : warning instanceof Error ? warning.message : "";
4347
+ const typeOrOptions = rest[0];
4348
+ const warningType = typeof warning === "string" ? typeof typeOrOptions === "string" ? typeOrOptions : typeof typeOrOptions === "object" && typeOrOptions !== null && "type" in typeOrOptions && typeof typeOrOptions.type === "string" ? typeOrOptions.type : "" : warning instanceof Error ? warning.name : "";
4349
+ const warningCode = typeof warning === "string" ? typeof typeOrOptions === "object" && typeOrOptions !== null && "code" in typeOrOptions && typeof typeOrOptions.code === "string" ? typeOrOptions.code : typeof rest[1] === "string" ? rest[1] : "" : warning instanceof Error && "code" in warning && typeof warning.code === "string" ? warning.code : "";
4350
+ if (message === SQLITE_EXPERIMENTAL_WARNING && (warningType === "ExperimentalWarning" || warningCode === "ExperimentalWarning")) {
4351
+ return;
4352
+ }
4353
+ forwardWarning(warning, ...rest);
4354
+ });
4355
+ try {
4356
+ try {
4357
+ const require2 = createRequire(import.meta.url);
4358
+ const sqliteModule = require2("node:sqlite");
4359
+ if (typeof sqliteModule !== "object" || sqliteModule === null || !("DatabaseSync" in sqliteModule) || typeof sqliteModule.DatabaseSync !== "function") {
4360
+ throw new Error("node:sqlite DatabaseSync unavailable");
4361
+ }
4362
+ DatabaseSyncCtor = sqliteModule.DatabaseSync;
4363
+ return DatabaseSyncCtor;
4364
+ } catch (error) {
4365
+ const message = error instanceof Error ? error.message : String(error);
4366
+ throw new Error(
4367
+ `TechStack SQLite store needs Node's built-in SQLite (node:sqlite), available since Node 22.5. This runtime doesn't provide it: ${message}`,
4368
+ { cause: error }
4369
+ );
4370
+ }
4371
+ } finally {
4372
+ process.emitWarning = originalEmitWarning;
4373
+ }
4374
+ }
3933
4375
  var TechStackStore = class {
3934
4376
  db;
3935
4377
  dbPath;
4378
+ /** Compile-once cache for fixed SQL used on hot job/snapshot paths. */
4379
+ stmtCache = /* @__PURE__ */ new Map();
3936
4380
  constructor(options) {
3937
- this.dbPath = options.dbPath ?? join7(
3938
- homedir(),
3939
- ".wrongstack",
3940
- "projects",
3941
- options.projectSlug,
3942
- "techstack",
3943
- "techstack.db"
3944
- );
3945
- const dir = this.dbPath.slice(0, this.dbPath.lastIndexOf("\\"));
3946
- if (!existsSync3(dir)) {
4381
+ this.dbPath = options.dbPath ?? join9(wstackGlobalRoot(), "projects", options.projectSlug, "techstack", "techstack.db");
4382
+ const dir = dirname2(this.dbPath);
4383
+ if (!existsSync2(dir)) {
3947
4384
  mkdirSync(dir, { recursive: true });
3948
4385
  }
3949
- this.db = new DatabaseSync(this.dbPath);
4386
+ const Database = loadDatabaseSync();
4387
+ this.db = new Database(this.dbPath);
3950
4388
  this.db.exec("PRAGMA journal_mode = WAL;");
4389
+ this.db.exec("PRAGMA synchronous = NORMAL;");
4390
+ this.db.exec("PRAGMA busy_timeout = 10000;");
4391
+ this.db.exec("PRAGMA temp_store = MEMORY;");
4392
+ this.db.exec("PRAGMA cache_size = -32768;");
4393
+ this.db.exec("PRAGMA mmap_size = 134217728;");
3951
4394
  this.db.exec("PRAGMA foreign_keys = ON;");
3952
4395
  applySchema(this.db);
3953
4396
  }
4397
+ /** Prepare-once helper: compile `sql` on first use, reuse thereafter. */
4398
+ stmt(sql) {
4399
+ let prepared = this.stmtCache.get(sql);
4400
+ if (prepared === void 0) {
4401
+ prepared = this.db.prepare(sql);
4402
+ this.stmtCache.set(sql, prepared);
4403
+ }
4404
+ return prepared;
4405
+ }
3954
4406
  // ── Lifecycle ───────────────────────────────────────────────────────────
3955
4407
  /** Close the database connection. Idempotent. */
3956
4408
  close() {
4409
+ this.stmtCache.clear();
3957
4410
  try {
3958
4411
  this.db.close();
3959
4412
  } catch {
@@ -3966,7 +4419,7 @@ var TechStackStore = class {
3966
4419
  // ── Snapshots ───────────────────────────────────────────────────────────
3967
4420
  /** Persist a snapshot. */
3968
4421
  saveSnapshot(snapshot) {
3969
- const stmt = this.db.prepare(`
4422
+ const stmt = this.stmt(`
3970
4423
  INSERT OR REPLACE INTO snapshots (id, project_id, target_root, fingerprint, created_at, raw_json, adapter_version)
3971
4424
  VALUES (?, ?, ?, ?, ?, ?, ?)
3972
4425
  `);
@@ -3982,7 +4435,7 @@ var TechStackStore = class {
3982
4435
  }
3983
4436
  /** Get a snapshot by project ID (latest). */
3984
4437
  getSnapshot(projectId) {
3985
- const stmt = this.db.prepare(`
4438
+ const stmt = this.stmt(`
3986
4439
  SELECT raw_json FROM snapshots
3987
4440
  WHERE project_id = ?
3988
4441
  ORDER BY created_at DESC
@@ -3998,7 +4451,7 @@ var TechStackStore = class {
3998
4451
  }
3999
4452
  /** Get a snapshot by ID. */
4000
4453
  getSnapshotById(id) {
4001
- const stmt = this.db.prepare(`
4454
+ const stmt = this.stmt(`
4002
4455
  SELECT raw_json FROM snapshots WHERE id = ?
4003
4456
  `);
4004
4457
  const row = stmt.get(id);
@@ -4011,7 +4464,7 @@ var TechStackStore = class {
4011
4464
  }
4012
4465
  /** List all snapshots for a project (newest first). */
4013
4466
  listSnapshots(projectId, limit = 20) {
4014
- const stmt = this.db.prepare(`
4467
+ const stmt = this.stmt(`
4015
4468
  SELECT raw_json FROM snapshots
4016
4469
  WHERE project_id = ?
4017
4470
  ORDER BY created_at DESC
@@ -4028,7 +4481,7 @@ var TechStackStore = class {
4028
4481
  }
4029
4482
  /** Delete snapshots older than a given timestamp. */
4030
4483
  deleteSnapshotsBefore(projectId, before) {
4031
- const stmt = this.db.prepare(`
4484
+ const stmt = this.stmt(`
4032
4485
  DELETE FROM snapshots WHERE project_id = ? AND created_at < ?
4033
4486
  `);
4034
4487
  const result = stmt.run(projectId, before);
@@ -4037,7 +4490,7 @@ var TechStackStore = class {
4037
4490
  // ── Jobs ────────────────────────────────────────────────────────────────
4038
4491
  /** Persist a job. */
4039
4492
  saveJob(job) {
4040
- const stmt = this.db.prepare(`
4493
+ const stmt = this.stmt(`
4041
4494
  INSERT OR REPLACE INTO jobs
4042
4495
  (id, project_id, target_root, kind, status, fingerprint, requested_by, session_id, created_at, completed_at, error, progress_json)
4043
4496
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
@@ -4059,7 +4512,7 @@ var TechStackStore = class {
4059
4512
  }
4060
4513
  /** Get a job by ID. */
4061
4514
  getJob(id) {
4062
- const stmt = this.db.prepare(`
4515
+ const stmt = this.stmt(`
4063
4516
  SELECT * FROM jobs WHERE id = ?
4064
4517
  `);
4065
4518
  const row = stmt.get(id);
@@ -4070,7 +4523,7 @@ var TechStackStore = class {
4070
4523
  updateJobStatus(id, status, progress) {
4071
4524
  const progressJson = progress ? JSON.stringify(progress) : null;
4072
4525
  const completedAt = status === "completed" || status === "failed" || status === "cancelled" ? (/* @__PURE__ */ new Date()).toISOString() : null;
4073
- const stmt = this.db.prepare(`
4526
+ const stmt = this.stmt(`
4074
4527
  UPDATE jobs
4075
4528
  SET status = ?, progress_json = ?, completed_at = COALESCE(?, completed_at)
4076
4529
  WHERE id = ?
@@ -4079,7 +4532,7 @@ var TechStackStore = class {
4079
4532
  }
4080
4533
  /** List jobs for a project (newest first). */
4081
4534
  listJobs(projectId, limit = 50) {
4082
- const stmt = this.db.prepare(`
4535
+ const stmt = this.stmt(`
4083
4536
  SELECT * FROM jobs WHERE project_id = ? ORDER BY created_at DESC LIMIT ?
4084
4537
  `);
4085
4538
  const rows = stmt.all(projectId, limit);
@@ -4088,7 +4541,7 @@ var TechStackStore = class {
4088
4541
  // ── Outbox ──────────────────────────────────────────────────────────────
4089
4542
  /** Create an outbox entry. */
4090
4543
  createOutbox(deliveryId, reportId, sessionId) {
4091
- const stmt = this.db.prepare(`
4544
+ const stmt = this.stmt(`
4092
4545
  INSERT OR IGNORE INTO outbox (delivery_id, report_id, session_id, status, attempts)
4093
4546
  VALUES (?, ?, ?, 'pending', 0)
4094
4547
  `);
@@ -4096,7 +4549,7 @@ var TechStackStore = class {
4096
4549
  }
4097
4550
  /** Claim an outbox entry (atomic CAS). */
4098
4551
  claimOutbox(deliveryId, sessionId) {
4099
- const stmt = this.db.prepare(`
4552
+ const stmt = this.stmt(`
4100
4553
  UPDATE outbox
4101
4554
  SET status = 'claimed', claimed_at = datetime('now'), attempts = attempts + 1
4102
4555
  WHERE delivery_id = ? AND session_id = ? AND status = 'pending'
@@ -4106,7 +4559,7 @@ var TechStackStore = class {
4106
4559
  }
4107
4560
  /** Mark an outbox entry as delivered. */
4108
4561
  deliverOutbox(deliveryId) {
4109
- const stmt = this.db.prepare(`
4562
+ const stmt = this.stmt(`
4110
4563
  UPDATE outbox SET status = 'delivered', delivered_at = datetime('now')
4111
4564
  WHERE delivery_id = ?
4112
4565
  `);
@@ -4114,14 +4567,14 @@ var TechStackStore = class {
4114
4567
  }
4115
4568
  /** Mark an outbox entry as failed. */
4116
4569
  failOutbox(deliveryId) {
4117
- const stmt = this.db.prepare(`
4570
+ const stmt = this.stmt(`
4118
4571
  UPDATE outbox SET status = 'failed' WHERE delivery_id = ?
4119
4572
  `);
4120
4573
  stmt.run(deliveryId);
4121
4574
  }
4122
4575
  /** List outbox entries by status. */
4123
4576
  listOutboxByStatus(status) {
4124
- const stmt = this.db.prepare(`
4577
+ const stmt = this.stmt(`
4125
4578
  SELECT * FROM outbox WHERE status = ?
4126
4579
  `);
4127
4580
  const rows = stmt.all(status);
@@ -4228,14 +4681,22 @@ export {
4228
4681
  DotNetAdapter,
4229
4682
  ElixirAdapter,
4230
4683
  GoAdapter,
4684
+ GradleAdapter,
4231
4685
  MavenAdapter,
4232
4686
  NpmAdapter,
4233
4687
  PhpAdapter,
4234
4688
  PythonAdapter,
4689
+ RegistryAuthError,
4690
+ RegistryNetworkError,
4691
+ RegistryNotFoundError,
4692
+ RegistryRateLimitError,
4235
4693
  RubyAdapter,
4236
4694
  RustAdapter,
4695
+ SwiftAdapter,
4237
4696
  TechStackEngine,
4238
4697
  TechStackStore,
4698
+ TrendStore,
4699
+ applyPlan,
4239
4700
  applySchema,
4240
4701
  attemptDelivery,
4241
4702
  buildPurl,
@@ -4246,6 +4707,7 @@ export {
4246
4707
  constructPurl,
4247
4708
  coverageForEcosystem,
4248
4709
  cppAdapter,
4710
+ createAuditRunner,
4249
4711
  createProviderLlm,
4250
4712
  createResearcher,
4251
4713
  createToolSearch,
@@ -4259,6 +4721,8 @@ export {
4259
4721
  failedLookupStatus,
4260
4722
  generateUpgradePlan,
4261
4723
  goAdapter,
4724
+ gradleAdapter,
4725
+ invalidateRegistryCache,
4262
4726
  isNativeAuditAvailable,
4263
4727
  lookupRegistry,
4264
4728
  lookupRegistryBatch,
@@ -4275,12 +4739,15 @@ export {
4275
4739
  queryOsvBatch,
4276
4740
  queryOsvSingle,
4277
4741
  renderPlanMarkdown,
4742
+ renderTrendMarkdown,
4278
4743
  rubyAdapter,
4279
4744
  runNativeAudit,
4280
4745
  runNpmAudit,
4281
4746
  rustAdapter,
4282
4747
  supportedRegistryEcosystems,
4748
+ swiftAdapter,
4283
4749
  toCycloneDX,
4750
+ toLanguagePackageInput,
4284
4751
  toSpdx,
4285
4752
  triageCandidates
4286
4753
  };