@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.
- package/DOCS.md +204 -9
- package/README.md +2 -0
- package/dist/{agent-4RHH2FR2.js → agent-5GW7C7EB.js} +14 -14
- package/dist/{agent-4RHH2FR2.js.map → agent-5GW7C7EB.js.map} +1 -1
- package/dist/{approvals-XDFM346D.js → approvals-ISJFILVK.js} +5 -5
- package/dist/{approvals-XDFM346D.js.map → approvals-ISJFILVK.js.map} +1 -1
- package/dist/{attach-LGTCYNKC.js → attach-JUSALQ75.js} +3 -3
- package/dist/{attach-LGTCYNKC.js.map → attach-JUSALQ75.js.map} +1 -1
- package/dist/baseline-YL2IZZ4Y.js +9 -0
- package/dist/{baseline-PRY5HKL5.js.map → baseline-YL2IZZ4Y.js.map} +1 -1
- package/dist/{chunk-ZRDTI67N.js → chunk-5RW5PIBH.js} +3 -3
- package/dist/{chunk-ZRDTI67N.js.map → chunk-5RW5PIBH.js.map} +1 -1
- package/dist/{chunk-TUVB6Q6F.js → chunk-65YN3BG3.js} +1207 -40
- package/dist/chunk-65YN3BG3.js.map +1 -0
- package/dist/{chunk-TKUWEJZJ.js → chunk-B5VCPIKT.js} +7 -6
- package/dist/chunk-B5VCPIKT.js.map +1 -0
- package/dist/{chunk-WU2DVESP.js → chunk-BA6YBVNT.js} +3 -3
- package/dist/{chunk-WU2DVESP.js.map → chunk-BA6YBVNT.js.map} +1 -1
- package/dist/{chunk-IM7HME2T.js → chunk-BODXEZEY.js} +9 -9
- package/dist/{chunk-IM7HME2T.js.map → chunk-BODXEZEY.js.map} +1 -1
- package/dist/{chunk-PIB3YNJU.js → chunk-D3SV3JWB.js} +3 -3
- package/dist/{chunk-PIB3YNJU.js.map → chunk-D3SV3JWB.js.map} +1 -1
- package/dist/chunk-EGMBHPXV.js +127 -0
- package/dist/chunk-EGMBHPXV.js.map +1 -0
- package/dist/{chunk-7KOEEEIZ.js → chunk-FHSEK64W.js} +7 -7
- package/dist/{chunk-7KOEEEIZ.js.map → chunk-FHSEK64W.js.map} +1 -1
- package/dist/{chunk-PEHMRPYM.js → chunk-FKCN653F.js} +3 -3
- package/dist/{chunk-PEHMRPYM.js.map → chunk-FKCN653F.js.map} +1 -1
- package/dist/{chunk-K2IWOZEQ.js → chunk-FPIMNVLI.js} +12 -4
- package/dist/chunk-FPIMNVLI.js.map +1 -0
- package/dist/{chunk-BHDP7EVJ.js → chunk-HHMWE52J.js} +10 -135
- package/dist/chunk-HHMWE52J.js.map +1 -0
- package/dist/{chunk-5UG4UHKU.js → chunk-HUJZO4K5.js} +281 -106
- package/dist/chunk-HUJZO4K5.js.map +1 -0
- package/dist/{chunk-TQ75FB52.js → chunk-JWQ2JCQS.js} +4 -4
- package/dist/{chunk-TQ75FB52.js.map → chunk-JWQ2JCQS.js.map} +1 -1
- package/dist/{chunk-WGA4KUGS.js → chunk-K7QR7U3V.js} +12 -11
- package/dist/chunk-K7QR7U3V.js.map +1 -0
- package/dist/{chunk-E44I7635.js → chunk-KCKG3EEJ.js} +3 -3
- package/dist/{chunk-E44I7635.js.map → chunk-KCKG3EEJ.js.map} +1 -1
- package/dist/{chunk-WGSLN73L.js → chunk-MMIIVRQC.js} +7 -7
- package/dist/chunk-MMIIVRQC.js.map +1 -0
- package/dist/chunk-N6R4XPLX.js +27 -0
- package/dist/chunk-N6R4XPLX.js.map +1 -0
- package/dist/{chunk-WPQCJADP.js → chunk-NQQT75AN.js} +62 -22
- package/dist/chunk-NQQT75AN.js.map +1 -0
- package/dist/{chunk-TOHNVUDD.js → chunk-NSC7ZK62.js} +3 -3
- package/dist/{chunk-TOHNVUDD.js.map → chunk-NSC7ZK62.js.map} +1 -1
- package/dist/{chunk-LAYDXJEQ.js → chunk-OAYSRM7J.js} +12 -14
- package/dist/chunk-OAYSRM7J.js.map +1 -0
- package/dist/{chunk-R5BGRJ6X.js → chunk-QMROD7MI.js} +15 -8
- package/dist/chunk-QMROD7MI.js.map +1 -0
- package/dist/{chunk-SHD5TCZ5.js → chunk-R2FX4USU.js} +7 -7
- package/dist/chunk-R2FX4USU.js.map +1 -0
- package/dist/{chunk-7S4ELKK6.js → chunk-RL7JYG3D.js} +3 -3
- package/dist/{chunk-7S4ELKK6.js.map → chunk-RL7JYG3D.js.map} +1 -1
- package/dist/{chunk-QLNJ3VWO.js → chunk-RRSUNTZI.js} +3 -3
- package/dist/{chunk-QLNJ3VWO.js.map → chunk-RRSUNTZI.js.map} +1 -1
- package/dist/{chunk-QONHVC4H.js → chunk-TWFPHXPN.js} +3 -3
- package/dist/{chunk-QONHVC4H.js.map → chunk-TWFPHXPN.js.map} +1 -1
- package/dist/chunk-UGRUG2FA.js +6 -0
- package/dist/{chunk-JQ7OL4XG.js.map → chunk-UGRUG2FA.js.map} +1 -1
- package/dist/{chunk-XW4FCBL4.js → chunk-VOEEYCR3.js} +6 -6
- package/dist/{chunk-XW4FCBL4.js.map → chunk-VOEEYCR3.js.map} +1 -1
- package/dist/{chunk-WHNB3443.js → chunk-WBDRBP22.js} +4 -4
- package/dist/{chunk-WHNB3443.js.map → chunk-WBDRBP22.js.map} +1 -1
- package/dist/{chunk-NQU6E6OG.js → chunk-YL332BU2.js} +20 -15
- package/dist/chunk-YL332BU2.js.map +1 -0
- package/dist/{chunk-IEZCFMNL.js → chunk-ZQY5TWCB.js} +3 -3
- package/dist/{chunk-IEZCFMNL.js.map → chunk-ZQY5TWCB.js.map} +1 -1
- package/dist/cli.d.ts +16 -1
- package/dist/cli.js +2359 -340
- package/dist/cli.js.map +1 -1
- package/dist/{daemon-2YMCH7R2.js → daemon-NRIIDMX2.js} +10 -10
- package/dist/{daemon-2YMCH7R2.js.map → daemon-NRIIDMX2.js.map} +1 -1
- package/dist/embed-worker-main.js +3 -3
- package/dist/{ensure-map-S2GBW2UC.js → ensure-map-VAIYCCEJ.js} +17 -16
- package/dist/{ensure-map-S2GBW2UC.js.map → ensure-map-VAIYCCEJ.js.map} +1 -1
- package/dist/explain-EXPJTCB5.js +126 -0
- package/dist/explain-EXPJTCB5.js.map +1 -0
- package/dist/{graph-backend-QDEU5RIR.js → graph-backend-V5A2H5ZB.js} +7 -7
- package/dist/{graph-backend-QDEU5RIR.js.map → graph-backend-V5A2H5ZB.js.map} +1 -1
- package/dist/index.d.ts +217 -13
- package/dist/index.js +17 -16
- package/dist/index.js.map +1 -1
- package/dist/{interactive-GMGWUEQO.js → interactive-VQM2CTE2.js} +32 -31
- package/dist/interactive-VQM2CTE2.js.map +1 -0
- package/dist/load-UAVA732M.js +11 -0
- package/dist/{load-JXCTAAEI.js.map → load-UAVA732M.js.map} +1 -1
- package/dist/parse-worker.js +2 -2
- package/dist/{rules-ASYEUNB5.js → rules-TLRGB5L4.js} +12 -4
- package/dist/rules-TLRGB5L4.js.map +1 -0
- package/dist/{run-outcome-5NA7PI52.js → run-outcome-EATWIKLR.js} +5 -5
- package/dist/{run-outcome-5NA7PI52.js.map → run-outcome-EATWIKLR.js.map} +1 -1
- package/dist/{runtime-session-IEM6DZDP.js → runtime-session-Y2BRBXUW.js} +9 -9
- package/dist/{runtime-session-IEM6DZDP.js.map → runtime-session-Y2BRBXUW.js.map} +1 -1
- package/dist/{session-VYFLJ7KL.js → session-N2FTYIHQ.js} +9 -9
- package/dist/{session-VYFLJ7KL.js.map → session-N2FTYIHQ.js.map} +1 -1
- package/dist/{stream-json-2ISQLKPD.js → stream-json-GCZAHORX.js} +14 -14
- package/dist/{stream-json-2ISQLKPD.js.map → stream-json-GCZAHORX.js.map} +1 -1
- package/dist/version-XNZ4GLMQ.js +4 -0
- package/dist/{version-B62ELJRY.js.map → version-XNZ4GLMQ.js.map} +1 -1
- package/dist/{vg-mcp-bridge-CJIK5U56.js → vg-mcp-bridge-VDZWFEO2.js} +22 -11
- package/dist/vg-mcp-bridge-VDZWFEO2.js.map +1 -0
- package/dist/{vgd-4EB6SKFB.js → vgd-3A2TRNP5.js} +8 -8
- package/dist/vgd-3A2TRNP5.js.map +1 -0
- package/package.json +2 -2
- package/dist/baseline-PRY5HKL5.js +0 -9
- package/dist/chunk-5UG4UHKU.js.map +0 -1
- package/dist/chunk-BHDP7EVJ.js.map +0 -1
- package/dist/chunk-JQ7OL4XG.js +0 -6
- package/dist/chunk-K2IWOZEQ.js.map +0 -1
- package/dist/chunk-LAYDXJEQ.js.map +0 -1
- package/dist/chunk-NQU6E6OG.js.map +0 -1
- package/dist/chunk-R5BGRJ6X.js.map +0 -1
- package/dist/chunk-SHD5TCZ5.js.map +0 -1
- package/dist/chunk-TKUWEJZJ.js.map +0 -1
- package/dist/chunk-TUVB6Q6F.js.map +0 -1
- package/dist/chunk-WGA4KUGS.js.map +0 -1
- package/dist/chunk-WGSLN73L.js.map +0 -1
- package/dist/chunk-WPQCJADP.js.map +0 -1
- package/dist/chunk-YENM3HT7.js +0 -23
- package/dist/chunk-YENM3HT7.js.map +0 -1
- package/dist/interactive-GMGWUEQO.js.map +0 -1
- package/dist/load-JXCTAAEI.js +0 -11
- package/dist/rules-ASYEUNB5.js.map +0 -1
- package/dist/version-B62ELJRY.js +0 -4
- package/dist/vg-mcp-bridge-CJIK5U56.js.map +0 -1
- 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-
|
|
3
|
-
import { searchSymbols, resolveVersion, resolveLib, loadCatalog, driftFor, localPackageDocs, readDoc, localApiSurface } from './chunk-
|
|
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-
|
|
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-
|
|
11
|
-
import { loadTopicTags, rankQuestion } from './chunk-
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
344
|
+
console.log(chalk3.green("\u2714") + " DSN created");
|
|
339
345
|
console.log("");
|
|
340
|
-
console.log(
|
|
346
|
+
console.log(chalk3.bold("Region:"));
|
|
341
347
|
console.log(` ${region ?? "custom"} (${ingestHost})`);
|
|
342
348
|
console.log("");
|
|
343
|
-
console.log(
|
|
349
|
+
console.log(chalk3.bold("DSN:"));
|
|
344
350
|
console.log(` ${dsn}`);
|
|
345
351
|
console.log("");
|
|
346
|
-
console.log(
|
|
352
|
+
console.log(chalk3.bold("Key ID:"));
|
|
347
353
|
console.log(` ${keyId}`);
|
|
348
354
|
if (isNewWorkspace) {
|
|
349
355
|
console.log("");
|
|
350
|
-
console.log(
|
|
356
|
+
console.log(chalk3.bold("Workspace ID:"));
|
|
351
357
|
console.log(` ${workspaceId}`);
|
|
352
358
|
}
|
|
353
359
|
console.log("");
|
|
354
|
-
console.log(
|
|
360
|
+
console.log(chalk3.dim("Set this as VIBGRATE_DSN in your CI environment."));
|
|
355
361
|
if (!isNewWorkspace) {
|
|
356
|
-
console.log(
|
|
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(
|
|
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(
|
|
374
|
+
console.log(chalk3.green("\u2714") + ` Created .gitignore and ignored ${rel}`);
|
|
369
375
|
} else if (res.status === "added") {
|
|
370
|
-
console.log(
|
|
376
|
+
console.log(chalk3.green("\u2714") + ` Added ${rel} to .gitignore`);
|
|
371
377
|
} else if (res.status === "present") {
|
|
372
|
-
console.log(
|
|
378
|
+
console.log(chalk3.dim(` ${rel} is already in .gitignore`));
|
|
373
379
|
}
|
|
374
380
|
} else {
|
|
375
381
|
console.log(
|
|
376
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
785
|
-
console.error(
|
|
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(
|
|
792
|
-
console.error(
|
|
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(
|
|
799
|
-
console.error(
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
986
|
+
console.log(chalk3.green("\u2714") + ` Scan queued for processing (${result.ingestId ?? "ok"})`);
|
|
843
987
|
console.log();
|
|
844
|
-
console.log(
|
|
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(
|
|
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(
|
|
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-
|
|
2248
|
-
//# sourceMappingURL=chunk-
|
|
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
|