@vibgrate/cli 2026.826.1 → 2026.829.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. package/DOCS.md +204 -9
  2. package/README.md +2 -0
  3. package/dist/{agent-4RHH2FR2.js → agent-5GW7C7EB.js} +14 -14
  4. package/dist/{agent-4RHH2FR2.js.map → agent-5GW7C7EB.js.map} +1 -1
  5. package/dist/{approvals-XDFM346D.js → approvals-ISJFILVK.js} +5 -5
  6. package/dist/{approvals-XDFM346D.js.map → approvals-ISJFILVK.js.map} +1 -1
  7. package/dist/{attach-LGTCYNKC.js → attach-JUSALQ75.js} +3 -3
  8. package/dist/{attach-LGTCYNKC.js.map → attach-JUSALQ75.js.map} +1 -1
  9. package/dist/baseline-YL2IZZ4Y.js +9 -0
  10. package/dist/{baseline-PRY5HKL5.js.map → baseline-YL2IZZ4Y.js.map} +1 -1
  11. package/dist/{chunk-ZRDTI67N.js → chunk-5RW5PIBH.js} +3 -3
  12. package/dist/{chunk-ZRDTI67N.js.map → chunk-5RW5PIBH.js.map} +1 -1
  13. package/dist/{chunk-TUVB6Q6F.js → chunk-65YN3BG3.js} +1207 -40
  14. package/dist/chunk-65YN3BG3.js.map +1 -0
  15. package/dist/{chunk-TKUWEJZJ.js → chunk-B5VCPIKT.js} +7 -6
  16. package/dist/chunk-B5VCPIKT.js.map +1 -0
  17. package/dist/{chunk-WU2DVESP.js → chunk-BA6YBVNT.js} +3 -3
  18. package/dist/{chunk-WU2DVESP.js.map → chunk-BA6YBVNT.js.map} +1 -1
  19. package/dist/{chunk-IM7HME2T.js → chunk-BODXEZEY.js} +9 -9
  20. package/dist/{chunk-IM7HME2T.js.map → chunk-BODXEZEY.js.map} +1 -1
  21. package/dist/{chunk-PIB3YNJU.js → chunk-D3SV3JWB.js} +3 -3
  22. package/dist/{chunk-PIB3YNJU.js.map → chunk-D3SV3JWB.js.map} +1 -1
  23. package/dist/chunk-EGMBHPXV.js +127 -0
  24. package/dist/chunk-EGMBHPXV.js.map +1 -0
  25. package/dist/{chunk-7KOEEEIZ.js → chunk-FHSEK64W.js} +7 -7
  26. package/dist/{chunk-7KOEEEIZ.js.map → chunk-FHSEK64W.js.map} +1 -1
  27. package/dist/{chunk-PEHMRPYM.js → chunk-FKCN653F.js} +3 -3
  28. package/dist/{chunk-PEHMRPYM.js.map → chunk-FKCN653F.js.map} +1 -1
  29. package/dist/{chunk-K2IWOZEQ.js → chunk-FPIMNVLI.js} +12 -4
  30. package/dist/chunk-FPIMNVLI.js.map +1 -0
  31. package/dist/{chunk-BHDP7EVJ.js → chunk-HHMWE52J.js} +10 -135
  32. package/dist/chunk-HHMWE52J.js.map +1 -0
  33. package/dist/{chunk-5UG4UHKU.js → chunk-HUJZO4K5.js} +281 -106
  34. package/dist/chunk-HUJZO4K5.js.map +1 -0
  35. package/dist/{chunk-TQ75FB52.js → chunk-JWQ2JCQS.js} +4 -4
  36. package/dist/{chunk-TQ75FB52.js.map → chunk-JWQ2JCQS.js.map} +1 -1
  37. package/dist/{chunk-WGA4KUGS.js → chunk-K7QR7U3V.js} +12 -11
  38. package/dist/chunk-K7QR7U3V.js.map +1 -0
  39. package/dist/{chunk-E44I7635.js → chunk-KCKG3EEJ.js} +3 -3
  40. package/dist/{chunk-E44I7635.js.map → chunk-KCKG3EEJ.js.map} +1 -1
  41. package/dist/{chunk-WGSLN73L.js → chunk-MMIIVRQC.js} +7 -7
  42. package/dist/chunk-MMIIVRQC.js.map +1 -0
  43. package/dist/chunk-N6R4XPLX.js +27 -0
  44. package/dist/chunk-N6R4XPLX.js.map +1 -0
  45. package/dist/{chunk-WPQCJADP.js → chunk-NQQT75AN.js} +62 -22
  46. package/dist/chunk-NQQT75AN.js.map +1 -0
  47. package/dist/{chunk-TOHNVUDD.js → chunk-NSC7ZK62.js} +3 -3
  48. package/dist/{chunk-TOHNVUDD.js.map → chunk-NSC7ZK62.js.map} +1 -1
  49. package/dist/{chunk-LAYDXJEQ.js → chunk-OAYSRM7J.js} +12 -14
  50. package/dist/chunk-OAYSRM7J.js.map +1 -0
  51. package/dist/{chunk-R5BGRJ6X.js → chunk-QMROD7MI.js} +15 -8
  52. package/dist/chunk-QMROD7MI.js.map +1 -0
  53. package/dist/{chunk-SHD5TCZ5.js → chunk-R2FX4USU.js} +7 -7
  54. package/dist/chunk-R2FX4USU.js.map +1 -0
  55. package/dist/{chunk-7S4ELKK6.js → chunk-RL7JYG3D.js} +3 -3
  56. package/dist/{chunk-7S4ELKK6.js.map → chunk-RL7JYG3D.js.map} +1 -1
  57. package/dist/{chunk-QLNJ3VWO.js → chunk-RRSUNTZI.js} +3 -3
  58. package/dist/{chunk-QLNJ3VWO.js.map → chunk-RRSUNTZI.js.map} +1 -1
  59. package/dist/{chunk-QONHVC4H.js → chunk-TWFPHXPN.js} +3 -3
  60. package/dist/{chunk-QONHVC4H.js.map → chunk-TWFPHXPN.js.map} +1 -1
  61. package/dist/chunk-UGRUG2FA.js +6 -0
  62. package/dist/{chunk-JQ7OL4XG.js.map → chunk-UGRUG2FA.js.map} +1 -1
  63. package/dist/{chunk-XW4FCBL4.js → chunk-VOEEYCR3.js} +6 -6
  64. package/dist/{chunk-XW4FCBL4.js.map → chunk-VOEEYCR3.js.map} +1 -1
  65. package/dist/{chunk-WHNB3443.js → chunk-WBDRBP22.js} +4 -4
  66. package/dist/{chunk-WHNB3443.js.map → chunk-WBDRBP22.js.map} +1 -1
  67. package/dist/{chunk-NQU6E6OG.js → chunk-YL332BU2.js} +20 -15
  68. package/dist/chunk-YL332BU2.js.map +1 -0
  69. package/dist/{chunk-IEZCFMNL.js → chunk-ZQY5TWCB.js} +3 -3
  70. package/dist/{chunk-IEZCFMNL.js.map → chunk-ZQY5TWCB.js.map} +1 -1
  71. package/dist/cli.d.ts +16 -1
  72. package/dist/cli.js +2359 -340
  73. package/dist/cli.js.map +1 -1
  74. package/dist/{daemon-2YMCH7R2.js → daemon-NRIIDMX2.js} +10 -10
  75. package/dist/{daemon-2YMCH7R2.js.map → daemon-NRIIDMX2.js.map} +1 -1
  76. package/dist/embed-worker-main.js +3 -3
  77. package/dist/{ensure-map-S2GBW2UC.js → ensure-map-VAIYCCEJ.js} +17 -16
  78. package/dist/{ensure-map-S2GBW2UC.js.map → ensure-map-VAIYCCEJ.js.map} +1 -1
  79. package/dist/explain-EXPJTCB5.js +126 -0
  80. package/dist/explain-EXPJTCB5.js.map +1 -0
  81. package/dist/{graph-backend-QDEU5RIR.js → graph-backend-V5A2H5ZB.js} +7 -7
  82. package/dist/{graph-backend-QDEU5RIR.js.map → graph-backend-V5A2H5ZB.js.map} +1 -1
  83. package/dist/index.d.ts +217 -13
  84. package/dist/index.js +17 -16
  85. package/dist/index.js.map +1 -1
  86. package/dist/{interactive-GMGWUEQO.js → interactive-VQM2CTE2.js} +32 -31
  87. package/dist/interactive-VQM2CTE2.js.map +1 -0
  88. package/dist/load-UAVA732M.js +11 -0
  89. package/dist/{load-JXCTAAEI.js.map → load-UAVA732M.js.map} +1 -1
  90. package/dist/parse-worker.js +2 -2
  91. package/dist/{rules-ASYEUNB5.js → rules-TLRGB5L4.js} +12 -4
  92. package/dist/rules-TLRGB5L4.js.map +1 -0
  93. package/dist/{run-outcome-5NA7PI52.js → run-outcome-EATWIKLR.js} +5 -5
  94. package/dist/{run-outcome-5NA7PI52.js.map → run-outcome-EATWIKLR.js.map} +1 -1
  95. package/dist/{runtime-session-IEM6DZDP.js → runtime-session-Y2BRBXUW.js} +9 -9
  96. package/dist/{runtime-session-IEM6DZDP.js.map → runtime-session-Y2BRBXUW.js.map} +1 -1
  97. package/dist/{session-VYFLJ7KL.js → session-N2FTYIHQ.js} +9 -9
  98. package/dist/{session-VYFLJ7KL.js.map → session-N2FTYIHQ.js.map} +1 -1
  99. package/dist/{stream-json-2ISQLKPD.js → stream-json-GCZAHORX.js} +14 -14
  100. package/dist/{stream-json-2ISQLKPD.js.map → stream-json-GCZAHORX.js.map} +1 -1
  101. package/dist/version-XNZ4GLMQ.js +4 -0
  102. package/dist/{version-B62ELJRY.js.map → version-XNZ4GLMQ.js.map} +1 -1
  103. package/dist/{vg-mcp-bridge-CJIK5U56.js → vg-mcp-bridge-VDZWFEO2.js} +22 -11
  104. package/dist/vg-mcp-bridge-VDZWFEO2.js.map +1 -0
  105. package/dist/{vgd-4EB6SKFB.js → vgd-3A2TRNP5.js} +8 -8
  106. package/dist/vgd-3A2TRNP5.js.map +1 -0
  107. package/package.json +2 -2
  108. package/dist/baseline-PRY5HKL5.js +0 -9
  109. package/dist/chunk-5UG4UHKU.js.map +0 -1
  110. package/dist/chunk-BHDP7EVJ.js.map +0 -1
  111. package/dist/chunk-JQ7OL4XG.js +0 -6
  112. package/dist/chunk-K2IWOZEQ.js.map +0 -1
  113. package/dist/chunk-LAYDXJEQ.js.map +0 -1
  114. package/dist/chunk-NQU6E6OG.js.map +0 -1
  115. package/dist/chunk-R5BGRJ6X.js.map +0 -1
  116. package/dist/chunk-SHD5TCZ5.js.map +0 -1
  117. package/dist/chunk-TKUWEJZJ.js.map +0 -1
  118. package/dist/chunk-TUVB6Q6F.js.map +0 -1
  119. package/dist/chunk-WGA4KUGS.js.map +0 -1
  120. package/dist/chunk-WGSLN73L.js.map +0 -1
  121. package/dist/chunk-WPQCJADP.js.map +0 -1
  122. package/dist/chunk-YENM3HT7.js +0 -23
  123. package/dist/chunk-YENM3HT7.js.map +0 -1
  124. package/dist/interactive-GMGWUEQO.js.map +0 -1
  125. package/dist/load-JXCTAAEI.js +0 -11
  126. package/dist/rules-ASYEUNB5.js.map +0 -1
  127. package/dist/version-B62ELJRY.js +0 -4
  128. package/dist/vg-mcp-bridge-CJIK5U56.js.map +0 -1
  129. package/dist/vgd-4EB6SKFB.js.map +0 -1
@@ -1,17 +1,20 @@
1
1
  import { availableRegionIds, resolveIngestHost, dashHostForIngestHost, ingestHostForRegionId } from './chunk-HTLDXEHW.js';
2
- import { loadConfig, computeUpgradeImpact, getChangelogSignals, gitHistoryAvailable, buildVersionTimelines, findPackageTimeline, severityRank, projectTypeToVulnEcosystem } from './chunk-5UG4UHKU.js';
3
- import { searchSymbols, resolveVersion, resolveLib, loadCatalog, driftFor, localPackageDocs, readDoc, localApiSurface } from './chunk-QONHVC4H.js';
2
+ import { loadConfig, computeUpgradeImpact, getChangelogSignals, gitHistoryAvailable, buildVersionTimelines, findPackageTimeline, severityRank, projectTypeToVulnEcosystem, classifyFile } from './chunk-HUJZO4K5.js';
3
+ import { searchSymbols, resolveVersion, resolveLib, loadCatalog, driftFor, localPackageDocs, readDoc, localApiSurface } from './chunk-TWFPHXPN.js';
4
4
  import { discoverModels } from './chunk-WNKGRXUA.js';
5
+ import { CliError, ExitCode } from './chunk-MS6V5XKB.js';
5
6
  import { writeTextFile, pathExists, readJsonFile } from './chunk-CRAMOEBE.js';
6
7
  import { inventory } from './chunk-6BEBJCK4.js';
7
8
  import { resolveOne } from './chunk-J7LKEB2R.js';
8
- import { impactOf, indexFor, queryGraphSemantic, queryGraph } from './chunk-NQU6E6OG.js';
9
+ import { impactOf, indexFor, rankingAskFrom, queryGraphSemantic, queryGraph } from './chunk-YL332BU2.js';
9
10
  import { loadOrDiscoverFederation, highConfidenceBridges } from './chunk-RTURQXQL.js';
10
- import { withTimeout, getNodeEmbeddings, loadEmbedder, isModelReady } from './chunk-WU2DVESP.js';
11
- import { loadTopicTags, rankQuestion } from './chunk-K2IWOZEQ.js';
11
+ import { withTimeout, getNodeEmbeddings, loadEmbedder, isModelReady } from './chunk-BA6YBVNT.js';
12
+ import { loadTopicTags, rankQuestion } from './chunk-FPIMNVLI.js';
12
13
  import { cacheDir } from './chunk-MVPRPSXU.js';
13
14
  import { parseGraph } from './chunk-O3LABX6I.js';
15
+ import { VERSION } from './chunk-UGRUG2FA.js';
14
16
  import { repositoryIdFromRoot } from './chunk-XP6UUCZZ.js';
17
+ import { canonicalize } from './chunk-JFGYT6BI.js';
15
18
  import { bidirectional } from 'graphology-shortest-path/unweighted.js';
16
19
  import Graph from 'graphology';
17
20
  import * as fs from 'fs';
@@ -19,10 +22,13 @@ import * as path4 from 'path';
19
22
  import * as crypto from 'crypto';
20
23
  import { createHash } from 'crypto';
21
24
  import { Command } from 'commander';
22
- import chalk from 'chalk';
25
+ import chalk3 from 'chalk';
23
26
  import * as os from 'os';
24
27
  import * as zlib from 'zlib';
25
28
  import { promisify } from 'util';
29
+ import { sha256 } from '@noble/hashes/sha2.js';
30
+ import { bytesToHex } from '@noble/hashes/utils.js';
31
+ import { spawnSync } from 'child_process';
26
32
  import { encode, decode } from 'gpt-tokenizer/encoding/cl100k_base';
27
33
 
28
34
  // src/grounding/pack.ts
@@ -317,63 +323,63 @@ dsnCommand.command("create").description("Create a new DSN token").option("--ing
317
323
  try {
318
324
  ingestHost = resolveIngestHost(opts.region, opts.ingest);
319
325
  } catch (e) {
320
- console.error(chalk.red(e instanceof Error ? e.message : String(e)));
326
+ console.error(chalk3.red(e instanceof Error ? e.message : String(e)));
321
327
  process.exit(1);
322
328
  }
323
329
  let workspaceId = opts.workspace;
324
330
  const isNewWorkspace = opts.workspace.toLowerCase() === "new";
325
331
  if (isNewWorkspace) {
326
332
  workspaceId = crypto.randomBytes(8).toString("hex");
327
- console.log(chalk.dim(`Provisioning new workspace ${workspaceId}...`));
333
+ console.log(chalk3.dim(`Provisioning new workspace ${workspaceId}...`));
328
334
  }
329
335
  const region = opts.ingest ? void 0 : opts.region.toLowerCase();
330
336
  if (isNewWorkspace) {
331
337
  const result = await provisionDsn(keyId, secret, workspaceId, ingestHost, region);
332
338
  if (!result.success) {
333
- console.error(chalk.red(`Failed to provision DSN: ${result.error}`));
339
+ console.error(chalk3.red(`Failed to provision DSN: ${result.error}`));
334
340
  process.exit(1);
335
341
  }
336
342
  }
337
343
  const dsn = buildDsn(keyId, secret, ingestHost, workspaceId);
338
- console.log(chalk.green("\u2714") + " DSN created");
344
+ console.log(chalk3.green("\u2714") + " DSN created");
339
345
  console.log("");
340
- console.log(chalk.bold("Region:"));
346
+ console.log(chalk3.bold("Region:"));
341
347
  console.log(` ${region ?? "custom"} (${ingestHost})`);
342
348
  console.log("");
343
- console.log(chalk.bold("DSN:"));
349
+ console.log(chalk3.bold("DSN:"));
344
350
  console.log(` ${dsn}`);
345
351
  console.log("");
346
- console.log(chalk.bold("Key ID:"));
352
+ console.log(chalk3.bold("Key ID:"));
347
353
  console.log(` ${keyId}`);
348
354
  if (isNewWorkspace) {
349
355
  console.log("");
350
- console.log(chalk.bold("Workspace ID:"));
356
+ console.log(chalk3.bold("Workspace ID:"));
351
357
  console.log(` ${workspaceId}`);
352
358
  }
353
359
  console.log("");
354
- console.log(chalk.dim("Set this as VIBGRATE_DSN in your CI environment."));
360
+ console.log(chalk3.dim("Set this as VIBGRATE_DSN in your CI environment."));
355
361
  if (!isNewWorkspace) {
356
- console.log(chalk.dim("The secret must be registered on your Vibgrate ingest API."));
362
+ console.log(chalk3.dim("The secret must be registered on your Vibgrate ingest API."));
357
363
  }
358
364
  if (opts.write) {
359
365
  const writePath = path4.resolve(opts.write);
360
366
  await writeTextFile(writePath, dsn + "\n");
361
367
  console.log("");
362
- console.log(chalk.green("\u2714") + ` DSN written to ${opts.write}`);
368
+ console.log(chalk3.green("\u2714") + ` DSN written to ${opts.write}`);
363
369
  const root = findGitRoot(path4.dirname(writePath));
364
370
  const rel = root ? path4.relative(root, writePath).split(path4.sep).join("/") : "";
365
371
  if (root && rel && !rel.startsWith("..")) {
366
372
  const res = ensureGitignored(rel, path4.dirname(writePath));
367
373
  if (res.status === "created") {
368
- console.log(chalk.green("\u2714") + ` Created .gitignore and ignored ${rel}`);
374
+ console.log(chalk3.green("\u2714") + ` Created .gitignore and ignored ${rel}`);
369
375
  } else if (res.status === "added") {
370
- console.log(chalk.green("\u2714") + ` Added ${rel} to .gitignore`);
376
+ console.log(chalk3.green("\u2714") + ` Added ${rel} to .gitignore`);
371
377
  } else if (res.status === "present") {
372
- console.log(chalk.dim(` ${rel} is already in .gitignore`));
378
+ console.log(chalk3.dim(` ${rel} is already in .gitignore`));
373
379
  }
374
380
  } else {
375
381
  console.log(
376
- chalk.yellow("\u26A0") + " Add this file to .gitignore to keep the DSN out of version control."
382
+ chalk3.yellow("\u26A0") + " Add this file to .gitignore to keep the DSN out of version control."
377
383
  );
378
384
  }
379
385
  }
@@ -754,7 +760,7 @@ async function uploadScanArtifact(input) {
754
760
  const target = await regionRedirectHost(response);
755
761
  if (target && target !== host) {
756
762
  console.log(
757
- chalk.yellow(
763
+ chalk3.yellow(
758
764
  `\u21BB Workspace is pinned to a different region \u2014 retrying upload to ${target}...`
759
765
  )
760
766
  );
@@ -764,6 +770,125 @@ async function uploadScanArtifact(input) {
764
770
  }
765
771
  return { response, host };
766
772
  }
773
+ var CAPSULE_SCHEMA = "vg.analysis.capsule.v1";
774
+ var FINDINGS_SCHEMA = "vg.review.findings.v1";
775
+ var RECEIPT_SCHEMA = "vg.review.receipt.v1";
776
+ var REVIEW_POLICY_VERSION = "vg-policy-0.1.0";
777
+ var CAPSULE_BUDGETS = {
778
+ "interactive-narrow": { median: 3e3, cap: 16e3 },
779
+ "ci-wide": { median: 1e4, cap: 16e3 }
780
+ };
781
+ var MAX_ROLE_PEERS = 5;
782
+ var TARGET_ALIGNMENTS = [
783
+ "regression",
784
+ "target",
785
+ "legacy_consistent",
786
+ "approved_exception",
787
+ "unknown"
788
+ ];
789
+ function digest(value) {
790
+ return `sha256:${bytesToHex(sha256(new TextEncoder().encode(canonicalize(value))))}`;
791
+ }
792
+ function digestString(input) {
793
+ return `sha256:${bytesToHex(sha256(new TextEncoder().encode(input)))}`;
794
+ }
795
+ function receiptDigest(receipt) {
796
+ const { digests, signature: _signature, ...rest } = receipt;
797
+ const { receipt: _self, ...digestsWithoutSelf } = digests;
798
+ return digest({ ...rest, digests: digestsWithoutSelf });
799
+ }
800
+ function receiptId(createdAtMs, entropy) {
801
+ const alphabet = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
802
+ let time = "";
803
+ let ms = Math.max(0, Math.floor(createdAtMs));
804
+ for (let i = 0; i < 10; i++) {
805
+ time = alphabet[ms % 32] + time;
806
+ ms = Math.floor(ms / 32);
807
+ }
808
+ const tail = bytesToHex(sha256(new TextEncoder().encode(entropy))).slice(0, 16).toUpperCase().replace(/[^0-9A-HJKMNP-TV-Z]/g, "0");
809
+ return `rvw_${time}${tail}`;
810
+ }
811
+
812
+ // src/review/push.ts
813
+ var MAX_SNIPPET_LINES = 12;
814
+ var MAX_SNIPPET_FINDINGS = 20;
815
+ function rejectPushWhenOffline(offline) {
816
+ if (!offline) return;
817
+ throw new CliError(
818
+ "`--push` needs the network \u2014 drop `--offline` (or `--local`) to upload the receipt, or drop `--push` to keep it on disk",
819
+ ExitCode.USAGE_ERROR
820
+ );
821
+ }
822
+ function buildEnvelope(receipt, opts = { pushedAt: (/* @__PURE__ */ new Date()).toISOString() }) {
823
+ return {
824
+ kind: "review",
825
+ schema_version: RECEIPT_SCHEMA,
826
+ workspace_id: opts.workspaceId ?? receipt.workspace_id ?? null,
827
+ pushed_at: opts.pushedAt,
828
+ cli_version: VERSION,
829
+ receipt
830
+ };
831
+ }
832
+ function collectSpans(root, receipt, capsule, privacy) {
833
+ if (!privacy.includeSpans && !privacy.includeSnippets) return void 0;
834
+ const citedIds = new Set(
835
+ [...receipt.findings.architecture_findings, ...receipt.findings.security_findings].slice(0, MAX_SNIPPET_FINDINGS).flatMap((f) => f.evidence_ids)
836
+ );
837
+ const spans = [];
838
+ for (const e of capsule.evidence) {
839
+ if (!citedIds.has(e.id)) continue;
840
+ if (!e.path || e.start_line === void 0 || e.end_line === void 0) continue;
841
+ const ref = {
842
+ evidence_id: e.id,
843
+ path: e.path,
844
+ start_line: e.start_line,
845
+ end_line: e.end_line
846
+ };
847
+ if (privacy.includeSnippets) {
848
+ const snippet = readSnippet(root, e.path, e.start_line, e.end_line);
849
+ if (snippet) ref.snippet = snippet;
850
+ }
851
+ spans.push(ref);
852
+ }
853
+ return spans;
854
+ }
855
+ function readSnippet(root, rel, start, end) {
856
+ try {
857
+ const abs = path4.join(root, rel);
858
+ if (!fs.existsSync(abs)) return null;
859
+ const lines = fs.readFileSync(abs, "utf8").split("\n");
860
+ return lines.slice(Math.max(0, start - 1), Math.min(lines.length, start - 1 + MAX_SNIPPET_LINES, end)).join("\n");
861
+ } catch {
862
+ return null;
863
+ }
864
+ }
865
+ async function pushReceipt(dsn, body, fetchImpl = fetch) {
866
+ const url = `${dsn.scheme}://${dsn.host}/v1/ingest/review`;
867
+ const payload = JSON.stringify(body);
868
+ let res;
869
+ try {
870
+ res = await fetchImpl(url, {
871
+ method: "POST",
872
+ headers: {
873
+ "Content-Type": "application/json",
874
+ "X-Vibgrate-Timestamp": String(Date.now()),
875
+ Authorization: `VibgrateDSN ${dsn.keyId}:${dsn.secret}`
876
+ },
877
+ body: payload,
878
+ signal: AbortSignal.timeout(3e4)
879
+ });
880
+ } catch (e) {
881
+ throw new CliError(
882
+ `could not reach ${dsn.host}: ${e instanceof Error ? e.message : String(e)}`,
883
+ ExitCode.ERROR
884
+ );
885
+ }
886
+ if (!res.ok) {
887
+ const detail = await res.text().catch(() => "");
888
+ return { ok: false, status: res.status, host: dsn.host, detail: detail.slice(0, 200) };
889
+ }
890
+ return { ok: true, status: res.status, host: dsn.host };
891
+ }
767
892
 
768
893
  // src/reporting/commands/push.ts
769
894
  function parseDsn(dsn) {
@@ -778,29 +903,48 @@ function parseDsn(dsn) {
778
903
  workspaceId: match[5]
779
904
  };
780
905
  }
781
- var pushCommand = new Command("push").description("Push scan results to Vibgrate API").option("--dsn <dsn>", "DSN token (or use VIBGRATE_DSN env)").option("--region <region>", `Override data residency region (${availableRegionIds().join(", ")})`).option("--file <file>", "Scan artifact file", ".vibgrate/scan_result.json").option("--strict", "Fail on upload errors").action(async (opts) => {
906
+ var pushCommand = new Command("push").description("Push a scan artifact \u2014 or a `vg review` receipt \u2014 to Vibgrate Cloud").option("--dsn <dsn>", "DSN token (or use VIBGRATE_DSN env)").option("--region <region>", `Override data residency region (${availableRegionIds().join(", ")})`).option("--file <file>", "Scan artifact, or a vg.review.receipt.v1 file (auto-detected)", ".vibgrate/scan_result.json").option("--strict", "Fail on upload errors").action(async (opts) => {
782
907
  const dsn = resolveDsn(opts.dsn);
783
908
  if (!dsn) {
784
- console.error(chalk.red("No DSN provided."));
785
- console.error(chalk.dim('Run "vibgrate login", set VIBGRATE_DSN, or use the --dsn flag.'));
909
+ console.error(chalk3.red("No DSN provided."));
910
+ console.error(chalk3.dim('Run "vibgrate login", set VIBGRATE_DSN, or use the --dsn flag.'));
786
911
  if (opts.strict) process.exit(1);
787
912
  return;
788
913
  }
789
914
  const parsed = parseDsn(dsn);
790
915
  if (!parsed) {
791
- console.error(chalk.red("Invalid DSN format."));
792
- console.error(chalk.dim("Expected: vibgrate+https://<key_id>:<secret>@<host>/<workspace_id>"));
916
+ console.error(chalk3.red("Invalid DSN format."));
917
+ console.error(chalk3.dim("Expected: vibgrate+https://<key_id>:<secret>@<host>/<workspace_id>"));
793
918
  if (opts.strict) process.exit(1);
794
919
  return;
795
920
  }
796
921
  const filePath = path4.resolve(opts.file);
797
922
  if (!await pathExists(filePath)) {
798
- console.error(chalk.red(`Scan artifact not found: ${filePath}`));
799
- console.error(chalk.dim('Run "vibgrate scan" first.'));
923
+ console.error(chalk3.red(`Artifact not found: ${filePath}`));
924
+ console.error(chalk3.dim('Run "vg scan" (or "vg review --out <file>") first.'));
800
925
  if (opts.strict) process.exit(1);
801
926
  return;
802
927
  }
803
- const artifact = await readJsonFile(filePath);
928
+ const document = await readJsonFile(filePath);
929
+ if (document?.schema_version === RECEIPT_SCHEMA) {
930
+ const receipt = document;
931
+ const envelope = buildEnvelope(receipt, {
932
+ workspaceId: parsed.workspaceId,
933
+ pushedAt: (/* @__PURE__ */ new Date()).toISOString()
934
+ });
935
+ console.log(
936
+ chalk3.dim(`Uploading review receipt (${receipt.decision}) for ${receipt.git.head_sha.slice(0, 8)}...`)
937
+ );
938
+ const result = await pushReceipt(parsed, envelope);
939
+ if (!result.ok) {
940
+ console.error(chalk3.red(`Upload failed (${result.status}): ${result.detail ?? ""}`));
941
+ if (opts.strict) process.exit(1);
942
+ return;
943
+ }
944
+ console.log(chalk3.green("\u2714") + ` Review receipt pushed to ${result.host}`);
945
+ return;
946
+ }
947
+ const artifact = document;
804
948
  const config = await loadConfig(process.cwd());
805
949
  const databaseSchemaCaps = config.scanners !== false ? config.scanners?.databaseSchema : void 0;
806
950
  const { body, contentEncoding } = await prepareCompressedUpload(artifact, { databaseSchemaCaps });
@@ -810,7 +954,7 @@ var pushCommand = new Command("push").description("Push scan results to Vibgrate
810
954
  try {
811
955
  host = resolveIngestHost(opts.region);
812
956
  } catch (e) {
813
- console.error(chalk.red(e instanceof Error ? e.message : String(e)));
957
+ console.error(chalk3.red(e instanceof Error ? e.message : String(e)));
814
958
  if (opts.strict) process.exit(1);
815
959
  return;
816
960
  }
@@ -818,7 +962,7 @@ var pushCommand = new Command("push").description("Push scan results to Vibgrate
818
962
  const originalSize = JSON.stringify(artifact).length;
819
963
  const compressedSize = body.length;
820
964
  const ratio = ((1 - compressedSize / originalSize) * 100).toFixed(0);
821
- console.log(chalk.dim(`Uploading to ${host}... (${(compressedSize / 1024).toFixed(0)} KB, ${ratio}% smaller)`));
965
+ console.log(chalk3.dim(`Uploading to ${host}... (${(compressedSize / 1024).toFixed(0)} KB, ${ratio}% smaller)`));
822
966
  try {
823
967
  const { response, host: uploadedHost } = await uploadScanArtifact({
824
968
  scheme: parsed.scheme,
@@ -839,18 +983,18 @@ var pushCommand = new Command("push").description("Push scan results to Vibgrate
839
983
  throw new Error(`HTTP ${response.status}: ${text}`);
840
984
  }
841
985
  const result = await response.json();
842
- console.log(chalk.green("\u2714") + ` Scan queued for processing (${result.ingestId ?? "ok"})`);
986
+ console.log(chalk3.green("\u2714") + ` Scan queued for processing (${result.ingestId ?? "ok"})`);
843
987
  console.log();
844
- console.log(chalk.dim("Processing continues in the background. Results available shortly."));
988
+ console.log(chalk3.dim("Processing continues in the background. Results available shortly."));
845
989
  console.log();
846
990
  if (result.ingestId) {
847
991
  const dashHost = dashHostForIngestHost(host);
848
992
  const reportUrl = `https://${dashHost}/${parsed.workspaceId}/scan/${result.ingestId}`;
849
- console.log(chalk.dim("View report: ") + chalk.underline(reportUrl));
993
+ console.log(chalk3.dim("View report: ") + chalk3.underline(reportUrl));
850
994
  }
851
995
  } catch (e) {
852
996
  const msg = e instanceof Error ? e.message : String(e);
853
- console.error(chalk.red(`Upload failed: ${msg}`));
997
+ console.error(chalk3.red(`Upload failed: ${msg}`));
854
998
  if (opts.strict) process.exit(1);
855
999
  }
856
1000
  });
@@ -933,6 +1077,1015 @@ async function attributedInventory(root, opts = {}) {
933
1077
  });
934
1078
  return { ...inv, records, attribution: "git" };
935
1079
  }
1080
+
1081
+ // src/review/auth.ts
1082
+ var MIN_SECURITY_PEERS = 4;
1083
+ var AUTH_DOMINANCE_SHARE = 0.75;
1084
+ var MUTATING_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE", "ALL"]);
1085
+ var GUARD_CALL = /\b(authorize|authorise|authenticate|requireAuth|requireUser|requireLogin|requireRole|requireScope|requirePermission|ensureAuth|ensureLoggedIn|checkPermission|checkAccess|hasPermission|hasRole|verifyToken|verifyJwt|verifySession|assertScope|assertPermission|isAuthenticated|withAuth|protect|guard)\b/i;
1086
+ var GUARD_ATTRIBUTE = /(\[\s*Authorize\b)|(@(?:PreAuthorize|PostAuthorize|Secured|RolesAllowed|UseGuards|login_required|permission_required|requires_auth|authenticated))\b/;
1087
+ var GUARD_MIDDLEWARE = /\b(passport|jwt|oauth|session|clerk|auth0|nextAuth|authMiddleware|isAuth)\b/i;
1088
+ var PUBLIC_MARKER = /(@vibgrate-public|@public\b|\[\s*AllowAnonymous\s*\]|@AnonymousAllowed|permit_all|public\s*:\s*true)/i;
1089
+ function looksLikeGuard(expression) {
1090
+ return GUARD_CALL.test(expression) || GUARD_ATTRIBUTE.test(expression) || GUARD_MIDDLEWARE.test(expression);
1091
+ }
1092
+ function classifyRoute(route) {
1093
+ const decide = (verdict, rule) => ({ ...route, verdict, rule });
1094
+ if (route.explicitlyPublic) return decide("not-auth", "explicit-public-marker");
1095
+ if (route.unreadable) return decide("unsure", "unreadable");
1096
+ if (route.inlineGuards.some(looksLikeGuard)) return decide("auth", "inline-guard");
1097
+ if (route.inheritedGuards.some(looksLikeGuard)) return decide("auth", "inherited-guard");
1098
+ if (route.inlineGuards.length > 0 || route.inheritedGuards.length > 0) {
1099
+ return decide("unsure", "unrecognised-guard-expression");
1100
+ }
1101
+ return decide("not-auth", "no-guard-in-scope");
1102
+ }
1103
+ function voteAuth(group, routes) {
1104
+ const mutating = routes.filter((r) => MUTATING_METHODS.has(r.method.toUpperCase()));
1105
+ const unsure = mutating.filter((r) => r.verdict === "unsure");
1106
+ const decided = mutating.filter((r) => r.verdict !== "unsure");
1107
+ const guarded = decided.filter((r) => r.verdict === "auth");
1108
+ const share = decided.length > 0 ? guarded.length / decided.length : 0;
1109
+ const aboveFloor = decided.length >= MIN_SECURITY_PEERS;
1110
+ const dominantlyGuarded = share >= AUTH_DOMINANCE_SHARE && guarded.length > 0;
1111
+ return {
1112
+ group,
1113
+ classified: decided.length,
1114
+ guarded: guarded.length,
1115
+ share,
1116
+ aboveFloor,
1117
+ deviators: dominantlyGuarded ? decided.filter((r) => r.verdict === "not-auth") : [],
1118
+ unsure
1119
+ };
1120
+ }
1121
+ function voteAllAuth(routes) {
1122
+ const groups = /* @__PURE__ */ new Map();
1123
+ for (const r of routes) {
1124
+ const dir = r.file.split("/").slice(0, -1).join("/") || ".";
1125
+ let bucket = groups.get(dir);
1126
+ if (!bucket) groups.set(dir, bucket = []);
1127
+ bucket.push(r);
1128
+ }
1129
+ return [...groups.entries()].sort((a, b) => a[0] < b[0] ? -1 : 1).map(([dir, rs]) => voteAuth(dir, rs));
1130
+ }
1131
+
1132
+ // src/review/dimensions.ts
1133
+ var EXEMPT = /* @__PURE__ */ new Set(["config", "shared", "testing"]);
1134
+ var PERSISTENCE = /* @__PURE__ */ new Set(["data-access", "infrastructure"]);
1135
+ function classifyDataAccess(fileLayer, dependsOnLayers) {
1136
+ if (PERSISTENCE.has(fileLayer) || EXEMPT.has(fileLayer)) return null;
1137
+ const targets = dependsOnLayers.filter((l) => !EXEMPT.has(l) && l !== fileLayer);
1138
+ if (targets.some((l) => PERSISTENCE.has(l))) return "direct-persistence";
1139
+ if (targets.includes("services")) return "via-service";
1140
+ if (targets.includes("domain")) return "via-domain";
1141
+ return "no-data-access";
1142
+ }
1143
+ function isRegression(actual, dominant) {
1144
+ if (actual !== "direct-persistence") return false;
1145
+ return dominant !== null && dominant !== "direct-persistence";
1146
+ }
1147
+ function describePattern(pattern) {
1148
+ switch (pattern) {
1149
+ case "direct-persistence":
1150
+ return "reaches persistence directly";
1151
+ case "via-service":
1152
+ return "goes through the service layer";
1153
+ case "via-domain":
1154
+ return "touches only domain types";
1155
+ case "no-data-access":
1156
+ return "does not reach data";
1157
+ default:
1158
+ return pattern;
1159
+ }
1160
+ }
1161
+
1162
+ // src/review/routes.ts
1163
+ var JS_ROUTE = /\b(?:app|router|api|server|r)\s*\.\s*(get|post|put|patch|delete|all|use)\s*\(\s*(['"`])([^'"`]*)\2\s*(,[^)]*)?/i;
1164
+ var DECORATOR_ROUTE = /@(?:app\.|router\.)?(Get|Post|Put|Patch|Delete|RequestMapping|GetMapping|PostMapping|PutMapping|DeleteMapping|route)\s*\(\s*(['"`])?([^'"`)]*)\2?/i;
1165
+ var DOTNET_ROUTE = /\[\s*Http(Get|Post|Put|Patch|Delete)\s*(?:\(\s*"([^"]*)"\s*\))?\s*\]/i;
1166
+ var GO_ROUTE = /\b\w+\s*\.\s*(?:(GET|POST|PUT|PATCH|DELETE)|Handle(?:Func)?)\s*\(\s*"([^"]*)"\s*(,[^)]*)?/;
1167
+ var SCOPE_GUARD = /\b(?:use|Use|UseMiddleware|beforeEach|before_request|AddAuthorization|RequireAuthorization)\s*\(/;
1168
+ var CLASS_DECLARATION = /\b(?:public\s+|export\s+|abstract\s+)*class\s+\w/;
1169
+ var INHERIT_WINDOW = 12;
1170
+ var SUPPORTED = /\.(ts|tsx|js|jsx|mjs|cjs|mts|cts|cs|py|go|java|kt|rb|php)$/i;
1171
+ function methodOf(raw) {
1172
+ const m = raw.toUpperCase().replace(/MAPPING$/, "").replace(/^REQUEST$/, "ALL");
1173
+ return m === "ROUTE" || m === "HANDLE" || m === "HANDLEFUNC" ? "ALL" : m;
1174
+ }
1175
+ function extractRoutes(file, text) {
1176
+ if (!SUPPORTED.test(file)) return [];
1177
+ const lines = text.split("\n");
1178
+ const routes = [];
1179
+ const classGuards = [];
1180
+ for (let i = 0; i < lines.length; i++) {
1181
+ if (!CLASS_DECLARATION.test(lines[i])) continue;
1182
+ for (let j = i - 1; j >= 0; j--) {
1183
+ const above = lines[j].trim();
1184
+ if (!above.startsWith("@") && !above.startsWith("[")) break;
1185
+ if (looksLikeGuard(above)) classGuards.push(above);
1186
+ }
1187
+ }
1188
+ const scopeGuards = [];
1189
+ for (let i = 0; i < lines.length; i++) {
1190
+ const line = lines[i];
1191
+ if (SCOPE_GUARD.test(line)) scopeGuards.push({ line: i, text: line.trim() });
1192
+ const js = JS_ROUTE.exec(line);
1193
+ const decorator = js ? null : DECORATOR_ROUTE.exec(line);
1194
+ const dotnet = js || decorator ? null : DOTNET_ROUTE.exec(line);
1195
+ const go = js || decorator || dotnet ? null : GO_ROUTE.exec(line);
1196
+ if (!js && !decorator && !dotnet && !go) continue;
1197
+ let method;
1198
+ let routePath;
1199
+ let inlineTail = "";
1200
+ if (js) {
1201
+ if (js[1].toLowerCase() === "use") continue;
1202
+ method = methodOf(js[1]);
1203
+ routePath = js[3] ?? "";
1204
+ inlineTail = js[4] ?? "";
1205
+ } else if (decorator) {
1206
+ method = methodOf(decorator[1]);
1207
+ routePath = decorator[3] ?? "";
1208
+ inlineTail = looksLikeGuard(line) ? line : "";
1209
+ } else if (dotnet) {
1210
+ method = methodOf(dotnet[1]);
1211
+ routePath = dotnet[2] ?? "";
1212
+ inlineTail = looksLikeGuard(line) ? line : "";
1213
+ } else {
1214
+ method = methodOf(go[1] ?? "ALL");
1215
+ routePath = go[2] ?? "";
1216
+ inlineTail = go[3] ?? "";
1217
+ }
1218
+ const attached = [];
1219
+ for (let j = i - 1; j >= 0; j--) {
1220
+ const above = lines[j].trim();
1221
+ if (above.startsWith("@") || above.startsWith("[")) attached.unshift(above);
1222
+ else break;
1223
+ }
1224
+ const tailArgs = inlineTail.replace(/^\s*,/, "").split(",").map((a) => a.trim()).filter((a) => a.length > 0);
1225
+ const middlewareArgs = tailArgs.slice(0, -1);
1226
+ const inline = [...middlewareArgs, ...attached].filter((t) => t.trim().length > 0);
1227
+ routes.push({
1228
+ path: routePath,
1229
+ file,
1230
+ line: i + 1,
1231
+ method,
1232
+ inlineGuards: inline,
1233
+ inheritedGuards: [
1234
+ ...classGuards,
1235
+ ...scopeGuards.filter((g) => i - g.line <= INHERIT_WINDOW || g.line < 20).map((g) => g.text)
1236
+ ],
1237
+ explicitlyPublic: [line, ...attached].some((t) => PUBLIC_MARKER.test(t)),
1238
+ unreadable: false
1239
+ });
1240
+ }
1241
+ return routes;
1242
+ }
1243
+ function looksLikeRouteFile(text) {
1244
+ return /\b(app|router|route|controller|handler|endpoint|@Get|@Post|HttpGet|HttpPost|HandleFunc)\b/i.test(
1245
+ text
1246
+ );
1247
+ }
1248
+ function routesForFile(file, text) {
1249
+ const routes = extractRoutes(file, text);
1250
+ if (routes.length > 0) return routes;
1251
+ if (!SUPPORTED.test(file) || !looksLikeRouteFile(text)) return [];
1252
+ return [
1253
+ {
1254
+ path: "",
1255
+ file,
1256
+ line: 1,
1257
+ method: "ALL",
1258
+ inlineGuards: [],
1259
+ inheritedGuards: [],
1260
+ explicitlyPublic: false,
1261
+ unreadable: true
1262
+ }
1263
+ ];
1264
+ }
1265
+
1266
+ // src/review/similarity.ts
1267
+ var SHINGLE_SIZE = 5;
1268
+ var MINHASH_PERMUTATIONS = 128;
1269
+ var LSH_BANDS = 16;
1270
+ var LSH_ROWS = 8;
1271
+ var LCS_THRESHOLD = 0.6;
1272
+ var MAX_CANDIDATES = 20;
1273
+ var MIN_CALL_OVERLAP = 0.8;
1274
+ var MIN_TOKENS = 12;
1275
+ var PUNCTUATION = /^[{}()[\];,.]$|^=>$|^[=!<>]=+$|^[+\-*/%&|^!<>?:]+$/;
1276
+ function hasSubstance(tokens) {
1277
+ const meaningful = tokens.filter((t) => !PUNCTUATION.test(t));
1278
+ if (meaningful.length < MIN_TOKENS) return false;
1279
+ const BRANCHING = /* @__PURE__ */ new Set(["if", "else", "for", "while", "do", "switch", "case", "try", "catch", "elif"]);
1280
+ const branches = meaningful.filter((t) => BRANCHING.has(t)).length;
1281
+ const distinctCalls = new Set(meaningful.filter((t) => t.startsWith("CALL:"))).size;
1282
+ return branches > 0 || distinctCalls > 1;
1283
+ }
1284
+ function normalizeTokens(text) {
1285
+ const withoutComments = text.replace(/\/\*[\s\S]*?\*\//g, " ").replace(/\/\/[^\n]*/g, " ").replace(/#[^\n]*/g, " ");
1286
+ const tokens = [];
1287
+ const pattern = /([A-Za-z_$][\w$]*)\s*\(|([A-Za-z_$][\w$]*)|(["'`])(?:\\.|(?!\3)[^\\])*\3|(\d[\d._]*)|([{}()[\];,.]|=>|[=!<>]=+|[+\-*/%&|^!<>?:]+)/g;
1288
+ const KEYWORDS = /* @__PURE__ */ new Set([
1289
+ "if",
1290
+ "else",
1291
+ "for",
1292
+ "while",
1293
+ "do",
1294
+ "switch",
1295
+ "case",
1296
+ "return",
1297
+ "try",
1298
+ "catch",
1299
+ "finally",
1300
+ "throw",
1301
+ "await",
1302
+ "async",
1303
+ "function",
1304
+ "const",
1305
+ "let",
1306
+ "var",
1307
+ "new",
1308
+ "class",
1309
+ "def",
1310
+ "elif",
1311
+ "except",
1312
+ "raise",
1313
+ "yield",
1314
+ "match",
1315
+ "foreach",
1316
+ "using"
1317
+ ]);
1318
+ const DECLARATORS = /* @__PURE__ */ new Set(["function", "def", "class", "fn", "func", "sub", "method"]);
1319
+ let m;
1320
+ while ((m = pattern.exec(withoutComments)) !== null) {
1321
+ const previous = tokens[tokens.length - 1];
1322
+ if (m[1] !== void 0) {
1323
+ if (KEYWORDS.has(m[1])) tokens.push(m[1]);
1324
+ else if (previous !== void 0 && DECLARATORS.has(previous)) tokens.push("DECL");
1325
+ else tokens.push(`CALL:${m[1]}`);
1326
+ } else if (m[2] !== void 0) {
1327
+ tokens.push(KEYWORDS.has(m[2]) ? m[2] : "ID");
1328
+ } else if (m[3] !== void 0) {
1329
+ tokens.push("STR");
1330
+ } else if (m[4] !== void 0) {
1331
+ tokens.push("NUM");
1332
+ } else if (m[5] !== void 0) {
1333
+ tokens.push(m[5]);
1334
+ }
1335
+ }
1336
+ return tokens;
1337
+ }
1338
+ function shingles(tokens, k = SHINGLE_SIZE) {
1339
+ if (tokens.length < k) return tokens.length > 0 ? [tokens.join(" ")] : [];
1340
+ const out = [];
1341
+ for (let i = 0; i + k <= tokens.length; i++) out.push(tokens.slice(i, i + k).join(" "));
1342
+ return out;
1343
+ }
1344
+ function fnv1a(input, seed) {
1345
+ let h = (2166136261 ^ seed) >>> 0;
1346
+ for (let i = 0; i < input.length; i++) {
1347
+ h ^= input.charCodeAt(i);
1348
+ h = Math.imul(h, 16777619) >>> 0;
1349
+ }
1350
+ return h >>> 0;
1351
+ }
1352
+ function minhash(shingleSet, permutations = MINHASH_PERMUTATIONS) {
1353
+ const signature = new Array(permutations).fill(4294967295);
1354
+ for (const shingle of shingleSet) {
1355
+ for (let p = 0; p < permutations; p++) {
1356
+ const h = fnv1a(shingle, p);
1357
+ if (h < signature[p]) signature[p] = h;
1358
+ }
1359
+ }
1360
+ return signature;
1361
+ }
1362
+ function callVocabulary(tokens) {
1363
+ return new Set(tokens.filter((t) => t.startsWith("CALL:")));
1364
+ }
1365
+ function callOverlap(a, b) {
1366
+ const x = callVocabulary(a);
1367
+ const y = callVocabulary(b);
1368
+ if (x.size === 0 && y.size === 0) return 0;
1369
+ let shared = 0;
1370
+ for (const call of x) if (y.has(call)) shared++;
1371
+ return shared / (/* @__PURE__ */ new Set([...x, ...y])).size;
1372
+ }
1373
+ function jaccard(a, b) {
1374
+ if (a.length === 0 || a.length !== b.length) return 0;
1375
+ let same = 0;
1376
+ for (let i = 0; i < a.length; i++) if (a[i] === b[i]) same++;
1377
+ return same / a.length;
1378
+ }
1379
+ function bandKeys(signature, bands = LSH_BANDS, rows = LSH_ROWS) {
1380
+ const keys = [];
1381
+ for (let b = 0; b < bands; b++) {
1382
+ const slice = signature.slice(b * rows, (b + 1) * rows);
1383
+ if (slice.length === 0) break;
1384
+ keys.push(`${b}:${fnv1a(slice.join(","), 0)}`);
1385
+ }
1386
+ return keys;
1387
+ }
1388
+ function lcsRatio(a, b, cap = 400) {
1389
+ const x = a.slice(0, cap);
1390
+ const y = b.slice(0, cap);
1391
+ if (x.length === 0 || y.length === 0) return 0;
1392
+ let previous = new Array(y.length + 1).fill(0);
1393
+ let current = new Array(y.length + 1).fill(0);
1394
+ for (let i = 1; i <= x.length; i++) {
1395
+ for (let j = 1; j <= y.length; j++) {
1396
+ current[j] = x[i - 1] === y[j - 1] ? previous[j - 1] + 1 : Math.max(previous[j], current[j - 1]);
1397
+ }
1398
+ [previous, current] = [current, previous];
1399
+ current.fill(0);
1400
+ }
1401
+ return previous[y.length] / Math.max(x.length, y.length);
1402
+ }
1403
+ var SimilarityIndex = class {
1404
+ bodies = /* @__PURE__ */ new Map();
1405
+ bands = /* @__PURE__ */ new Map();
1406
+ add(body) {
1407
+ const tokens = normalizeTokens(body.text);
1408
+ if (!hasSubstance(tokens)) return;
1409
+ const signature = minhash(shingles(tokens));
1410
+ const indexed = { ...body, tokens, signature };
1411
+ this.bodies.set(body.id, indexed);
1412
+ for (const key of bandKeys(signature)) {
1413
+ let bucket = this.bands.get(key);
1414
+ if (!bucket) this.bands.set(key, bucket = []);
1415
+ bucket.push(body.id);
1416
+ }
1417
+ }
1418
+ get size() {
1419
+ return this.bodies.size;
1420
+ }
1421
+ /**
1422
+ * Bodies similar to `query`, best first. `excludeIds` drops the query's own
1423
+ * entry (and anything else the caller knows is the same thing).
1424
+ */
1425
+ find(query, excludeIds = /* @__PURE__ */ new Set()) {
1426
+ const tokens = normalizeTokens(query.text);
1427
+ if (!hasSubstance(tokens)) return [];
1428
+ const signature = minhash(shingles(tokens));
1429
+ const candidateIds = /* @__PURE__ */ new Set();
1430
+ for (const key of bandKeys(signature)) {
1431
+ for (const id of this.bands.get(key) ?? []) {
1432
+ if (id !== query.id && !excludeIds.has(id)) candidateIds.add(id);
1433
+ }
1434
+ }
1435
+ const ranked = [...candidateIds].map((id) => this.bodies.get(id)).sort((a, b) => jaccard(signature, b.signature) - jaccard(signature, a.signature)).slice(0, MAX_CANDIDATES);
1436
+ const hits = [];
1437
+ for (const candidate of ranked) {
1438
+ const score = lcsRatio(tokens, candidate.tokens);
1439
+ if (score < LCS_THRESHOLD) continue;
1440
+ if (callOverlap(tokens, candidate.tokens) < MIN_CALL_OVERLAP) continue;
1441
+ hits.push({
1442
+ id: candidate.id,
1443
+ name: candidate.name,
1444
+ file: candidate.file,
1445
+ startLine: candidate.startLine,
1446
+ endLine: candidate.endLine,
1447
+ score
1448
+ });
1449
+ }
1450
+ return hits.sort((a, b) => b.score - a.score || (a.file < b.file ? -1 : 1));
1451
+ }
1452
+ };
1453
+ function isComparable(file) {
1454
+ const p = file.replace(/\\/g, "/");
1455
+ if (/(^|\/)(__tests__|__mocks__|tests?|spec|fixtures?|vendor|node_modules|dist|build|generated)\//i.test(p)) {
1456
+ return false;
1457
+ }
1458
+ return !/\.(test|spec|d)\.[^.]+$|\.min\.[^.]+$|\.generated\.[^.]+$/i.test(p);
1459
+ }
1460
+
1461
+ // src/review/assess.ts
1462
+ function assessChange(input) {
1463
+ const conflicts = [];
1464
+ const unknowns = [];
1465
+ const haveGraph = input.similarity !== null && input.votes.length > 0;
1466
+ if (!haveGraph) {
1467
+ unknowns.push("No code map was available \u2014 run `vg` to build one so this can compare against the repository.");
1468
+ }
1469
+ const vote = input.group ? input.votes.find((v) => v.group === input.group) : input.votes.find((v) => v.deviators.includes(input.file) || v.exemplars.includes(input.file));
1470
+ const convention = vote && vote.reason === "dominant" && vote.dominant ? {
1471
+ group: vote.group,
1472
+ pattern: vote.dominant,
1473
+ share: vote.share,
1474
+ exemplars: vote.exemplars
1475
+ } : null;
1476
+ if (vote && vote.reason === "no_convention") {
1477
+ unknowns.push(
1478
+ `${vote.group} has no convention \u2014 its peers are split, so there is no house style to match. Pick the shape that matches the declared target.`
1479
+ );
1480
+ }
1481
+ const proposedPattern = inferProposedPattern(input.content, input.dataAccess);
1482
+ if (convention && proposedPattern) {
1483
+ if (isRegression(proposedPattern, convention.pattern)) {
1484
+ conflicts.push({
1485
+ kind: "peer_deviation",
1486
+ severity: input.declaredTarget ? "high" : "medium",
1487
+ message: `This ${describePattern(proposedPattern)}, while ${(convention.share * 100).toFixed(0)}% of its peers in ${convention.group} ${describePattern(convention.pattern)}.`,
1488
+ referenceFiles: convention.exemplars.slice(0, 3),
1489
+ protectedRule: false
1490
+ });
1491
+ }
1492
+ } else if (!convention && proposedPattern === "direct-persistence") {
1493
+ unknowns.push(
1494
+ "No peer convention could be established for this file, so reaching persistence directly is neither confirmed as normal here nor flagged as a deviation."
1495
+ );
1496
+ }
1497
+ for (const route of routesForFile(input.file, input.content)) {
1498
+ const verdict = classifyRoute(route);
1499
+ if (verdict.verdict === "unsure") {
1500
+ unknowns.push(
1501
+ `A route in this file could not be classified as guarded or open (${verdict.rule}).`
1502
+ );
1503
+ continue;
1504
+ }
1505
+ if (verdict.verdict !== "not-auth") continue;
1506
+ if (!/^(POST|PUT|PATCH|DELETE|ALL)$/i.test(route.method)) continue;
1507
+ const share = input.guardedShare;
1508
+ const dominantlyGuarded = share && share.classified > 0 && share.guarded / share.classified >= 0.75;
1509
+ if (!dominantlyGuarded) {
1510
+ unknowns.push(
1511
+ `${route.method} ${route.path || input.file} has no guard, and there are too few classified peer routes to say whether that is the convention here.`
1512
+ );
1513
+ continue;
1514
+ }
1515
+ conflicts.push({
1516
+ kind: "unguarded_route",
1517
+ severity: "high",
1518
+ message: `${route.method} ${route.path || input.file} would have no authorization guard, while ${share.guarded} of ${share.classified} mutating routes alongside it do.`,
1519
+ referenceFiles: [],
1520
+ // This maps to a protected rule: if it lands, `vg review` cannot pass it.
1521
+ protectedRule: true
1522
+ });
1523
+ }
1524
+ const duplicateOf = [];
1525
+ if (input.similarity && isComparable(input.file)) {
1526
+ const query = {
1527
+ id: `__proposed__:${input.file}`,
1528
+ name: "(proposed)",
1529
+ file: input.file,
1530
+ startLine: 1,
1531
+ endLine: input.content.split("\n").length,
1532
+ text: input.content
1533
+ };
1534
+ for (const hit of input.similarity.find(query).filter((h) => isComparable(h.file))) {
1535
+ duplicateOf.push({ name: hit.name, file: hit.file, startLine: hit.startLine, score: hit.score });
1536
+ }
1537
+ }
1538
+ const status = !haveGraph ? "no_baseline" : unknowns.length > 0 ? "partial" : "ok";
1539
+ return {
1540
+ status,
1541
+ // `ok` is about the change, not the lookup — a `no_baseline` run with no
1542
+ // conflicts is not an endorsement, and the status is what says so.
1543
+ ok: conflicts.length === 0 && duplicateOf.length === 0,
1544
+ conflicts,
1545
+ duplicateOf,
1546
+ convention,
1547
+ declaredTarget: input.declaredTarget,
1548
+ unknowns,
1549
+ confidence: confidenceOf(status, convention, conflicts.length)
1550
+ };
1551
+ }
1552
+ function inferProposedPattern(content, dataAccess) {
1553
+ const imports = [...content.matchAll(/(?:from|import|require)\s*\(?\s*['"]([^'"]+)['"]/g)].map(
1554
+ (m) => m[1]
1555
+ );
1556
+ if (imports.length === 0) return null;
1557
+ let sawPersistence = false;
1558
+ let sawService = false;
1559
+ for (const specifier of imports) {
1560
+ const base = specifier.replace(/^.*\//, "").replace(/\.(js|ts|mjs|cjs)$/, "").toLowerCase();
1561
+ for (const [path12, pattern] of dataAccess) {
1562
+ const target = path12.replace(/^.*\//, "").replace(/\.[^.]+$/, "").toLowerCase();
1563
+ if (target !== base) continue;
1564
+ if (pattern === "direct-persistence" && /repositor|dao|store|entity|model/.test(target)) {
1565
+ sawPersistence = true;
1566
+ }
1567
+ if (/service|usecase|application|handler/.test(target)) sawService = true;
1568
+ }
1569
+ if (/repositor|dao|persistence|entity/.test(base)) sawPersistence = true;
1570
+ if (/service|usecase/.test(base)) sawService = true;
1571
+ }
1572
+ if (sawPersistence) return "direct-persistence";
1573
+ if (sawService) return "via-service";
1574
+ return null;
1575
+ }
1576
+ function confidenceOf(status, convention, conflictCount) {
1577
+ if (status === "no_baseline") return 0.2;
1578
+ let confidence = status === "ok" ? 0.8 : 0.6;
1579
+ if (convention) confidence += 0.1;
1580
+ if (conflictCount === 0 && !convention) confidence -= 0.2;
1581
+ return Math.max(0, Math.min(1, Number(confidence.toFixed(2))));
1582
+ }
1583
+
1584
+ // src/review/dominance.ts
1585
+ var MIN_GROUP_SIZE = 3;
1586
+ var DOMINANCE_SHARE = 0.7;
1587
+ var ENTROPY_NO_CONVENTION = 0.8;
1588
+ var NO_CONVENTION_MIN_FILES = 5;
1589
+ var TEMPORAL_HALF_LIFE_DAYS = 90;
1590
+ var INTENT_BOOST = 1.5;
1591
+ function temporalWeight(daysSinceCommit, halfLifeDays = TEMPORAL_HALF_LIFE_DAYS) {
1592
+ if (daysSinceCommit === null || !Number.isFinite(daysSinceCommit)) return 1;
1593
+ const days = Math.max(0, daysSinceCommit);
1594
+ return 2 * Math.exp(-Math.LN2 * days / halfLifeDays);
1595
+ }
1596
+ function normalizedEntropy(tally) {
1597
+ const values = Object.values(tally).filter((v) => v > 0);
1598
+ if (values.length <= 1) return 0;
1599
+ const total = values.reduce((a, b) => a + b, 0);
1600
+ if (total <= 0) return 0;
1601
+ let h = 0;
1602
+ for (const v of values) {
1603
+ const p = v / total;
1604
+ h -= p * Math.log(p);
1605
+ }
1606
+ return h / Math.log(values.length);
1607
+ }
1608
+ function voteGroup(files, opts = {}) {
1609
+ const minGroupSize = opts.minGroupSize ?? MIN_GROUP_SIZE;
1610
+ const dominanceShare = opts.dominanceShare ?? DOMINANCE_SHARE;
1611
+ const declared = new Set(opts.declaredPatterns ?? []);
1612
+ const group = files[0]?.group ?? "";
1613
+ const groupKind = files[0]?.groupKind ?? "directory";
1614
+ const tally = {};
1615
+ for (const f of files) {
1616
+ const boost = declared.has(f.pattern) ? INTENT_BOOST : 1;
1617
+ tally[f.pattern] = (tally[f.pattern] ?? 0) + temporalWeight(f.daysSinceCommit) * boost;
1618
+ }
1619
+ const base = {
1620
+ group,
1621
+ groupKind,
1622
+ size: files.length,
1623
+ tally,
1624
+ entropy: normalizedEntropy(tally),
1625
+ exemplars: [],
1626
+ deviators: []
1627
+ };
1628
+ if (files.length < minGroupSize) {
1629
+ return { ...base, dominant: null, share: 0, reason: "group_too_small" };
1630
+ }
1631
+ const total = Object.values(tally).reduce((a, b) => a + b, 0);
1632
+ const ranked = Object.entries(tally).sort((a, b) => b[1] - a[1] || (a[0] < b[0] ? -1 : 1));
1633
+ const [leader, leaderWeight] = ranked[0];
1634
+ const share = total > 0 ? leaderWeight / total : 0;
1635
+ const distinctPatterns = Object.values(tally).filter((v) => v > 0).length;
1636
+ if (distinctPatterns >= 3 && base.entropy > ENTROPY_NO_CONVENTION) {
1637
+ return {
1638
+ ...base,
1639
+ dominant: null,
1640
+ share,
1641
+ reason: files.length >= NO_CONVENTION_MIN_FILES ? "no_convention" : "no_clear_leader"
1642
+ };
1643
+ }
1644
+ if (share < dominanceShare) {
1645
+ return { ...base, dominant: null, share, reason: "no_clear_leader" };
1646
+ }
1647
+ const following = files.filter((f) => f.pattern === leader);
1648
+ return {
1649
+ ...base,
1650
+ dominant: leader,
1651
+ share,
1652
+ reason: "dominant",
1653
+ // Most recently touched first — the best example is a live one.
1654
+ exemplars: [...following].sort((a, b) => (a.daysSinceCommit ?? 1e9) - (b.daysSinceCommit ?? 1e9)).slice(0, 3).map((f) => f.path),
1655
+ deviators: files.filter((f) => f.pattern !== leader).map((f) => f.path).sort()
1656
+ };
1657
+ }
1658
+ function voteAll(files, opts = {}) {
1659
+ const groups = /* @__PURE__ */ new Map();
1660
+ for (const f of files) {
1661
+ let bucket = groups.get(f.group);
1662
+ if (!bucket) groups.set(f.group, bucket = []);
1663
+ bucket.push(f);
1664
+ }
1665
+ return [...groups.entries()].sort((a, b) => a[0] < b[0] ? -1 : 1).map(([, bucket]) => voteGroup(bucket, opts));
1666
+ }
1667
+ function defaultRun(args, cwd) {
1668
+ const res = spawnSync("git", args, {
1669
+ cwd,
1670
+ encoding: "utf8",
1671
+ timeout: 2e4,
1672
+ maxBuffer: 64 * 1024 * 1024,
1673
+ windowsHide: true
1674
+ });
1675
+ return { stdout: res.stdout ?? "", status: res.status ?? 1 };
1676
+ }
1677
+ function isGitRepo(root, run = defaultRun) {
1678
+ return run(["rev-parse", "--git-dir"], root).status === 0;
1679
+ }
1680
+ function gitTopLevel(root, run = defaultRun) {
1681
+ const res = run(["rev-parse", "--show-toplevel"], root);
1682
+ const top = res.stdout.trim();
1683
+ return res.status === 0 && top ? top : root;
1684
+ }
1685
+ function normalizeRemote(url) {
1686
+ const raw = url.trim();
1687
+ if (!raw) return null;
1688
+ const scp = raw.match(/^[^@/]+@([^:]+):(.+)$/);
1689
+ if (scp) return `${scp[1]}/${scp[2]}`.replace(/\.git$/, "");
1690
+ const m = raw.match(/^[a-z+]+:\/\/(?:[^@/]*@)?([^/]+)\/(.+)$/i);
1691
+ if (m) return `${m[1]}/${m[2]}`.replace(/\.git$/, "");
1692
+ return raw.replace(/\.git$/, "");
1693
+ }
1694
+ function repoKey(remote, root) {
1695
+ return digestString(`vg.review.repo\0${remote ?? `local:${root}`}`);
1696
+ }
1697
+ function parseNumstat(out) {
1698
+ const map = /* @__PURE__ */ new Map();
1699
+ for (const line of out.split("\n")) {
1700
+ if (!line.trim()) continue;
1701
+ const parts = line.split(" ");
1702
+ if (parts.length < 3) continue;
1703
+ const added = parts[0] === "-" ? 0 : Number(parts[0]);
1704
+ const removed = parts[1] === "-" ? 0 : Number(parts[1]);
1705
+ const path12 = parts.length >= 4 ? parts[3] : parts[2];
1706
+ const op = parts.length >= 4 ? "renamed" : removed > 0 && added === 0 ? "modified" : "modified";
1707
+ map.set(path12, { added, removed, op });
1708
+ }
1709
+ return map;
1710
+ }
1711
+ function opFromStatusLetters(letters) {
1712
+ if (letters.includes("R")) return "renamed";
1713
+ if (letters.includes("A") || letters.includes("?")) return "added";
1714
+ if (letters.includes("D")) return "removed";
1715
+ return "modified";
1716
+ }
1717
+ function parsePorcelainStatus(out) {
1718
+ const map = /* @__PURE__ */ new Map();
1719
+ for (const line of out.split("\n")) {
1720
+ if (!line.trim()) continue;
1721
+ const letters = line.slice(0, 2);
1722
+ const rest = line.slice(3);
1723
+ const raw = rest.includes(" -> ") ? rest.split(" -> ")[1] : rest;
1724
+ map.set(raw.replace(/^"|"$/g, ""), opFromStatusLetters(letters));
1725
+ }
1726
+ return map;
1727
+ }
1728
+ function parseDiffNameStatus(out) {
1729
+ const map = /* @__PURE__ */ new Map();
1730
+ for (const line of out.split("\n")) {
1731
+ if (!line.trim()) continue;
1732
+ const parts = line.split(" ");
1733
+ if (parts.length < 2) continue;
1734
+ const letters = parts[0];
1735
+ const path12 = parts.length >= 3 ? parts[2] : parts[1];
1736
+ map.set(path12.replace(/^"|"$/g, ""), opFromStatusLetters(letters));
1737
+ }
1738
+ return map;
1739
+ }
1740
+ function parseHunks(diff) {
1741
+ const map = /* @__PURE__ */ new Map();
1742
+ let current = null;
1743
+ for (const line of diff.split("\n")) {
1744
+ if (line.startsWith("+++ ")) {
1745
+ const p = line.slice(4).trim();
1746
+ current = p === "/dev/null" ? null : p.replace(/^b\//, "");
1747
+ if (current && !map.has(current)) map.set(current, []);
1748
+ continue;
1749
+ }
1750
+ if (current && line.startsWith("@@")) {
1751
+ const m = line.match(/@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/);
1752
+ if (m) {
1753
+ const start = Number(m[1]);
1754
+ const count = m[2] === void 0 ? 1 : Number(m[2]);
1755
+ if (count > 0) map.get(current).push({ start, end: start + count - 1 });
1756
+ }
1757
+ }
1758
+ }
1759
+ return map;
1760
+ }
1761
+ function readBaseFileTexts(change, run = defaultRun) {
1762
+ const out = /* @__PURE__ */ new Map();
1763
+ const base = change.baseSha;
1764
+ if (!base) return out;
1765
+ for (const file of change.files) {
1766
+ if (file.op === "added") continue;
1767
+ const spec = `${base}:${file.path.replace(/\\/g, "/")}`;
1768
+ const res = run(["show", spec], change.topLevel);
1769
+ if (res.status === 0 && res.stdout.length <= 2 * 1024 * 1024) {
1770
+ out.set(file.path.replace(/\\/g, "/"), res.stdout);
1771
+ }
1772
+ }
1773
+ return out;
1774
+ }
1775
+ function collectChangeSet(root, base, run = defaultRun) {
1776
+ const topLevel = gitTopLevel(root, run);
1777
+ const headSha = run(["rev-parse", "HEAD"], root).stdout.trim();
1778
+ const refRaw = run(["rev-parse", "--abbrev-ref", "HEAD"], root).stdout.trim();
1779
+ const ref = !refRaw || refRaw === "HEAD" ? null : `refs/heads/${refRaw}`;
1780
+ const remoteRaw = run(["config", "--get", "remote.origin.url"], root);
1781
+ const remote = remoteRaw.status === 0 ? normalizeRemote(remoteRaw.stdout) : null;
1782
+ if (base) {
1783
+ const mb = run(["merge-base", "HEAD", base], root);
1784
+ const mergeBase = mb.status === 0 ? mb.stdout.trim() : null;
1785
+ const baseSha = mergeBase ?? run(["rev-parse", base], root).stdout.trim();
1786
+ const numstat2 = parseNumstat(run(["diff", "--numstat", "-M", `${baseSha}..HEAD`], root).stdout);
1787
+ const status2 = parseDiffNameStatus(run(["diff", "--name-status", "-M", `${baseSha}..HEAD`], root).stdout);
1788
+ const hunks2 = parseHunks(run(["diff", "-U0", "-M", `${baseSha}..HEAD`], root).stdout);
1789
+ return {
1790
+ topLevel,
1791
+ baseSha,
1792
+ headSha,
1793
+ mergeBase,
1794
+ ref,
1795
+ dirty: false,
1796
+ dirtyTreeHash: null,
1797
+ files: mergeFiles(numstat2, status2, hunks2),
1798
+ remote
1799
+ };
1800
+ }
1801
+ const numstat = parseNumstat(run(["diff", "--numstat", "-M", "HEAD"], root).stdout);
1802
+ const status = parsePorcelainStatus(run(["status", "--porcelain", "-uall"], root).stdout);
1803
+ const hunks = parseHunks(run(["diff", "-U0", "-M", "HEAD"], root).stdout);
1804
+ const files = mergeFiles(numstat, status, hunks);
1805
+ const dirty = files.length > 0;
1806
+ return {
1807
+ topLevel,
1808
+ baseSha: headSha,
1809
+ headSha,
1810
+ mergeBase: null,
1811
+ ref,
1812
+ dirty,
1813
+ // Identifies *which* dirty state was reviewed, so two reviews of the same
1814
+ // uncommitted work dedupe and a later edit produces a new receipt.
1815
+ dirtyTreeHash: dirty ? dirtyTreeHash(files) : null,
1816
+ files,
1817
+ remote
1818
+ };
1819
+ }
1820
+ var NOT_REVIEWABLE = /(^|\/)(\.vibgrate|node_modules|dist|build|out|target|bin|obj|coverage|\.next|\.turbo|vendor)\//;
1821
+ function isReviewable(path12) {
1822
+ return !NOT_REVIEWABLE.test(path12.replace(/\\/g, "/"));
1823
+ }
1824
+ function mergeFiles(numstat, status, hunks) {
1825
+ const paths = /* @__PURE__ */ new Set([...numstat.keys(), ...status.keys()]);
1826
+ const files = [];
1827
+ for (const path12 of paths) {
1828
+ const n = numstat.get(path12);
1829
+ files.push({
1830
+ path: path12,
1831
+ op: status.get(path12) ?? n?.op ?? "modified",
1832
+ addedLines: n?.added ?? 0,
1833
+ removedLines: n?.removed ?? 0,
1834
+ hunks: hunks.get(path12) ?? []
1835
+ });
1836
+ }
1837
+ return files.filter((f) => isReviewable(f.path)).sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
1838
+ }
1839
+ function dirtyTreeHash(files) {
1840
+ return digestString(
1841
+ files.map((f) => `${f.path}\0${f.op}\0${f.addedLines}\0${f.removedLines}\0${f.hunks.map((h) => `${h.start}-${h.end}`).join(",")}`).join("\n")
1842
+ );
1843
+ }
1844
+ function fileRecencyDays(root, opts = {}, run = defaultRun) {
1845
+ const sinceDays = opts.sinceDays ?? 730;
1846
+ const nowSec = Math.floor((opts.now ?? Date.now()) / 1e3);
1847
+ const out = /* @__PURE__ */ new Map();
1848
+ const res = run(
1849
+ ["log", `--since=${sinceDays}.days.ago`, "--name-only", "--pretty=format:%ct", "--no-merges"],
1850
+ root
1851
+ );
1852
+ if (res.status !== 0) return out;
1853
+ let currentTs = null;
1854
+ for (const line of res.stdout.split("\n")) {
1855
+ const trimmed = line.trim();
1856
+ if (!trimmed) continue;
1857
+ if (/^\d{9,}$/.test(trimmed)) {
1858
+ currentTs = Number(trimmed);
1859
+ continue;
1860
+ }
1861
+ if (currentTs === null) continue;
1862
+ if (out.has(trimmed)) continue;
1863
+ out.set(trimmed, Math.max(0, (nowSec - currentTs) / 86400));
1864
+ }
1865
+ return out;
1866
+ }
1867
+ var INTENT_FILES = [
1868
+ "CLAUDE.md",
1869
+ "AGENTS.md",
1870
+ ".cursorrules",
1871
+ ".github/copilot-instructions.md",
1872
+ "CONTRIBUTING.md"
1873
+ ];
1874
+ var MAX_INTENT_BYTES = 256 * 1024;
1875
+ var PATTERN_VOCABULARY = [
1876
+ { pattern: "clean", match: /\bclean architecture\b/i },
1877
+ { pattern: "hexagonal", match: /\bhexagonal\b|\bports?\s+and\s+adapters?\b/i },
1878
+ { pattern: "onion", match: /\bonion architecture\b/i },
1879
+ { pattern: "layered", match: /\blayered architecture\b|\bn-tier\b|\bthree-tier\b/i },
1880
+ { pattern: "vertical-slice", match: /\bvertical slices?\b|\bfeature slices?\b/i },
1881
+ { pattern: "mvc", match: /\bMVC\b/ },
1882
+ { pattern: "mvvm", match: /\bMVVM\b/ }
1883
+ ];
1884
+ function readDeclaredIntent(root, files = INTENT_FILES) {
1885
+ const patterns = /* @__PURE__ */ new Set();
1886
+ const sources = [];
1887
+ const citations = [];
1888
+ for (const rel of files) {
1889
+ const abs = path4.join(root, rel);
1890
+ let text;
1891
+ try {
1892
+ const stat = fs.statSync(abs);
1893
+ if (!stat.isFile() || stat.size > MAX_INTENT_BYTES) continue;
1894
+ text = fs.readFileSync(abs, "utf8");
1895
+ } catch {
1896
+ continue;
1897
+ }
1898
+ let matchedHere = false;
1899
+ const lines = text.split("\n");
1900
+ for (let i = 0; i < lines.length; i++) {
1901
+ for (const { pattern, match } of PATTERN_VOCABULARY) {
1902
+ if (!match.test(lines[i])) continue;
1903
+ patterns.add(pattern);
1904
+ matchedHere = true;
1905
+ if (citations.length < 12) {
1906
+ citations.push({ file: rel, line: i + 1, text: lines[i].trim().slice(0, 200) });
1907
+ }
1908
+ }
1909
+ }
1910
+ if (matchedHere) sources.push(rel);
1911
+ }
1912
+ return { patterns: [...patterns].sort(), sources, citations };
1913
+ }
1914
+
1915
+ // src/review/mcp-assess.ts
1916
+ var cache = /* @__PURE__ */ new Map();
1917
+ var BOUNDARY_EDGE_KINDS = /* @__PURE__ */ new Set(["import", "call"]);
1918
+ var FUNCTION_KINDS = /* @__PURE__ */ new Set(["function", "method"]);
1919
+ var ROUTE_LAYERS = /* @__PURE__ */ new Set(["routing", "middleware", "presentation"]);
1920
+ var MAX_ROUTE_FILES = 300;
1921
+ function buildBaseline(graph, root) {
1922
+ const nodeById = new Map(graph.nodes.map((n) => [n.id, n]));
1923
+ const normalize = (p) => p.replace(/\\/g, "/");
1924
+ const layerCache = /* @__PURE__ */ new Map();
1925
+ const layerOf = (p) => {
1926
+ if (layerCache.has(p)) return layerCache.get(p);
1927
+ const cls = classifyFile(p, "unknown");
1928
+ const layer = cls?.layer ?? null;
1929
+ layerCache.set(p, layer);
1930
+ return layer;
1931
+ };
1932
+ const outgoing = /* @__PURE__ */ new Map();
1933
+ for (const edge of graph.edges) {
1934
+ if (!BOUNDARY_EDGE_KINDS.has(edge.kind)) continue;
1935
+ const src = nodeById.get(edge.src);
1936
+ const dst = nodeById.get(edge.dst);
1937
+ if (!src || !dst) continue;
1938
+ const from = normalize(src.file);
1939
+ const to = normalize(dst.file);
1940
+ if (from === to) continue;
1941
+ const toLayer = layerOf(to);
1942
+ if (!toLayer) continue;
1943
+ let set = outgoing.get(from);
1944
+ if (!set) outgoing.set(from, set = /* @__PURE__ */ new Set());
1945
+ set.add(toLayer);
1946
+ }
1947
+ const areaOfFile = /* @__PURE__ */ new Map();
1948
+ for (const area of graph.areas) {
1949
+ for (const member of area.members) {
1950
+ const node = nodeById.get(member);
1951
+ if (node) areaOfFile.set(normalize(node.file), area.id);
1952
+ }
1953
+ }
1954
+ const files = [...new Set(graph.nodes.map((n) => normalize(n.file)))].sort();
1955
+ const recency = fileRecencyDays(root);
1956
+ const intent = readDeclaredIntent(root);
1957
+ const dataAccess = /* @__PURE__ */ new Map();
1958
+ const candidates = [];
1959
+ for (const file of files) {
1960
+ const cls = classifyFile(file, "unknown");
1961
+ if (!cls) continue;
1962
+ const pattern = classifyDataAccess(cls.layer, [...outgoing.get(file) ?? []]);
1963
+ if (!pattern) continue;
1964
+ dataAccess.set(file, pattern);
1965
+ const areaId = areaOfFile.get(file);
1966
+ const role = cls.role ?? cls.layer;
1967
+ candidates.push({
1968
+ path: file,
1969
+ pattern,
1970
+ role,
1971
+ areaGroup: areaId !== void 0 ? `area:${areaId}:${role}` : null
1972
+ });
1973
+ }
1974
+ const areaCounts = /* @__PURE__ */ new Map();
1975
+ for (const c of candidates) {
1976
+ if (c.areaGroup) areaCounts.set(c.areaGroup, (areaCounts.get(c.areaGroup) ?? 0) + 1);
1977
+ }
1978
+ const peerFiles = candidates.map((c) => {
1979
+ const useArea = c.areaGroup !== null && (areaCounts.get(c.areaGroup) ?? 0) >= MIN_GROUP_SIZE;
1980
+ return {
1981
+ path: c.path,
1982
+ pattern: c.pattern,
1983
+ group: useArea ? c.areaGroup : `role:${c.role}`,
1984
+ groupKind: useArea ? "area" : "role",
1985
+ daysSinceCommit: recency.get(c.path) ?? null
1986
+ };
1987
+ });
1988
+ const similarity = new SimilarityIndex();
1989
+ const lineCache = /* @__PURE__ */ new Map();
1990
+ const linesOf = (rel) => {
1991
+ if (lineCache.has(rel)) return lineCache.get(rel);
1992
+ let value = null;
1993
+ try {
1994
+ const abs = path4.join(root, rel);
1995
+ const stat = fs.statSync(abs);
1996
+ if (stat.isFile() && stat.size <= 1024 * 1024) value = fs.readFileSync(abs, "utf8").split("\n");
1997
+ } catch {
1998
+ value = null;
1999
+ }
2000
+ lineCache.set(rel, value);
2001
+ return value;
2002
+ };
2003
+ for (const node of graph.nodes) {
2004
+ if (!FUNCTION_KINDS.has(node.kind)) continue;
2005
+ const rel = normalize(node.file);
2006
+ if (!isComparable(rel)) continue;
2007
+ const lines = linesOf(rel);
2008
+ if (!lines) continue;
2009
+ const text = lines.slice(node.span.start - 1, node.span.end).join("\n");
2010
+ if (!text.trim()) continue;
2011
+ const body = {
2012
+ id: node.id,
2013
+ name: node.name,
2014
+ file: rel,
2015
+ startLine: node.span.start,
2016
+ endLine: node.span.end,
2017
+ text
2018
+ };
2019
+ similarity.add(body);
2020
+ }
2021
+ const guardedByDirectory = /* @__PURE__ */ new Map();
2022
+ let routeFiles = 0;
2023
+ for (const file of files) {
2024
+ if (routeFiles >= MAX_ROUTE_FILES) break;
2025
+ const layer = layerOf(file);
2026
+ if (!layer || !ROUTE_LAYERS.has(layer)) continue;
2027
+ const lines = linesOf(file);
2028
+ if (!lines) continue;
2029
+ routeFiles++;
2030
+ const dir = file.split("/").slice(0, -1).join("/") || ".";
2031
+ for (const route of routesForFile(file, lines.join("\n"))) {
2032
+ const verdict = classifyRoute(route);
2033
+ if (verdict.verdict === "unsure") continue;
2034
+ if (!/^(POST|PUT|PATCH|DELETE|ALL)$/i.test(route.method)) continue;
2035
+ const entry = guardedByDirectory.get(dir) ?? { guarded: 0, classified: 0 };
2036
+ entry.classified++;
2037
+ if (verdict.verdict === "auth") entry.guarded++;
2038
+ guardedByDirectory.set(dir, entry);
2039
+ }
2040
+ }
2041
+ return {
2042
+ corpusHash: graph.provenance.corpusHash,
2043
+ votes: voteAll(peerFiles, { declaredPatterns: intent.patterns }),
2044
+ dataAccess,
2045
+ similarity,
2046
+ declaredTarget: intent.patterns[0] ?? null,
2047
+ guardedByDirectory
2048
+ };
2049
+ }
2050
+ function assessProposedChange(graph, root, file, content) {
2051
+ if (!file) return { error: "bad_request", message: "file is required" };
2052
+ if (!content) return { error: "bad_request", message: "content is required" };
2053
+ const key = path4.resolve(root);
2054
+ let baseline = cache.get(key);
2055
+ if (!baseline || baseline.corpusHash !== graph.provenance.corpusHash) {
2056
+ try {
2057
+ baseline = buildBaseline(graph, root);
2058
+ cache.set(key, baseline);
2059
+ } catch {
2060
+ return {
2061
+ status: "no_baseline",
2062
+ ok: true,
2063
+ conflicts: [],
2064
+ duplicateOf: [],
2065
+ convention: null,
2066
+ declaredTarget: null,
2067
+ unknowns: ["The baseline could not be built from the code map, so nothing was checked."],
2068
+ confidence: 0.2
2069
+ };
2070
+ }
2071
+ }
2072
+ const normalized = file.replace(/\\/g, "/");
2073
+ const cls = classifyFile(normalized, "unknown");
2074
+ const role = cls ? cls.role ?? cls.layer : null;
2075
+ const directory = normalized.split("/").slice(0, -1).join("/") || ".";
2076
+ return assessChange({
2077
+ file: normalized,
2078
+ content,
2079
+ votes: baseline.votes,
2080
+ dataAccess: baseline.dataAccess,
2081
+ similarity: baseline.similarity,
2082
+ declaredTarget: baseline.declaredTarget,
2083
+ // Resolve the peer group the same way the capsule does, preferring the
2084
+ // file's area when it has one and falling back to its role.
2085
+ group: baseline.votes.find((v) => v.exemplars.includes(normalized) || v.deviators.includes(normalized))?.group ?? (role ? `role:${role}` : void 0),
2086
+ guardedShare: baseline.guardedByDirectory.get(directory) ?? null
2087
+ });
2088
+ }
936
2089
  function countTokens(text) {
937
2090
  return encode(text).length;
938
2091
  }
@@ -1414,7 +2567,7 @@ function readyEmbedder(local) {
1414
2567
  async function retrieve(graph, question, budget, ctx) {
1415
2568
  let mode = "lexical";
1416
2569
  const topicTags = await loadTopicTags(graph, ctx.root, ctx.graphPath);
1417
- const modRank = await rankQuestion(graph, question, { limit: 48, topicTags });
2570
+ const modRank = await rankQuestion(graph, rankingAskFrom(question), { limit: 48, topicTags });
1418
2571
  if (ctx.semanticSession && !ctx.local) {
1419
2572
  try {
1420
2573
  const ranked = await withTimeout(
@@ -2158,6 +3311,20 @@ var TOOLS = [
2158
3311
  }
2159
3312
  return answer ?? { error: "not_found" };
2160
3313
  }
3314
+ },
3315
+ {
3316
+ name: "assess_change",
3317
+ description: "Before you write: would this code fit? One call reports whether the proposal conflicts with what its peers do, would add an unguarded route, or re-implements something that already exists \u2014 with the files to follow instead. Ask this while deciding; `vg review` asks the same questions after the fact, when the fix costs more.",
3318
+ inputSchema: obj(
3319
+ {
3320
+ file: { type: "string", description: "repo-relative path the code would live at" },
3321
+ content: { type: "string", maxLength: 2e4, description: "the proposed code" }
3322
+ },
3323
+ ["file", "content"]
3324
+ ),
3325
+ // A thin adapter. Every rule lives in `review/assess.ts`, transport-free, so
3326
+ // the hook, the CLI and this tool cannot drift into three sets of answers.
3327
+ handler: (graph, args, ctx) => assessProposedChange(graph, ctx.root, String(args.file ?? ""), String(args.content ?? ""))
2161
3328
  }
2162
3329
  ];
2163
3330
  var VERBOSITY_BUDGET = { concise: 1500, balanced: 4e3, exhaustive: 12e3 };
@@ -2243,6 +3410,6 @@ function navigationToolsetConfig(serverName = "vibgrate") {
2243
3410
  };
2244
3411
  }
2245
3412
 
2246
- export { FREE_PACK, HOT_TOOLS, TOOLS, assessDocQuality, budgetSuffix, clearStoredCredentials, countTokens, coveringTests, createWorkspaceDsn, credentialsPath, deferredToolNames, detectRunner, dsnCommand, ensureGitignored, fetchHostedDocsCached, findGitRoot, gitignoreEntryForCredentials, homeCredentialsPath, hostedBase, listedToolNames, navigationToolsetConfig, parseDsn, pathDisconnect, projectCredentialsPath, publishPrivateLibrary, pushCommand, readScanArtifact, readStoredCredentials, renderToolResult, resolveDsn, selectForBudget, shortestPath, symbolsFromApi, testsToRun, uploadScanArtifact, warmEmbedderInBackground, writeStoredCredentials };
2247
- //# sourceMappingURL=chunk-TUVB6Q6F.js.map
2248
- //# sourceMappingURL=chunk-TUVB6Q6F.js.map
3413
+ export { CAPSULE_BUDGETS, CAPSULE_SCHEMA, FINDINGS_SCHEMA, FREE_PACK, HOT_TOOLS, MAX_ROLE_PEERS, MIN_GROUP_SIZE, RECEIPT_SCHEMA, REVIEW_POLICY_VERSION, SimilarityIndex, TARGET_ALIGNMENTS, TOOLS, assessDocQuality, budgetSuffix, buildEnvelope, classifyDataAccess, classifyRoute, clearStoredCredentials, collectChangeSet, collectSpans, countTokens, coveringTests, createWorkspaceDsn, credentialsPath, defaultRun, deferredToolNames, describePattern, detectRunner, digest, digestString, dsnCommand, ensureGitignored, fetchHostedDocsCached, fileRecencyDays, findGitRoot, gitignoreEntryForCredentials, homeCredentialsPath, hostedBase, isComparable, isGitRepo, isRegression, listedToolNames, navigationToolsetConfig, parseDsn, pathDisconnect, projectCredentialsPath, publishPrivateLibrary, pushCommand, pushReceipt, readBaseFileTexts, readDeclaredIntent, readScanArtifact, readStoredCredentials, receiptDigest, receiptId, rejectPushWhenOffline, renderToolResult, repoKey, resolveDsn, routesForFile, selectForBudget, shortestPath, symbolsFromApi, testsToRun, uploadScanArtifact, voteAll, voteAllAuth, warmEmbedderInBackground, writeStoredCredentials };
3414
+ //# sourceMappingURL=chunk-65YN3BG3.js.map
3415
+ //# sourceMappingURL=chunk-65YN3BG3.js.map