@vibgrate/cli 2026.825.2 → 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-DHQYEHVW.js → agent-5GW7C7EB.js} +14 -14
- package/dist/{agent-DHQYEHVW.js.map → agent-5GW7C7EB.js.map} +1 -1
- package/dist/{approvals-KBM72NU2.js → approvals-ISJFILVK.js} +5 -5
- package/dist/{approvals-KBM72NU2.js.map → approvals-ISJFILVK.js.map} +1 -1
- package/dist/{attach-O74NL4M5.js → attach-JUSALQ75.js} +3 -3
- package/dist/{attach-O74NL4M5.js.map → attach-JUSALQ75.js.map} +1 -1
- package/dist/baseline-YL2IZZ4Y.js +9 -0
- package/dist/{baseline-4X6MQBDA.js.map → baseline-YL2IZZ4Y.js.map} +1 -1
- package/dist/{chunk-JAEHLTVX.js → chunk-5RW5PIBH.js} +3 -3
- package/dist/{chunk-JAEHLTVX.js.map → chunk-5RW5PIBH.js.map} +1 -1
- package/dist/{chunk-TT2J2TB6.js → chunk-65YN3BG3.js} +1207 -40
- package/dist/chunk-65YN3BG3.js.map +1 -0
- package/dist/{chunk-VMPKOMHG.js → chunk-B5VCPIKT.js} +7 -6
- package/dist/chunk-B5VCPIKT.js.map +1 -0
- package/dist/{chunk-FQHWPOTS.js → chunk-BA6YBVNT.js} +3 -3
- package/dist/{chunk-FQHWPOTS.js.map → chunk-BA6YBVNT.js.map} +1 -1
- package/dist/{chunk-3E7QT4NE.js → chunk-BODXEZEY.js} +9 -9
- package/dist/{chunk-3E7QT4NE.js.map → chunk-BODXEZEY.js.map} +1 -1
- package/dist/{chunk-BICZR6UJ.js → chunk-D3SV3JWB.js} +3 -3
- package/dist/{chunk-BICZR6UJ.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-DIZ5KI5Q.js → chunk-FHSEK64W.js} +7 -7
- package/dist/{chunk-DIZ5KI5Q.js.map → chunk-FHSEK64W.js.map} +1 -1
- package/dist/{chunk-E3XVIFSH.js → chunk-FKCN653F.js} +3 -3
- package/dist/{chunk-E3XVIFSH.js.map → chunk-FKCN653F.js.map} +1 -1
- package/dist/{chunk-OIUVWCKM.js → chunk-FPIMNVLI.js} +12 -4
- package/dist/chunk-FPIMNVLI.js.map +1 -0
- package/dist/{chunk-NS4DL3QR.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-HFQIYM6E.js → chunk-JWQ2JCQS.js} +4 -4
- package/dist/{chunk-HFQIYM6E.js.map → chunk-JWQ2JCQS.js.map} +1 -1
- package/dist/{chunk-27TS2AUJ.js → chunk-K7QR7U3V.js} +12 -11
- package/dist/chunk-K7QR7U3V.js.map +1 -0
- package/dist/{chunk-IJVWKZRS.js → chunk-KCKG3EEJ.js} +3 -3
- package/dist/{chunk-IJVWKZRS.js.map → chunk-KCKG3EEJ.js.map} +1 -1
- package/dist/{chunk-GWSJYDK7.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-GR7SFCQJ.js → chunk-NQQT75AN.js} +77 -27
- package/dist/chunk-NQQT75AN.js.map +1 -0
- package/dist/{chunk-6MIJLO5B.js → chunk-NSC7ZK62.js} +3 -3
- package/dist/{chunk-6MIJLO5B.js.map → chunk-NSC7ZK62.js.map} +1 -1
- package/dist/{chunk-SYMJSRYF.js → chunk-OAYSRM7J.js} +12 -14
- package/dist/chunk-OAYSRM7J.js.map +1 -0
- package/dist/{chunk-L7GCJNDT.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-7M4E7UGX.js → chunk-RL7JYG3D.js} +3 -3
- package/dist/{chunk-7M4E7UGX.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-NZES47ST.js → chunk-TWFPHXPN.js} +3 -3
- package/dist/{chunk-NZES47ST.js.map → chunk-TWFPHXPN.js.map} +1 -1
- package/dist/chunk-UGRUG2FA.js +6 -0
- package/dist/{chunk-G3ACLBMG.js.map → chunk-UGRUG2FA.js.map} +1 -1
- package/dist/{chunk-KYM55VZX.js → chunk-VOEEYCR3.js} +6 -6
- package/dist/{chunk-KYM55VZX.js.map → chunk-VOEEYCR3.js.map} +1 -1
- package/dist/{chunk-SIXWWQ6S.js → chunk-WBDRBP22.js} +4 -4
- package/dist/{chunk-SIXWWQ6S.js.map → chunk-WBDRBP22.js.map} +1 -1
- package/dist/{chunk-K3SXWOTT.js → chunk-YL332BU2.js} +20 -15
- package/dist/chunk-YL332BU2.js.map +1 -0
- package/dist/{chunk-2NUS5BOR.js → chunk-ZQY5TWCB.js} +3 -3
- package/dist/{chunk-2NUS5BOR.js.map → chunk-ZQY5TWCB.js.map} +1 -1
- package/dist/cli.d.ts +16 -1
- package/dist/cli.js +2362 -342
- package/dist/cli.js.map +1 -1
- package/dist/{daemon-RZS5FPIW.js → daemon-NRIIDMX2.js} +10 -10
- package/dist/{daemon-RZS5FPIW.js.map → daemon-NRIIDMX2.js.map} +1 -1
- package/dist/embed-worker-main.js +3 -3
- package/dist/{ensure-map-DL2S774J.js → ensure-map-VAIYCCEJ.js} +17 -16
- package/dist/{ensure-map-DL2S774J.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-JXARVT3R.js → graph-backend-V5A2H5ZB.js} +7 -7
- package/dist/{graph-backend-JXARVT3R.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-2XRBRFRB.js → interactive-VQM2CTE2.js} +37 -32
- package/dist/interactive-VQM2CTE2.js.map +1 -0
- package/dist/load-UAVA732M.js +11 -0
- package/dist/{load-7C5VETRD.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-TMB5TJNA.js → run-outcome-EATWIKLR.js} +5 -5
- package/dist/{run-outcome-TMB5TJNA.js.map → run-outcome-EATWIKLR.js.map} +1 -1
- package/dist/{runtime-session-Y23NVY7R.js → runtime-session-Y2BRBXUW.js} +9 -9
- package/dist/{runtime-session-Y23NVY7R.js.map → runtime-session-Y2BRBXUW.js.map} +1 -1
- package/dist/{session-KXT2ZTPE.js → session-N2FTYIHQ.js} +9 -9
- package/dist/{session-KXT2ZTPE.js.map → session-N2FTYIHQ.js.map} +1 -1
- package/dist/{stream-json-NW6AOE47.js → stream-json-GCZAHORX.js} +14 -14
- package/dist/{stream-json-NW6AOE47.js.map → stream-json-GCZAHORX.js.map} +1 -1
- package/dist/version-XNZ4GLMQ.js +4 -0
- package/dist/{version-7ABYXWOZ.js.map → version-XNZ4GLMQ.js.map} +1 -1
- package/dist/{vg-mcp-bridge-LXBHKEFR.js → vg-mcp-bridge-VDZWFEO2.js} +22 -11
- package/dist/vg-mcp-bridge-VDZWFEO2.js.map +1 -0
- package/dist/{vgd-RW7HCEIP.js → vgd-3A2TRNP5.js} +8 -8
- package/dist/vgd-3A2TRNP5.js.map +1 -0
- package/package.json +2 -2
- package/dist/baseline-4X6MQBDA.js +0 -9
- package/dist/chunk-27TS2AUJ.js.map +0 -1
- package/dist/chunk-5UG4UHKU.js.map +0 -1
- package/dist/chunk-G3ACLBMG.js +0 -6
- package/dist/chunk-GR7SFCQJ.js.map +0 -1
- package/dist/chunk-GWSJYDK7.js.map +0 -1
- package/dist/chunk-K3SXWOTT.js.map +0 -1
- package/dist/chunk-L7GCJNDT.js.map +0 -1
- package/dist/chunk-NS4DL3QR.js.map +0 -1
- package/dist/chunk-OIUVWCKM.js.map +0 -1
- package/dist/chunk-SHD5TCZ5.js.map +0 -1
- package/dist/chunk-SYMJSRYF.js.map +0 -1
- package/dist/chunk-TT2J2TB6.js.map +0 -1
- package/dist/chunk-VMPKOMHG.js.map +0 -1
- package/dist/chunk-YENM3HT7.js +0 -23
- package/dist/chunk-YENM3HT7.js.map +0 -1
- package/dist/interactive-2XRBRFRB.js.map +0 -1
- package/dist/load-7C5VETRD.js +0 -11
- package/dist/rules-ASYEUNB5.js.map +0 -1
- package/dist/version-7ABYXWOZ.js +0 -4
- package/dist/vg-mcp-bridge-LXBHKEFR.js.map +0 -1
- package/dist/vgd-RW7HCEIP.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -1,45 +1,46 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { fetchCatalog, resolveProviders } from './chunk-
|
|
3
|
-
import { telemetryOptOut, StatsSharer, statsEndpoint, isCI } from './chunk-
|
|
4
|
-
import { loadAdvancedScanHook, baselineCommand } from './chunk-
|
|
5
|
-
import { sha256Hex, generateKeypair, dssePae, DSSE_PAYLOAD_TYPE, keyId, registerBuild, LiveStatus, ProgressBar, printLogo, runBuild } from './chunk-
|
|
6
|
-
export { runBuild } from './chunk-
|
|
2
|
+
import { fetchCatalog, resolveProviders } from './chunk-WBDRBP22.js';
|
|
3
|
+
import { telemetryOptOut, StatsSharer, statsEndpoint, isCI } from './chunk-D3SV3JWB.js';
|
|
4
|
+
import { loadAdvancedScanHook, baselineCommand } from './chunk-RRSUNTZI.js';
|
|
5
|
+
import { sha256Hex, generateKeypair, dssePae, DSSE_PAYLOAD_TYPE, keyId, registerBuild, LiveStatus, ProgressBar, printLogo, runBuild } from './chunk-K7QR7U3V.js';
|
|
6
|
+
export { runBuild } from './chunk-K7QR7U3V.js';
|
|
7
7
|
import { readOrchestratorState, writeOrchestratorState, parseMode, setDefaultMode, recommendMode, activePin, resolveMode, providerForBackend, QUALIFIED_PACKS, pinPack, unpinPack, buildOrchestratorStatus } from './chunk-OLIMW4PQ.js';
|
|
8
8
|
import { defaultProcessEndpoint, startProcessHostServer, resolveInferenceIsolation } from './chunk-IASO2HCI.js';
|
|
9
|
-
import { runCodeSession } from './chunk-
|
|
10
|
-
import { CONTEXT_POLICY_VERSION, computeFcsReport, renderFcsReportMarkdown, createTrajectoryCollector, runAgent } from './chunk-
|
|
9
|
+
import { runCodeSession } from './chunk-MMIIVRQC.js';
|
|
10
|
+
import { CONTEXT_POLICY_VERSION, computeFcsReport, renderFcsReportMarkdown, createTrajectoryCollector, runAgent } from './chunk-NQQT75AN.js';
|
|
11
11
|
import './chunk-543E6WDJ.js';
|
|
12
|
-
import './chunk-
|
|
13
|
-
import './chunk-
|
|
14
|
-
import { parseReasoningEffort, summarizeDiffs, ScriptedProvider } from './chunk-
|
|
12
|
+
import './chunk-FKCN653F.js';
|
|
13
|
+
import './chunk-5RW5PIBH.js';
|
|
14
|
+
import { parseReasoningEffort, summarizeDiffs, ScriptedProvider } from './chunk-R2FX4USU.js';
|
|
15
15
|
import './chunk-NV7XNBX4.js';
|
|
16
16
|
import { buildModelInstallPlan } from './chunk-W44THEXF.js';
|
|
17
17
|
import { ensurePackage, ensureUnavailableMessage } from './chunk-3HNPVXUK.js';
|
|
18
|
-
import { vgdVersionSkew, restartVgdIfRunning, stopVgd, registerDaemon, rootOf, requireGraph } from './chunk-
|
|
19
|
-
import { readGlobal, applyGlobalOptions } from './chunk-
|
|
18
|
+
import { vgdVersionSkew, restartVgdIfRunning, stopVgd, registerDaemon, rootOf, requireGraph } from './chunk-FHSEK64W.js';
|
|
19
|
+
import { readGlobal, applyGlobalOptions } from './chunk-N6R4XPLX.js';
|
|
20
20
|
import { ActivityLog } from './chunk-H5Q4JUY4.js';
|
|
21
21
|
import { disableColor, info, c, json, out } from './chunk-CT5BS56U.js';
|
|
22
22
|
import { init_resolve_ollama, gatherSystemMemory, resolveOllamaBinary, hasOllamaBinary, resolve_ollama_exports } from './chunk-WUFIXAEJ.js';
|
|
23
23
|
import { fmt, estimateModelBytes } from './chunk-YJYEPYB2.js';
|
|
24
|
-
import { isManifest, isDependencyFile, manifestHash, loadScanCache, writeScanCache, manifestKind, findPackageLine, endOfLine, findRuntimeLine } from './chunk-
|
|
24
|
+
import { isManifest, isDependencyFile, manifestHash, loadScanCache, writeScanCache, manifestKind, findPackageLine, endOfLine, findRuntimeLine } from './chunk-VOEEYCR3.js';
|
|
25
25
|
import { clearHostSessionPool, acquireHostSession, generateOnSession, listHostSessions, hostSessionPoolSize } from './chunk-3LOWJSAF.js';
|
|
26
26
|
import { buildIdentifierTrieFromGraph } from './chunk-5HYCB55P.js';
|
|
27
27
|
import './chunk-IS7VTH2Q.js';
|
|
28
28
|
import './chunk-IZDH67DX.js';
|
|
29
|
-
import { moduleInstalled, removeRelevanceModule, installRelevanceModule, DISCLOSURE, RELEVANCE_MODULE_NAME, moduleInstalledAt, removeModule, installModule, kickRelevanceReadiness, modulesBaseDir, ensureRelevanceModule, DaemonSemanticSession, serveStdio, kernelDisabled, writeConsent, readConsent, exportGraph, GraphSource, createServer, formatForExt, RefreshScheduler } from './chunk-
|
|
30
|
-
import { refreshIfStale } from './chunk-
|
|
31
|
-
import { driftCount, ASSISTANTS, detectAssistants, isAssistantInstalled, detectServeLaunch, installAssistant, writeNavigationConfig, uninstallAssistant, buildGraph, writeSnapshot, probeFreshness, assistantById, SMALL_REPO_FILES } from './chunk-
|
|
32
|
-
import
|
|
29
|
+
import { moduleInstalled, removeRelevanceModule, installRelevanceModule, DISCLOSURE, RELEVANCE_MODULE_NAME, moduleInstalledAt, removeModule, installModule, kickRelevanceReadiness, modulesBaseDir, ensureRelevanceModule, DaemonSemanticSession, serveStdio, kernelDisabled, writeConsent, readConsent, exportGraph, GraphSource, createServer, formatForExt, RefreshScheduler } from './chunk-BODXEZEY.js';
|
|
30
|
+
import { refreshIfStale } from './chunk-B5VCPIKT.js';
|
|
31
|
+
import { driftCount, ASSISTANTS, detectAssistants, isAssistantInstalled, detectServeLaunch, installAssistant, writeNavigationConfig, uninstallAssistant, buildGraph, writeSnapshot, probeFreshness, assistantById, SMALL_REPO_FILES } from './chunk-HHMWE52J.js';
|
|
32
|
+
import './chunk-EGMBHPXV.js';
|
|
33
|
+
import { resolveDsn, createWorkspaceDsn, writeStoredCredentials, credentialsPath, findGitRoot, ensureGitignored, gitignoreEntryForCredentials, clearStoredCredentials, parseDsn as parseDsn$1, uploadScanArtifact, dsnCommand, pushCommand, countTokens, shortestPath, pathDisconnect, testsToRun, detectRunner, coveringTests, FREE_PACK, readScanArtifact, rejectPushWhenOffline, publishPrivateLibrary, assessDocQuality, symbolsFromApi, fetchHostedDocsCached, selectForBudget, hostedBase, isGitRepo, collectChangeSet, fileRecencyDays, readDeclaredIntent, readBaseFileTexts, SimilarityIndex, isComparable, FINDINGS_SCHEMA, digest, repoKey, receiptId, receiptDigest, CAPSULE_SCHEMA, REVIEW_POLICY_VERSION, RECEIPT_SCHEMA, buildEnvelope, collectSpans, pushReceipt, readStoredCredentials, projectCredentialsPath, MAX_ROLE_PEERS, classifyDataAccess, MIN_GROUP_SIZE, voteAll, CAPSULE_BUDGETS, digestString, routesForFile, classifyRoute, voteAllAuth, isRegression, describePattern, homeCredentialsPath, TARGET_ALIGNMENTS, defaultRun } from './chunk-65YN3BG3.js';
|
|
33
34
|
import { resolveIngestHost, dashHostForIngestHost, availableRegionIds, ingestHostForRegionId } from './chunk-HTLDXEHW.js';
|
|
34
|
-
import { writeDefaultConfig, parseDsn, detectVcs, computeRepoFingerprint, resolveRepositoryName, fetchScanPreflight, runCoreScan, loadConfig, prepareCompressedUpload, computeDriftScore, titleBox, projectTypeToVulnEcosystem, driftBar, gitHistoryAvailable, buildVersionTimelines, findPackageAnyEcosystem, normalizeConstraint, findVersionCrossings, versionSatisfies, refineArchitectureWithAstRoles, refineArchitectureWithGraph, PROJECT_TYPE_TO_OSV_ECOSYSTEM, fetchRiskySymbols, perDependencyDrift,
|
|
35
|
-
import './chunk-
|
|
35
|
+
import { writeDefaultConfig, parseDsn, detectVcs, computeRepoFingerprint, resolveRepositoryName, fetchScanPreflight, runCoreScan, loadConfig, prepareCompressedUpload, computeDriftScore, titleBox, projectTypeToVulnEcosystem, driftBar, gitHistoryAvailable, buildVersionTimelines, findPackageAnyEcosystem, normalizeConstraint, findVersionCrossings, versionSatisfies, refineArchitectureWithAstRoles, refineArchitectureWithGraph, classifyFile, PROJECT_TYPE_TO_OSV_ECOSYSTEM, fetchRiskySymbols, boundaryProfileRules, evaluateLayerBoundary, perDependencyDrift, parseToml } from './chunk-HUJZO4K5.js';
|
|
36
|
+
import './chunk-QMROD7MI.js';
|
|
36
37
|
import './chunk-VIWZ4QT7.js';
|
|
37
|
-
import { catalogPath, libDir, loadCatalog, driftFor, addLibrary, resolveLib, libId, resolveVersion, readDoc, localPackageDocs, localApiSurface } from './chunk-
|
|
38
|
-
import { recordCliCall, CLI_TOOL_ALIASES, savingsLedgerPath, clearSavings, readSavings, readUsage, readModelSavings, CAPSULE_RANKING_VERSION, SAVINGS_TOOLS, sanitizeClient } from './chunk-
|
|
38
|
+
import { catalogPath, libDir, loadCatalog, driftFor, addLibrary, resolveLib, libId, resolveVersion, readDoc, localPackageDocs, localApiSurface } from './chunk-TWFPHXPN.js';
|
|
39
|
+
import { recordCliCall, CLI_TOOL_ALIASES, savingsLedgerPath, clearSavings, readSavings, readUsage, readModelSavings, CAPSULE_RANKING_VERSION, SAVINGS_TOOLS, sanitizeClient } from './chunk-OAYSRM7J.js';
|
|
39
40
|
import { discoverModels } from './chunk-WNKGRXUA.js';
|
|
40
41
|
import { freeBytesOnVolume, weightStoreDir, listCachedWeights, ensureWeightCached, catalogIntegrityReport } from './chunk-57ZRSCHY.js';
|
|
41
42
|
import { CliError, ExitCode, usageError, engineUnavailable } from './chunk-MS6V5XKB.js';
|
|
42
|
-
import { attachVgd } from './chunk-
|
|
43
|
+
import { attachVgd } from './chunk-KCKG3EEJ.js';
|
|
43
44
|
import { pathExists as pathExists$1, parseExcludePatterns } from './chunk-PVNALGTG.js';
|
|
44
45
|
import { ensureDir, pathExists, readJsonFile, writeTextFile, writeJsonFile } from './chunk-CRAMOEBE.js';
|
|
45
46
|
import './chunk-5IQBIXHS.js';
|
|
@@ -48,27 +49,27 @@ import { publishGraphToVgd } from './chunk-A3WWRW7U.js';
|
|
|
48
49
|
import { isRelevantChange } from './chunk-DBX3DL27.js';
|
|
49
50
|
import { inventory, enrichOnline, ECOSYSTEMS } from './chunk-6BEBJCK4.js';
|
|
50
51
|
import { resolveOne } from './chunk-J7LKEB2R.js';
|
|
51
|
-
import { queryGraphSemantic, queryGraph, indexFor, impactOf } from './chunk-
|
|
52
|
-
import { loadGraph, loadGraphPreferIndex, SCHEMA_VERSION } from './chunk-
|
|
52
|
+
import { rankingAskFrom, queryGraphSemantic, queryGraph, indexFor, impactOf } from './chunk-YL332BU2.js';
|
|
53
|
+
import { loadGraph, loadGraphPreferIndex, SCHEMA_VERSION } from './chunk-RL7JYG3D.js';
|
|
53
54
|
import './chunk-VHRSG6CF.js';
|
|
54
55
|
import './chunk-6H2QV5DR.js';
|
|
55
|
-
import { discover, isSkippedDirName } from './chunk-RTURQXQL.js';
|
|
56
|
-
import { resolvedGrammarFiles, TOOLCHAIN_GRAMMARS, resolveToolchainGrammar, grammarSetVersion, fileRolesFromParseCache } from './chunk-
|
|
56
|
+
import { discover, mergeExcludes, isSkippedDirName } from './chunk-RTURQXQL.js';
|
|
57
|
+
import { resolvedGrammarFiles, TOOLCHAIN_GRAMMARS, resolveToolchainGrammar, grammarSetVersion, fileRolesFromParseCache } from './chunk-NSC7ZK62.js';
|
|
57
58
|
import './chunk-T7RSQ5WD.js';
|
|
58
59
|
import './chunk-6WRG7VDO.js';
|
|
59
|
-
import { embeddingsCached, resolveEmbedModel, loadEmbedder, getNodeEmbeddings, unavailableMessage, countPending, modelCacheInfo, embeddingsPath, clearModelCache, withTimeout } from './chunk-
|
|
60
|
-
import { resetRelevanceProviderCache, loadRelevanceProvider, resolveGraphPath, loadTopicTags, rankQuestion, renderReport, loadGraphFileWithSnapshot, writeArtifacts, vibgrateDir, mapFileExists, ensureVibgrateGitignore } from './chunk-
|
|
60
|
+
import { embeddingsCached, resolveEmbedModel, loadEmbedder, getNodeEmbeddings, unavailableMessage, countPending, modelCacheInfo, embeddingsPath, clearModelCache, acquireLock, releaseLock, withTimeout } from './chunk-BA6YBVNT.js';
|
|
61
|
+
import { resetRelevanceProviderCache, loadRelevanceProvider, resolveGraphPath, loadTopicTags, rankQuestion, renderReport, loadGraphFileWithSnapshot, writeArtifacts, displayGraphPath, vibgrateDir, mapFileExists, ensureVibgrateGitignore } from './chunk-FPIMNVLI.js';
|
|
61
62
|
import { cacheDir } from './chunk-MVPRPSXU.js';
|
|
62
63
|
import { serializeGraph, stableStringify, parseGraph } from './chunk-O3LABX6I.js';
|
|
63
|
-
import { VERSION } from './chunk-
|
|
64
|
+
import { VERSION } from './chunk-UGRUG2FA.js';
|
|
64
65
|
import './chunk-XP6UUCZZ.js';
|
|
65
66
|
import { canonicalize, hashString } from './chunk-JFGYT6BI.js';
|
|
66
67
|
import './chunk-XQNKCA2J.js';
|
|
67
68
|
import { __toCommonJS } from './chunk-Z6ZWNWWR.js';
|
|
68
69
|
import * as fs12 from 'fs';
|
|
69
70
|
import fs12__default, { realpathSync } from 'fs';
|
|
70
|
-
import * as
|
|
71
|
-
import
|
|
71
|
+
import * as path51 from 'path';
|
|
72
|
+
import path51__default from 'path';
|
|
72
73
|
import { fileURLToPath, pathToFileURL } from 'url';
|
|
73
74
|
import { Command, CommanderError, Option } from 'commander';
|
|
74
75
|
import { execSync, spawn, spawnSync, execFileSync } from 'child_process';
|
|
@@ -79,7 +80,7 @@ import * as readline from 'readline';
|
|
|
79
80
|
import * as crypto3 from 'crypto';
|
|
80
81
|
import { randomUUID } from 'crypto';
|
|
81
82
|
import chalk7 from 'chalk';
|
|
82
|
-
import * as
|
|
83
|
+
import * as fs32 from 'fs/promises';
|
|
83
84
|
import { parse, stringify } from 'yaml';
|
|
84
85
|
|
|
85
86
|
init_cli_invocation();
|
|
@@ -90,10 +91,10 @@ function registerStatus(program) {
|
|
|
90
91
|
applyGlobalOptions(cmd);
|
|
91
92
|
}
|
|
92
93
|
async function runStatus(global) {
|
|
93
|
-
const root3 =
|
|
94
|
+
const root3 = path51.resolve(global.cwd ?? ".");
|
|
94
95
|
const graphPath = resolveGraphPath(root3, global.graph);
|
|
95
96
|
const graph = loadGraph(root3, graphPath);
|
|
96
|
-
const hasCache = fs12.existsSync(
|
|
97
|
+
const hasCache = fs12.existsSync(path51.join(cacheDir(root3), "parse-cache.json"));
|
|
97
98
|
let stale = null;
|
|
98
99
|
let staleExact = false;
|
|
99
100
|
if (graph) {
|
|
@@ -113,8 +114,8 @@ async function runStatus(global) {
|
|
|
113
114
|
}
|
|
114
115
|
if (global.json) {
|
|
115
116
|
json({
|
|
116
|
-
root:
|
|
117
|
-
graphPath:
|
|
117
|
+
root: path51.relative(process.cwd(), root3) || ".",
|
|
118
|
+
graphPath: displayGraphPath(root3, graphPath),
|
|
118
119
|
built: graph !== null,
|
|
119
120
|
generatedAt: graph?.generatedAt ?? null,
|
|
120
121
|
counts: graph?.meta.counts ?? null,
|
|
@@ -129,11 +130,11 @@ async function runStatus(global) {
|
|
|
129
130
|
return;
|
|
130
131
|
}
|
|
131
132
|
if (!graph) {
|
|
132
|
-
info(`${c.cyan("vg")} \xB7 no map yet at ${c.dim(
|
|
133
|
+
info(`${c.cyan("vg")} \xB7 no map yet at ${c.dim(displayGraphPath(root3, graphPath))}`);
|
|
133
134
|
info(` run ${c.bold("vg")} to build one`);
|
|
134
135
|
return;
|
|
135
136
|
}
|
|
136
|
-
info(`${c.cyan("vg")} \xB7 ${
|
|
137
|
+
info(`${c.cyan("vg")} \xB7 ${displayGraphPath(root3, graphPath)}`);
|
|
137
138
|
info(` generated ${graph.generatedAt}`);
|
|
138
139
|
info(
|
|
139
140
|
` nodes ${c.bold(String(graph.meta.counts.nodes))} edges ${c.bold(
|
|
@@ -183,13 +184,13 @@ function registerAsk(program) {
|
|
|
183
184
|
);
|
|
184
185
|
const budget = Number(opts.budget) || 2e3;
|
|
185
186
|
const q = question.join(" ");
|
|
186
|
-
const wantSemantic = opts.semantic !== false && !global.
|
|
187
|
+
const wantSemantic = opts.semantic !== false && !global.offline;
|
|
187
188
|
let result;
|
|
188
189
|
let mode = "lexical";
|
|
189
190
|
let note = "";
|
|
190
|
-
if (!global.
|
|
191
|
+
if (!global.offline) await ensureRelevanceModule();
|
|
191
192
|
const topicTags = await loadTopicTags(graph, root3, resolveGraphPath(root3, global.graph));
|
|
192
|
-
const modRank = await rankQuestion(graph, q, { limit: 48, topicTags });
|
|
193
|
+
const modRank = await rankQuestion(graph, rankingAskFrom(q), { limit: 48, topicTags });
|
|
193
194
|
try {
|
|
194
195
|
if (wantSemantic) {
|
|
195
196
|
live?.set("asking the daemon\u2026");
|
|
@@ -219,7 +220,7 @@ function registerAsk(program) {
|
|
|
219
220
|
let reason;
|
|
220
221
|
let detail;
|
|
221
222
|
const embedder = await loadEmbedder({
|
|
222
|
-
local: global.
|
|
223
|
+
local: global.offline,
|
|
223
224
|
onUnavailable: (r, d) => {
|
|
224
225
|
reason = r;
|
|
225
226
|
detail = d;
|
|
@@ -242,8 +243,8 @@ function registerAsk(program) {
|
|
|
242
243
|
}
|
|
243
244
|
} else {
|
|
244
245
|
result = queryGraph(graph, q, { budget, ranked: modRank });
|
|
245
|
-
if (global.
|
|
246
|
-
activity.add("answer", "skip", global.
|
|
246
|
+
if (global.offline) note = "semantic skipped under --offline; used lexical";
|
|
247
|
+
activity.add("answer", "skip", global.offline ? "lexical only (--offline)" : "lexical only (--no-semantic)");
|
|
247
248
|
}
|
|
248
249
|
} finally {
|
|
249
250
|
live?.stop();
|
|
@@ -309,11 +310,11 @@ function crashedMessage(signal) {
|
|
|
309
310
|
return `semantic search is disabled: its native backend (onnxruntime-node, via the vendored fastembed path) crashed with ${signal} \u2014 using fast lexical search instead. This usually means the installed native binaries are broken (an interrupted install, or a platform without prebuilt CPU binaries). Reinstall to repair \u2014 ${envPrefix}npm install -g --allow-scripts=@vibgrate/cli,onnxruntime-node,msgpackr-extract @vibgrate/cli \u2014 or run with --local to stay lexical quietly.`;
|
|
310
311
|
}
|
|
311
312
|
function defaultCacheDir() {
|
|
312
|
-
const base = process.env.XDG_CACHE_HOME ||
|
|
313
|
-
return
|
|
313
|
+
const base = process.env.XDG_CACHE_HOME || path51.join(os.homedir(), ".cache");
|
|
314
|
+
return path51.join(base, "vibgrate", "models");
|
|
314
315
|
}
|
|
315
316
|
function verdictPath(dir) {
|
|
316
|
-
return
|
|
317
|
+
return path51.join(dir, `embed-health-${process.platform}-${process.arch}.json`);
|
|
317
318
|
}
|
|
318
319
|
function readVerdict(dir) {
|
|
319
320
|
try {
|
|
@@ -333,12 +334,12 @@ function writeVerdict(dir, verdict) {
|
|
|
333
334
|
}
|
|
334
335
|
}
|
|
335
336
|
function probeModuleUrl(from = import.meta.url) {
|
|
336
|
-
const dir =
|
|
337
|
+
const dir = path51.dirname(fileURLToPath(from));
|
|
337
338
|
for (const name of ["embeddings.js", "index.js"]) {
|
|
338
|
-
const candidate =
|
|
339
|
+
const candidate = path51.join(dir, name);
|
|
339
340
|
if (fs12.existsSync(candidate)) return pathToFileURL(candidate).href;
|
|
340
341
|
}
|
|
341
|
-
return pathToFileURL(
|
|
342
|
+
return pathToFileURL(path51.join(dir, "index.js")).href;
|
|
342
343
|
}
|
|
343
344
|
function defaultRunProbe(timeoutMs) {
|
|
344
345
|
const moduleUrl = probeModuleUrl();
|
|
@@ -357,7 +358,7 @@ function defaultRunProbe(timeoutMs) {
|
|
|
357
358
|
})
|
|
358
359
|
.catch(() => process.stdout.write('{"ok":false}'));
|
|
359
360
|
`;
|
|
360
|
-
return new Promise((
|
|
361
|
+
return new Promise((resolve25) => {
|
|
361
362
|
const child = spawn(process.execPath, ["--input-type=module", "-e", script], {
|
|
362
363
|
stdio: ["ignore", "pipe", "ignore"],
|
|
363
364
|
// The probe must never inherit a debugger port or loader flags that would
|
|
@@ -373,7 +374,7 @@ function defaultRunProbe(timeoutMs) {
|
|
|
373
374
|
if (settled) return;
|
|
374
375
|
settled = true;
|
|
375
376
|
clearTimeout(timer);
|
|
376
|
-
|
|
377
|
+
resolve25({ signal, status, stdout });
|
|
377
378
|
};
|
|
378
379
|
const timer = setTimeout(() => {
|
|
379
380
|
child.kill("SIGTERM");
|
|
@@ -437,7 +438,7 @@ function registerEmbed(program) {
|
|
|
437
438
|
const root3 = rootOf(global);
|
|
438
439
|
const bg = opts.bg === true;
|
|
439
440
|
const speak = !bg && !global.json;
|
|
440
|
-
if (global.
|
|
441
|
+
if (global.offline) {
|
|
441
442
|
if (global.json) json({ embedded: 0, model: modelId, semantic: false, reason: "local" });
|
|
442
443
|
else if (speak) info(c.dim("vg embed \xB7 semantic is off (--local) \u2014 lexical search only"));
|
|
443
444
|
return;
|
|
@@ -457,7 +458,7 @@ function registerEmbed(program) {
|
|
|
457
458
|
let reason;
|
|
458
459
|
let detail;
|
|
459
460
|
const embedder = await loadEmbedder({
|
|
460
|
-
local: global.
|
|
461
|
+
local: global.offline,
|
|
461
462
|
// Background warm-ups skip the download unless explicitly asked to fetch
|
|
462
463
|
// (`--bg --download`, spawned by `vg build` so the model is ready by the
|
|
463
464
|
// time `vg serve`/`vg ask` needs it).
|
|
@@ -1091,7 +1092,7 @@ var PUBLISH_INTERVAL_MS = 2e3;
|
|
|
1091
1092
|
var KEEPALIVE_MS = 1e4;
|
|
1092
1093
|
var STALE_MS = 6e4;
|
|
1093
1094
|
function liveStatsDir(root3) {
|
|
1094
|
-
return
|
|
1095
|
+
return path51.join(cacheDir(root3), "serve-live");
|
|
1095
1096
|
}
|
|
1096
1097
|
function reapOtherVersionServers(dir, keepVersion, now = Date.now(), kill = (pid, signal) => process.kill(pid, signal), selfPid = process.pid) {
|
|
1097
1098
|
let entries;
|
|
@@ -1105,7 +1106,7 @@ function reapOtherVersionServers(dir, keepVersion, now = Date.now(), kill = (pid
|
|
|
1105
1106
|
if (!/^serve-\d+\.json$/.test(name)) continue;
|
|
1106
1107
|
let parsed;
|
|
1107
1108
|
try {
|
|
1108
|
-
parsed = JSON.parse(fs12.readFileSync(
|
|
1109
|
+
parsed = JSON.parse(fs12.readFileSync(path51.join(dir, name), "utf8"));
|
|
1109
1110
|
} catch {
|
|
1110
1111
|
continue;
|
|
1111
1112
|
}
|
|
@@ -1127,7 +1128,7 @@ var LiveStatsBus = class {
|
|
|
1127
1128
|
this.dir = dir;
|
|
1128
1129
|
this.stats = stats;
|
|
1129
1130
|
this.pid = pid;
|
|
1130
|
-
this.file =
|
|
1131
|
+
this.file = path51.join(dir, `serve-${this.pid}.json`);
|
|
1131
1132
|
}
|
|
1132
1133
|
dir;
|
|
1133
1134
|
stats;
|
|
@@ -1180,7 +1181,7 @@ var LiveStatsBus = class {
|
|
|
1180
1181
|
const out2 = [];
|
|
1181
1182
|
for (const name of entries.sort()) {
|
|
1182
1183
|
if (!/^serve-\d+\.json$/.test(name)) continue;
|
|
1183
|
-
const file =
|
|
1184
|
+
const file = path51.join(this.dir, name);
|
|
1184
1185
|
let parsed;
|
|
1185
1186
|
try {
|
|
1186
1187
|
parsed = JSON.parse(fs12.readFileSync(file, "utf8"));
|
|
@@ -1570,7 +1571,7 @@ function registerServe(program) {
|
|
|
1570
1571
|
const root3 = rootOf(global);
|
|
1571
1572
|
const graphPath = resolveGraphPath(root3, global.graph);
|
|
1572
1573
|
const refresh = opts.refresh !== false && !global.graph;
|
|
1573
|
-
const local = global.
|
|
1574
|
+
const local = global.offline === true;
|
|
1574
1575
|
const optOut = telemetryOptOut();
|
|
1575
1576
|
const shareStats = opts.shareStats === true && !local && optOut === null;
|
|
1576
1577
|
const quiet = global.quiet === true;
|
|
@@ -1713,7 +1714,7 @@ async function serveHttp(graphPath, host, port, opts, freshness, onReady) {
|
|
|
1713
1714
|
info(c.red(`vg serve: request error: ${err.message}`));
|
|
1714
1715
|
}
|
|
1715
1716
|
});
|
|
1716
|
-
await new Promise((
|
|
1717
|
+
await new Promise((resolve25) => httpServer.listen(port, host, resolve25));
|
|
1717
1718
|
info(c.dim(`vg \xB7 MCP server on http://${host}:${port}/mcp (read-only, local, ${freshness})`));
|
|
1718
1719
|
onReady?.();
|
|
1719
1720
|
await new Promise(() => {
|
|
@@ -1826,10 +1827,10 @@ var Connection = class {
|
|
|
1826
1827
|
};
|
|
1827
1828
|
function projectDir(root3, projectPath) {
|
|
1828
1829
|
const rel = projectPath === "" || projectPath === "." ? "" : projectPath;
|
|
1829
|
-
return rel ?
|
|
1830
|
+
return rel ? path51.resolve(root3, rel) : path51.resolve(root3);
|
|
1830
1831
|
}
|
|
1831
1832
|
function projectForFile(root3, projects, filePath) {
|
|
1832
|
-
const dir =
|
|
1833
|
+
const dir = path51.dirname(path51.resolve(filePath));
|
|
1833
1834
|
for (const proj of projects) {
|
|
1834
1835
|
if (projectDir(root3, proj.path) === dir) return proj;
|
|
1835
1836
|
}
|
|
@@ -1838,7 +1839,7 @@ function projectForFile(root3, projects, filePath) {
|
|
|
1838
1839
|
let bestLen = -1;
|
|
1839
1840
|
for (const proj of projects) {
|
|
1840
1841
|
const projDir = projectDir(root3, proj.path);
|
|
1841
|
-
if ((dir === projDir || dir.startsWith(projDir +
|
|
1842
|
+
if ((dir === projDir || dir.startsWith(projDir + path51.sep)) && projDir.length > bestLen) {
|
|
1842
1843
|
best = proj;
|
|
1843
1844
|
bestLen = projDir.length;
|
|
1844
1845
|
}
|
|
@@ -1848,7 +1849,7 @@ function projectForFile(root3, projects, filePath) {
|
|
|
1848
1849
|
var MAX_ENTRIES = 1e3;
|
|
1849
1850
|
var KEEP_ENTRIES = 500;
|
|
1850
1851
|
function historyPath(root3) {
|
|
1851
|
-
return
|
|
1852
|
+
return path51.join(root3, ".vibgrate", "score-history.jsonl");
|
|
1852
1853
|
}
|
|
1853
1854
|
function parseLine(line) {
|
|
1854
1855
|
try {
|
|
@@ -1889,7 +1890,7 @@ function recordScore(root3, entry) {
|
|
|
1889
1890
|
}
|
|
1890
1891
|
}
|
|
1891
1892
|
const file = historyPath(root3);
|
|
1892
|
-
fs12.mkdirSync(
|
|
1893
|
+
fs12.mkdirSync(path51.dirname(file), { recursive: true });
|
|
1893
1894
|
ensureVibgrateGitignore(root3);
|
|
1894
1895
|
fs12.appendFileSync(file, `${JSON.stringify(entry)}
|
|
1895
1896
|
`);
|
|
@@ -1907,7 +1908,7 @@ function deltaFrom(prev, current) {
|
|
|
1907
1908
|
return current.score - prev.score;
|
|
1908
1909
|
}
|
|
1909
1910
|
function ignoreFilePath(root3) {
|
|
1910
|
-
return
|
|
1911
|
+
return path51.join(root3, ".vibgrate", "ignore.json");
|
|
1911
1912
|
}
|
|
1912
1913
|
function isActive(entry, now = Date.now()) {
|
|
1913
1914
|
if (!entry.until) return true;
|
|
@@ -1939,7 +1940,7 @@ function readIgnores(root3, now = Date.now()) {
|
|
|
1939
1940
|
return out2;
|
|
1940
1941
|
}
|
|
1941
1942
|
function inventoryPath(root3) {
|
|
1942
|
-
return
|
|
1943
|
+
return path51.join(root3, ".vibgrate", "inline-inventory.json");
|
|
1943
1944
|
}
|
|
1944
1945
|
function inventoryKey(projectPath, pkg) {
|
|
1945
1946
|
return `${projectPath}::${pkg}`;
|
|
@@ -2492,6 +2493,9 @@ function architectureWire(arch) {
|
|
|
2492
2493
|
};
|
|
2493
2494
|
if (arch.folders) out2.folders = arch.folders;
|
|
2494
2495
|
if (arch.unclassifiedFiles) out2.unclassifiedFiles = arch.unclassifiedFiles;
|
|
2496
|
+
if (arch.coverage) out2.coverage = arch.coverage;
|
|
2497
|
+
if (arch.unclassifiedFolders) out2.unclassifiedFolders = arch.unclassifiedFolders;
|
|
2498
|
+
if (arch.unclassifiedFoldersCapped) out2.unclassifiedFoldersCapped = true;
|
|
2495
2499
|
if (arch.classified) out2.classified = arch.classified;
|
|
2496
2500
|
if (arch.workspaceShape) out2.workspaceShape = arch.workspaceShape;
|
|
2497
2501
|
if (arch.supportLevel !== void 0) out2.supportLevel = arch.supportLevel;
|
|
@@ -2889,7 +2893,7 @@ var VibgrateLanguageServer = class {
|
|
|
2889
2893
|
* the workspace scan cache (billing / repo score stay on the official scan).
|
|
2890
2894
|
*/
|
|
2891
2895
|
async scanOpenManifest(uri, filePath) {
|
|
2892
|
-
const relDir =
|
|
2896
|
+
const relDir = path51.relative(this.opts.root, path51.dirname(path51.resolve(filePath)));
|
|
2893
2897
|
const key2 = relDir === "" ? "." : relDir;
|
|
2894
2898
|
if (this.openManifestScans.has(key2) || this.openManifestFailed.has(key2)) return;
|
|
2895
2899
|
if (key2 === "." || key2.startsWith("..")) return;
|
|
@@ -2909,7 +2913,7 @@ var VibgrateLanguageServer = class {
|
|
|
2909
2913
|
quiet: true,
|
|
2910
2914
|
vibgrateVersion: VERSION
|
|
2911
2915
|
};
|
|
2912
|
-
const sub = await runCoreScanSilently(
|
|
2916
|
+
const sub = await runCoreScanSilently(path51.dirname(filePath), scanOpts, advanced);
|
|
2913
2917
|
const proj = (sub.projects ?? []).find((p) => p.path === "." || p.path === "") ?? sub.projects?.[0];
|
|
2914
2918
|
if (!proj || !this.artifact) {
|
|
2915
2919
|
this.openManifestFailed.add(key2);
|
|
@@ -3217,7 +3221,7 @@ var VibgrateLanguageServer = class {
|
|
|
3217
3221
|
const ecosystem = osvEcosystem(proj.type);
|
|
3218
3222
|
if (!ecosystem) continue;
|
|
3219
3223
|
const relManifest = manifestRelativePath(proj);
|
|
3220
|
-
const absManifest =
|
|
3224
|
+
const absManifest = path51.resolve(this.opts.root, relManifest);
|
|
3221
3225
|
let text = null;
|
|
3222
3226
|
try {
|
|
3223
3227
|
if (fs12.existsSync(absManifest)) text = fs12.readFileSync(absManifest, "utf8");
|
|
@@ -3410,7 +3414,7 @@ function pruneArtifactProjects(a) {
|
|
|
3410
3414
|
return { ...a, projects, findings };
|
|
3411
3415
|
}
|
|
3412
3416
|
function projectByPath(a, projectPath) {
|
|
3413
|
-
const norm = (p) =>
|
|
3417
|
+
const norm = (p) => path51.normalize((p === "" ? "." : p).replace(/\\/g, "/"));
|
|
3414
3418
|
const want = norm(projectPath);
|
|
3415
3419
|
for (const proj of a.projects ?? []) {
|
|
3416
3420
|
if (norm(proj.path) === want) return proj;
|
|
@@ -3452,7 +3456,7 @@ function projectForFile2(root3, a, filePath) {
|
|
|
3452
3456
|
return projectForFile(root3, a.projects ?? [], filePath);
|
|
3453
3457
|
}
|
|
3454
3458
|
function sameProject(location, projectPath) {
|
|
3455
|
-
return
|
|
3459
|
+
return path51.normalize(location) === path51.normalize(projectPath);
|
|
3456
3460
|
}
|
|
3457
3461
|
function hasReleaseDates(a) {
|
|
3458
3462
|
for (const proj of a.projects ?? []) {
|
|
@@ -3522,7 +3526,7 @@ function buildBreakdown(rootDir, projects) {
|
|
|
3522
3526
|
const relManifest = manifestRelativePath(p);
|
|
3523
3527
|
if (isPrunedWorkspacePath(p.path) || isPrunedWorkspacePath(relManifest)) continue;
|
|
3524
3528
|
const ecosystem = osvEcosystem(p.type);
|
|
3525
|
-
const absManifest =
|
|
3529
|
+
const absManifest = path51.resolve(rootDir, relManifest);
|
|
3526
3530
|
let text = null;
|
|
3527
3531
|
try {
|
|
3528
3532
|
if (fs12.existsSync(absManifest)) text = fs12.readFileSync(absManifest, "utf8");
|
|
@@ -3590,34 +3594,34 @@ function buildBreakdown(rootDir, projects) {
|
|
|
3590
3594
|
}
|
|
3591
3595
|
function manifestRelativePath(proj) {
|
|
3592
3596
|
const base = proj.path === "." || proj.path === "" ? "" : proj.path;
|
|
3593
|
-
const
|
|
3597
|
+
const join39 = (name) => base ? path51.join(base, name) : name;
|
|
3594
3598
|
switch (proj.type) {
|
|
3595
3599
|
case "node":
|
|
3596
3600
|
case "typescript":
|
|
3597
|
-
return
|
|
3601
|
+
return join39("package.json");
|
|
3598
3602
|
case "python":
|
|
3599
|
-
return
|
|
3603
|
+
return join39("pyproject.toml");
|
|
3600
3604
|
case "go":
|
|
3601
|
-
return
|
|
3605
|
+
return join39("go.mod");
|
|
3602
3606
|
case "rust":
|
|
3603
|
-
return
|
|
3607
|
+
return join39("Cargo.toml");
|
|
3604
3608
|
case "ruby":
|
|
3605
|
-
return
|
|
3609
|
+
return join39("Gemfile");
|
|
3606
3610
|
case "php":
|
|
3607
|
-
return
|
|
3611
|
+
return join39("composer.json");
|
|
3608
3612
|
case "java":
|
|
3609
3613
|
case "kotlin":
|
|
3610
3614
|
case "scala":
|
|
3611
3615
|
case "groovy":
|
|
3612
|
-
return
|
|
3616
|
+
return join39("pom.xml");
|
|
3613
3617
|
case "dotnet":
|
|
3614
|
-
return
|
|
3618
|
+
return join39(proj.name ? `${proj.name}.csproj` : "Directory.Build.props");
|
|
3615
3619
|
case "dart":
|
|
3616
|
-
return
|
|
3620
|
+
return join39("pubspec.yaml");
|
|
3617
3621
|
case "elixir":
|
|
3618
|
-
return
|
|
3622
|
+
return join39("mix.exs");
|
|
3619
3623
|
default:
|
|
3620
|
-
return
|
|
3624
|
+
return join39("package.json");
|
|
3621
3625
|
}
|
|
3622
3626
|
}
|
|
3623
3627
|
function lockfileRelativePath(rootDir, proj) {
|
|
@@ -3648,9 +3652,9 @@ function lockfileRelativePath(rootDir, proj) {
|
|
|
3648
3652
|
const dirsToTry = base ? [base, ""] : [""];
|
|
3649
3653
|
for (const dir of dirsToTry) {
|
|
3650
3654
|
for (const name of candidateNames) {
|
|
3651
|
-
const rel = dir ?
|
|
3655
|
+
const rel = dir ? path51.join(dir, name) : name;
|
|
3652
3656
|
try {
|
|
3653
|
-
if (fs12.existsSync(
|
|
3657
|
+
if (fs12.existsSync(path51.join(rootDir, rel))) return rel;
|
|
3654
3658
|
} catch {
|
|
3655
3659
|
}
|
|
3656
3660
|
}
|
|
@@ -3721,7 +3725,7 @@ function lookupImportFiles(map, pkg) {
|
|
|
3721
3725
|
}
|
|
3722
3726
|
function firstPackageMentionLine(root3, relFile, pkg) {
|
|
3723
3727
|
try {
|
|
3724
|
-
const abs =
|
|
3728
|
+
const abs = path51.resolve(root3, relFile);
|
|
3725
3729
|
const stat2 = fs12.statSync(abs);
|
|
3726
3730
|
if (!stat2.isFile() || stat2.size > 256 * 1024) return null;
|
|
3727
3731
|
const text = fs12.readFileSync(abs, "utf8");
|
|
@@ -3952,7 +3956,7 @@ function registerLsp(program) {
|
|
|
3952
3956
|
root: root3,
|
|
3953
3957
|
// `--local` is the established air-gap flag across the CLI; honour it
|
|
3954
3958
|
// here too, so an offline editor session stays offline.
|
|
3955
|
-
offline: global.
|
|
3959
|
+
offline: global.offline === true,
|
|
3956
3960
|
diagnostics: opts.diagnostics === true,
|
|
3957
3961
|
graph: opts.graph !== false,
|
|
3958
3962
|
semantic: opts.semantic !== false,
|
|
@@ -3970,8 +3974,8 @@ function hookCommand(vgBin) {
|
|
|
3970
3974
|
return `${vgBin} hook pre-tool-use`;
|
|
3971
3975
|
}
|
|
3972
3976
|
function installClaudeHooks(root3, vgBin = "vg") {
|
|
3973
|
-
const file =
|
|
3974
|
-
const rel =
|
|
3977
|
+
const file = path51.join(root3, ".claude", "settings.json");
|
|
3978
|
+
const rel = path51.join(".claude", "settings.json");
|
|
3975
3979
|
let settings = {};
|
|
3976
3980
|
if (fs12.existsSync(file)) {
|
|
3977
3981
|
try {
|
|
@@ -4012,8 +4016,8 @@ function installClaudeHooks(root3, vgBin = "vg") {
|
|
|
4012
4016
|
return { file: rel, status: "written" };
|
|
4013
4017
|
}
|
|
4014
4018
|
function uninstallClaudeHooks(root3) {
|
|
4015
|
-
const file =
|
|
4016
|
-
const rel =
|
|
4019
|
+
const file = path51.join(root3, ".claude", "settings.json");
|
|
4020
|
+
const rel = path51.join(".claude", "settings.json");
|
|
4017
4021
|
if (!fs12.existsSync(file)) return { file: rel, status: "unchanged" };
|
|
4018
4022
|
let settings;
|
|
4019
4023
|
try {
|
|
@@ -4039,7 +4043,7 @@ function uninstallClaudeHooks(root3) {
|
|
|
4039
4043
|
return { file: rel, status: "written" };
|
|
4040
4044
|
}
|
|
4041
4045
|
function writeSettings(file, settings) {
|
|
4042
|
-
fs12.mkdirSync(
|
|
4046
|
+
fs12.mkdirSync(path51.dirname(file), { recursive: true });
|
|
4043
4047
|
fs12.writeFileSync(file, `${JSON.stringify(settings, null, 2)}
|
|
4044
4048
|
`);
|
|
4045
4049
|
}
|
|
@@ -4159,19 +4163,19 @@ function disabled() {
|
|
|
4159
4163
|
return v === "1" || v === "true";
|
|
4160
4164
|
}
|
|
4161
4165
|
function hcsModuleDir() {
|
|
4162
|
-
return
|
|
4166
|
+
return path51.join(modulesBaseDir(), "hcs");
|
|
4163
4167
|
}
|
|
4164
4168
|
function candidatePaths() {
|
|
4165
4169
|
const custom = process.env.VIBGRATE_HCS_PATH;
|
|
4166
4170
|
if (custom) {
|
|
4167
|
-
const p =
|
|
4171
|
+
const p = path51.resolve(custom);
|
|
4168
4172
|
try {
|
|
4169
|
-
if (fs12.statSync(p).isDirectory()) return [
|
|
4173
|
+
if (fs12.statSync(p).isDirectory()) return [path51.join(p, "index.js")];
|
|
4170
4174
|
} catch {
|
|
4171
4175
|
}
|
|
4172
4176
|
return [p];
|
|
4173
4177
|
}
|
|
4174
|
-
return [
|
|
4178
|
+
return [path51.join(hcsModuleDir(), "index.js")];
|
|
4175
4179
|
}
|
|
4176
4180
|
var cached;
|
|
4177
4181
|
function resetHcsEngineCache() {
|
|
@@ -4321,11 +4325,11 @@ function requireModule(name) {
|
|
|
4321
4325
|
return mod;
|
|
4322
4326
|
}
|
|
4323
4327
|
function confirm(question) {
|
|
4324
|
-
return new Promise((
|
|
4328
|
+
return new Promise((resolve25) => {
|
|
4325
4329
|
const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
|
|
4326
4330
|
rl.question(question, (answer) => {
|
|
4327
4331
|
rl.close();
|
|
4328
|
-
|
|
4332
|
+
resolve25(answer.trim() === "" || /^y(es)?$/i.test(answer.trim()));
|
|
4329
4333
|
});
|
|
4330
4334
|
});
|
|
4331
4335
|
}
|
|
@@ -4391,7 +4395,7 @@ function registerShare(program) {
|
|
|
4391
4395
|
const cmd = program.command("share").description("make the map committable + auto-updating for your team (git)").option("--undo", "reverse what `vg share` installed").option("--reports", "also commit graph.html / GRAPH_REPORT.md (default: gitignored)").action(function(opts) {
|
|
4392
4396
|
const global = readGlobal(this);
|
|
4393
4397
|
const root3 = rootOf(global);
|
|
4394
|
-
const gitDir =
|
|
4398
|
+
const gitDir = path51.join(root3, ".git");
|
|
4395
4399
|
if (!fs12.existsSync(gitDir)) {
|
|
4396
4400
|
throw new CliError("not a git repository \u2014 run `vg share` inside a git repo", ExitCode.USAGE_ERROR);
|
|
4397
4401
|
}
|
|
@@ -4413,15 +4417,15 @@ function registerShare(program) {
|
|
|
4413
4417
|
applyGlobalOptions(cmd);
|
|
4414
4418
|
}
|
|
4415
4419
|
function writeVibgrateGitignore(root3, reports) {
|
|
4416
|
-
const dir =
|
|
4420
|
+
const dir = path51.join(root3, ".vibgrate");
|
|
4417
4421
|
fs12.mkdirSync(dir, { recursive: true });
|
|
4418
4422
|
const lines = ["# Managed by `vg share` \u2014 keep graph.json committed, ignore the rest", "cache/", "facts.jsonl", "graph.snap"];
|
|
4419
4423
|
if (!reports) lines.push("graph.html", "GRAPH_REPORT.md");
|
|
4420
|
-
fs12.writeFileSync(
|
|
4424
|
+
fs12.writeFileSync(path51.join(dir, ".gitignore"), `${lines.join("\n")}
|
|
4421
4425
|
`);
|
|
4422
4426
|
}
|
|
4423
4427
|
function writeGitAttributes(root3) {
|
|
4424
|
-
const file =
|
|
4428
|
+
const file = path51.join(root3, ".gitattributes");
|
|
4425
4429
|
const line = ".vibgrate/graph.json merge=vg";
|
|
4426
4430
|
let content = fs12.existsSync(file) ? fs12.readFileSync(file, "utf8") : "";
|
|
4427
4431
|
if (!content.split("\n").some((l) => l.trim() === line)) {
|
|
@@ -4439,7 +4443,7 @@ function configureMergeDriver(root3) {
|
|
|
4439
4443
|
function installPreCommitHook(root3) {
|
|
4440
4444
|
const hooksDir = hookDir(root3);
|
|
4441
4445
|
fs12.mkdirSync(hooksDir, { recursive: true });
|
|
4442
|
-
const file =
|
|
4446
|
+
const file = path51.join(hooksDir, "pre-commit");
|
|
4443
4447
|
const block = [
|
|
4444
4448
|
HOOK_BEGIN,
|
|
4445
4449
|
"if command -v vg >/dev/null 2>&1; then",
|
|
@@ -4461,14 +4465,14 @@ ${content}`;
|
|
|
4461
4465
|
fs12.chmodSync(file, 493);
|
|
4462
4466
|
}
|
|
4463
4467
|
function undo(root3) {
|
|
4464
|
-
const hookFile =
|
|
4468
|
+
const hookFile = path51.join(hookDir(root3), "pre-commit");
|
|
4465
4469
|
if (fs12.existsSync(hookFile)) {
|
|
4466
4470
|
const re = new RegExp(`\\n*${escapeRe(HOOK_BEGIN)}[\\s\\S]*?${escapeRe(HOOK_END)}\\n*`);
|
|
4467
4471
|
const next2 = fs12.readFileSync(hookFile, "utf8").replace(re, "\n");
|
|
4468
4472
|
if (next2.trim() === "#!/bin/sh") fs12.rmSync(hookFile);
|
|
4469
4473
|
else fs12.writeFileSync(hookFile, next2);
|
|
4470
4474
|
}
|
|
4471
|
-
const attr =
|
|
4475
|
+
const attr = path51.join(root3, ".gitattributes");
|
|
4472
4476
|
if (fs12.existsSync(attr)) {
|
|
4473
4477
|
const next2 = fs12.readFileSync(attr, "utf8").split("\n").filter((l) => l.trim() !== ".vibgrate/graph.json merge=vg").join("\n");
|
|
4474
4478
|
fs12.writeFileSync(attr, next2);
|
|
@@ -4481,10 +4485,10 @@ function undo(root3) {
|
|
|
4481
4485
|
function hookDir(root3) {
|
|
4482
4486
|
try {
|
|
4483
4487
|
const configured = git(root3, ["config", "--get", "core.hooksPath"]).trim();
|
|
4484
|
-
if (configured) return
|
|
4488
|
+
if (configured) return path51.resolve(root3, configured);
|
|
4485
4489
|
} catch {
|
|
4486
4490
|
}
|
|
4487
|
-
return
|
|
4491
|
+
return path51.join(root3, ".git", "hooks");
|
|
4488
4492
|
}
|
|
4489
4493
|
function git(root3, args) {
|
|
4490
4494
|
return execFileSync("git", args, { cwd: root3, encoding: "utf8" });
|
|
@@ -4498,7 +4502,7 @@ function registerWatch(program) {
|
|
|
4498
4502
|
const global = readGlobal(this);
|
|
4499
4503
|
const root3 = rootOf(global);
|
|
4500
4504
|
const debounceMs = Math.max(50, Number(opts.debounce) || DEFAULT_DEBOUNCE_MS);
|
|
4501
|
-
const watchRoots = (paths.length ? paths : ["."]).map((p) =>
|
|
4505
|
+
const watchRoots = (paths.length ? paths : ["."]).map((p) => path51.resolve(root3, p));
|
|
4502
4506
|
for (const w of watchRoots) {
|
|
4503
4507
|
if (!fs12.existsSync(w)) {
|
|
4504
4508
|
throw new CliError(`watch path not found: ${w}`, ExitCode.USAGE_ERROR);
|
|
@@ -4566,12 +4570,12 @@ function registerWatch(program) {
|
|
|
4566
4570
|
throw new CliError(`cannot watch ${w}: ${err.message}`, ExitCode.ERROR);
|
|
4567
4571
|
}
|
|
4568
4572
|
}
|
|
4569
|
-
info(c.dim(`vg watch \xB7 watching ${watchRoots.map((p) =>
|
|
4570
|
-
await new Promise((
|
|
4573
|
+
info(c.dim(`vg watch \xB7 watching ${watchRoots.map((p) => path51.relative(root3, p) || ".").join(", ")} (debounce ${debounceMs}ms) \xB7 Ctrl+C to stop`));
|
|
4574
|
+
await new Promise((resolve25) => {
|
|
4571
4575
|
const stop = () => {
|
|
4572
4576
|
for (const w of watchers) w.close();
|
|
4573
4577
|
if (timer) clearTimeout(timer);
|
|
4574
|
-
|
|
4578
|
+
resolve25();
|
|
4575
4579
|
};
|
|
4576
4580
|
process.once("SIGINT", stop);
|
|
4577
4581
|
process.once("SIGTERM", stop);
|
|
@@ -4899,7 +4903,7 @@ function normalizeStandards(raw) {
|
|
|
4899
4903
|
}
|
|
4900
4904
|
function loadStandards(root3) {
|
|
4901
4905
|
for (const rel of STANDARDS_FILES) {
|
|
4902
|
-
const p =
|
|
4906
|
+
const p = path51__default.join(root3, rel);
|
|
4903
4907
|
let text;
|
|
4904
4908
|
try {
|
|
4905
4909
|
text = fs12__default.readFileSync(p, "utf8");
|
|
@@ -4928,14 +4932,14 @@ function registerDrift(program) {
|
|
|
4928
4932
|
const cmd = program.command("drift").description("what is outdated across dependencies (offline; --online for currency)").option("--online", "query the npm registry for latest versions (opt-in network)").option("--fail-on <level>", "exit 2 on violations \u2014 major|minor (version drift) or standards (enterprise policy)").action(async function(opts) {
|
|
4929
4933
|
const global = readGlobal(this);
|
|
4930
4934
|
const root3 = rootOf(global);
|
|
4931
|
-
if (opts.online && global.
|
|
4935
|
+
if (opts.online && global.offline) {
|
|
4932
4936
|
throw new CliError("--online conflicts with --local (no network in local mode)", ExitCode.USAGE_ERROR);
|
|
4933
4937
|
}
|
|
4934
4938
|
if (opts.failOn && !["major", "minor", "standards"].includes(opts.failOn)) {
|
|
4935
4939
|
throw new CliError(`--fail-on must be one of: major, minor, standards (got "${opts.failOn}")`, ExitCode.USAGE_ERROR);
|
|
4936
4940
|
}
|
|
4937
4941
|
const inv = inventory(root3);
|
|
4938
|
-
if (opts.online && !global.
|
|
4942
|
+
if (opts.online && !global.offline) await enrichOnline(inv.records);
|
|
4939
4943
|
let standards = null;
|
|
4940
4944
|
if (opts.failOn === "standards") {
|
|
4941
4945
|
const loaded = loadStandards(root3);
|
|
@@ -5620,8 +5624,8 @@ async function runHostBench(options = {}) {
|
|
|
5620
5624
|
}
|
|
5621
5625
|
async function runArmSimulate(input) {
|
|
5622
5626
|
const { arm, lib, modelPath, turns, trie, env, live } = input;
|
|
5623
|
-
const
|
|
5624
|
-
const session = await acquireHostSession(lib,
|
|
5627
|
+
const path53 = `${modelPath}#${arm.id}`;
|
|
5628
|
+
const session = await acquireHostSession(lib, path53);
|
|
5625
5629
|
const system = "You are a coding agent with a Task Capsule.";
|
|
5626
5630
|
const capsule = "function readConfig(): Config { return load(); }";
|
|
5627
5631
|
let last = null;
|
|
@@ -6567,7 +6571,7 @@ function registerModels(program) {
|
|
|
6567
6571
|
);
|
|
6568
6572
|
});
|
|
6569
6573
|
applyGlobalOptions(modeCmd);
|
|
6570
|
-
const
|
|
6574
|
+
const resolve25 = cmd.command("resolve").description("resolve a Code Mode to pack + underlying model + fit (no download)").argument("[mode]", "spark | flow | forge (default: auto-fit or configured default)").action(async function(modeArg) {
|
|
6571
6575
|
const global = readGlobal(this);
|
|
6572
6576
|
const root3 = rootOf(global);
|
|
6573
6577
|
const sys = await gatherSystemMemory();
|
|
@@ -6599,7 +6603,7 @@ function registerModels(program) {
|
|
|
6599
6603
|
info(c.yellow(" will not fit \u2014 choose a lower mode or free memory before loading"));
|
|
6600
6604
|
}
|
|
6601
6605
|
});
|
|
6602
|
-
applyGlobalOptions(
|
|
6606
|
+
applyGlobalOptions(resolve25);
|
|
6603
6607
|
const installCmd = cmd.command("install").description("resolve the best Code Mode for this machine and install the pack").argument("[mode]", "spark | flow | forge (default: auto-fit)").option("--dry-run", "print the install plan only \u2014 do not download").option("--yes", "skip prompts (install already runs by default)").action(async function(modeArg, opts) {
|
|
6604
6608
|
const global = readGlobal(this);
|
|
6605
6609
|
const root3 = rootOf(global);
|
|
@@ -6636,7 +6640,7 @@ function registerModels(program) {
|
|
|
6636
6640
|
promise: resolved.pack.promise,
|
|
6637
6641
|
fit: resolved.fit.label,
|
|
6638
6642
|
installedNames: models.map((m) => m.name),
|
|
6639
|
-
offline: !!global.
|
|
6643
|
+
offline: !!global.offline,
|
|
6640
6644
|
hasBinary,
|
|
6641
6645
|
downloadUrl: resolved.underlying.downloadUrl,
|
|
6642
6646
|
sha256: resolved.underlying.sha256
|
|
@@ -6664,7 +6668,7 @@ function registerModels(program) {
|
|
|
6664
6668
|
if (installPlan.license) info(c.dim(` license: ${installPlan.license}`));
|
|
6665
6669
|
}
|
|
6666
6670
|
const result = await executeModelInstallPlan(installPlan, {
|
|
6667
|
-
offline: !!global.
|
|
6671
|
+
offline: !!global.offline,
|
|
6668
6672
|
quiet: !!global.json,
|
|
6669
6673
|
onProgress: (line) => {
|
|
6670
6674
|
if (!global.json) info(c.dim(` ${line}`));
|
|
@@ -6733,7 +6737,7 @@ function registerModels(program) {
|
|
|
6733
6737
|
backend: "ollama",
|
|
6734
6738
|
weightsRef: name,
|
|
6735
6739
|
installedNames: models.map((m) => m.name),
|
|
6736
|
-
offline: !!global.
|
|
6740
|
+
offline: !!global.offline,
|
|
6737
6741
|
hasBinary
|
|
6738
6742
|
});
|
|
6739
6743
|
if (installPlan.ready) {
|
|
@@ -6751,7 +6755,7 @@ function registerModels(program) {
|
|
|
6751
6755
|
}
|
|
6752
6756
|
if (!global.json) printInstallPlanHuman("vg models pull", installPlan);
|
|
6753
6757
|
const result = await executeModelInstallPlan(installPlan, {
|
|
6754
|
-
offline: !!global.
|
|
6758
|
+
offline: !!global.offline,
|
|
6755
6759
|
quiet: !!global.json,
|
|
6756
6760
|
onProgress: (line) => {
|
|
6757
6761
|
if (!global.json) info(c.dim(` ${line}`));
|
|
@@ -6988,8 +6992,8 @@ function registerModels(program) {
|
|
|
6988
6992
|
}
|
|
6989
6993
|
});
|
|
6990
6994
|
if (opts.out) {
|
|
6991
|
-
const outPath =
|
|
6992
|
-
fs12.mkdirSync(
|
|
6995
|
+
const outPath = path51.resolve(opts.out);
|
|
6996
|
+
fs12.mkdirSync(path51.dirname(outPath), { recursive: true });
|
|
6993
6997
|
fs12.writeFileSync(outPath, `${JSON.stringify(report, null, 2)}
|
|
6994
6998
|
`);
|
|
6995
6999
|
if (!global.json) info(c.dim(` wrote ${outPath}`));
|
|
@@ -7011,9 +7015,9 @@ function registerModels(program) {
|
|
|
7011
7015
|
}
|
|
7012
7016
|
});
|
|
7013
7017
|
applyGlobalOptions(codingMetrics);
|
|
7014
|
-
const catalog = cmd.command("catalog").description("the live hosted model catalog (OpenRouter) \u2014 key-free, cached, refreshed from the network").option("--
|
|
7018
|
+
const catalog = cmd.command("catalog").description("the live hosted model catalog (OpenRouter) \u2014 key-free, cached, refreshed from the network").option("--refresh", "bypass the cache and refresh from the network").action(async function(opts) {
|
|
7015
7019
|
const global = readGlobal(this);
|
|
7016
|
-
const cat = await fetchCatalog({ offline:
|
|
7020
|
+
const cat = await fetchCatalog({ offline: global.offline, noCache: opts.refresh });
|
|
7017
7021
|
if (global.json) {
|
|
7018
7022
|
json(cat);
|
|
7019
7023
|
return;
|
|
@@ -7104,11 +7108,11 @@ function fitColor(label) {
|
|
|
7104
7108
|
return label;
|
|
7105
7109
|
}
|
|
7106
7110
|
function confirmYesNo(question) {
|
|
7107
|
-
return new Promise((
|
|
7111
|
+
return new Promise((resolve25) => {
|
|
7108
7112
|
const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
|
|
7109
7113
|
rl.question(`${question} [y/N] `, (answer) => {
|
|
7110
7114
|
rl.close();
|
|
7111
|
-
|
|
7115
|
+
resolve25(/^y(es)?$/i.test(answer.trim()));
|
|
7112
7116
|
});
|
|
7113
7117
|
});
|
|
7114
7118
|
}
|
|
@@ -7139,14 +7143,14 @@ function resolveUninstallTarget(name, runtimeOpt) {
|
|
|
7139
7143
|
}
|
|
7140
7144
|
return exact[0];
|
|
7141
7145
|
}
|
|
7142
|
-
const base =
|
|
7146
|
+
const base = path51.basename(wantLc);
|
|
7143
7147
|
const soft = fleet.filter((m) => {
|
|
7144
7148
|
const n = m.name.toLowerCase();
|
|
7145
|
-
return n === base || n.endsWith("/" + base) || n.endsWith(wantLc) ||
|
|
7149
|
+
return n === base || n.endsWith("/" + base) || n.endsWith(wantLc) || path51.basename(n) === base;
|
|
7146
7150
|
});
|
|
7147
7151
|
if (soft.length === 1) return soft[0];
|
|
7148
7152
|
if (soft.length > 1) {
|
|
7149
|
-
const baseHits = soft.filter((m) =>
|
|
7153
|
+
const baseHits = soft.filter((m) => path51.basename(m.name).toLowerCase() === base);
|
|
7150
7154
|
const pool = baseHits.length ? baseHits : soft;
|
|
7151
7155
|
const order = ["ollama", "gguf", "lm-studio"];
|
|
7152
7156
|
for (const rt of order) {
|
|
@@ -7184,7 +7188,7 @@ function printBackends(backends) {
|
|
|
7184
7188
|
}
|
|
7185
7189
|
}
|
|
7186
7190
|
function codeConfigPath(root3) {
|
|
7187
|
-
return
|
|
7191
|
+
return path51.join(root3, ".vibgrate", "code.json");
|
|
7188
7192
|
}
|
|
7189
7193
|
function loadCodeConfig(root3) {
|
|
7190
7194
|
try {
|
|
@@ -7266,7 +7270,7 @@ function discoverMcpServers(root3, ownServers) {
|
|
|
7266
7270
|
const servers = {};
|
|
7267
7271
|
const sources = [];
|
|
7268
7272
|
for (const rel of MCP_CONFIG_FILES) {
|
|
7269
|
-
const found = readMcpServersFile(
|
|
7273
|
+
const found = readMcpServersFile(path51.join(root3, rel));
|
|
7270
7274
|
const names = Object.keys(found);
|
|
7271
7275
|
if (names.length) {
|
|
7272
7276
|
sources.push(rel);
|
|
@@ -7282,7 +7286,7 @@ function discoverMcpServers(root3, ownServers) {
|
|
|
7282
7286
|
|
|
7283
7287
|
// src/commands/code.ts
|
|
7284
7288
|
function registerCode(program) {
|
|
7285
|
-
const cmd = program.command("code").description("graph-grounded coding agent (guided mode with no instruction; every edit and command is approved)").argument("[instruction...]", "what to change, in plain language (omit for guided interactive mode)").option("--provider <id>", "backend: vibgrate-relay (Vibgrate Relay), ollama, lmstudio, foundry-local, openrouter, litellm, openai, together, llama-cpp").option("--model <id>", "model id (or set VG_CODE_MODEL). No model is hard-coded \u2014 pick the current best.").option("--mode <mode>", "Code Mode: spark | flow | forge (preferred over raw model names; auto-fits when omitted and no --model)").option("--model-path <gguf>", "gguf path for --provider llama-cpp (weights are never auto-downloaded)").option("-f, --file <path>", "restrict the edit surface to this file (repeatable)", collect, []).option("-b, --budget <n>", "approx context token budget", "3000").option("--apply", "one-shot path (--single/--mock) only: write the change (still requires --yes or an interactive confirm)").option("--yes", "consent to write / to a first-use package install, non-interactively").option("--auto", "autonomous agent: auto-approve every edit and command (use with care)").option("--max-steps <n>", "cap the number of agent steps
|
|
7289
|
+
const cmd = program.command("code").description("graph-grounded coding agent (guided mode with no instruction; every edit and command is approved)").argument("[instruction...]", "what to change, in plain language (omit for guided interactive mode)").option("--provider <id>", "backend: vibgrate-relay (Vibgrate Relay), ollama, lmstudio, foundry-local, openrouter, litellm, openai, together, llama-cpp").option("--model <id>", "model id (or set VG_CODE_MODEL). No model is hard-coded \u2014 pick the current best.").option("--mode <mode>", "Code Mode: spark | flow | forge (preferred over raw model names; auto-fits when omitted and no --model)").option("--model-path <gguf>", "gguf path for --provider llama-cpp (weights are never auto-downloaded)").option("-f, --file <path>", "restrict the edit surface to this file (repeatable)", collect, []).option("-b, --budget <n>", "approx context token budget", "3000").option("--apply", "one-shot path (--single/--mock) only: write the change (still requires --yes or an interactive confirm)").option("--yes", "consent to write / to a first-use package install, non-interactively").option("--auto", "autonomous agent: auto-approve every edit and command (use with care)").option("--max-steps <n>", "cap the number of agent steps (default 24; also settable as maxSteps in vibgrate.config.json)").option("--single", "one-shot planner (single edit) instead of the multi-step agent").option("--stream", "stream the model output live").option("--stream-json", "machine protocol: NDJSON agent events on stdout, approval decisions on stdin (for host UIs like the VS Code panel)").option("--session", "with --stream-json: stay open for further turns (warm graph and tools) instead of exiting after one").option("--verify [command]", "after the agent finishes, run tests and make it fix failures (uses the config testCommand if no command is given)").option("--continue [id]", "resume a session: the most recent one, or the given session id (recap + restore /undo)").option("--restore-checkpoint <commit>", "one-shot: restore the given --file paths from a checkpoint commit, then exit (for host UIs after the original session ended)").option("--reasoning-effort <level>", "how hard a reasoning-capable model should think: low | medium | high (ignored by models without the knob)").option("--worktree [id]", "run the session in an isolated git worktree under .vibgrate/worktrees (bare: create a new one; with id: reuse it)").option("--worktree-diff <id>", "one-shot: print the worktree's delta against its base as a patch, then exit").option("--worktree-apply <id>", "one-shot: apply the worktree's delta onto the main tree (git apply --3way), then exit").option("--worktree-remove <id>", "one-shot: remove the worktree checkout, then exit").option("--capsule", "use a source-bearing Task Capsule for first context (Fusion A/B; exact source ranges from the graph)").option("--no-capsule", "disable Task Capsule context even if enabled in .vibgrate/code.json").option("--security-tier <tier>", "shell isolation: L0 (host), L1 (Seatbelt/bubblewrap when available), L2/L3 reserved").option("--mock <file>", "use a scripted reply from <file> instead of a model (offline; for tests/CI)").option("-o, --out <file>", "write the JSON result to <file> (implies --json shape; for CI/benchmarks)").action(async function(instructionParts, opts) {
|
|
7286
7290
|
const global = readGlobal(this);
|
|
7287
7291
|
let instruction = (instructionParts ?? []).join(" ").trim();
|
|
7288
7292
|
if (typeof opts.continue === "string") {
|
|
@@ -7436,7 +7440,7 @@ function registerCode(program) {
|
|
|
7436
7440
|
}
|
|
7437
7441
|
global.cwd = activeWorktree.path;
|
|
7438
7442
|
}
|
|
7439
|
-
if (!global.
|
|
7443
|
+
if (!global.offline) {
|
|
7440
7444
|
await ensureRelevanceModule();
|
|
7441
7445
|
}
|
|
7442
7446
|
const config = loadCodeConfig(rootOf(global));
|
|
@@ -7449,7 +7453,7 @@ function registerCode(program) {
|
|
|
7449
7453
|
try {
|
|
7450
7454
|
const { parseMode: parseMode2, resolveMode: resolveMode2, readOrchestratorState: readOrchestratorState2, activePin: activePin2 } = await import('./model-orchestrator-X3NE3Y2G.js');
|
|
7451
7455
|
const { gatherSystemMemory: gatherSystemMemory2 } = await import('./local-runtime-GA43KYS5.js');
|
|
7452
|
-
const { loadGraph: loadGraph2 } = await import('./load-
|
|
7456
|
+
const { loadGraph: loadGraph2 } = await import('./load-UAVA732M.js');
|
|
7453
7457
|
const state = readOrchestratorState2();
|
|
7454
7458
|
const mode = parseMode2(modeFlag) ?? state.defaultMode ?? void 0;
|
|
7455
7459
|
const pin = mode ? activePin2(mode, state) : null;
|
|
@@ -7522,7 +7526,8 @@ function registerCode(program) {
|
|
|
7522
7526
|
}
|
|
7523
7527
|
}
|
|
7524
7528
|
const auto = opts.auto ?? config.auto;
|
|
7525
|
-
const
|
|
7529
|
+
const parsedMaxSteps = opts.maxSteps === void 0 ? NaN : Number(opts.maxSteps);
|
|
7530
|
+
const maxSteps = Number.isFinite(parsedMaxSteps) && parsedMaxSteps > 0 ? Math.floor(parsedMaxSteps) : config.maxSteps;
|
|
7526
7531
|
const capsule = typeof opts.capsule === "boolean" ? opts.capsule : !!config.capsule;
|
|
7527
7532
|
const securityTier = resolveSecurityTierFlag(opts.securityTier, config.securityTier, !!auto);
|
|
7528
7533
|
const contextBudget = contextBudgetFor(config);
|
|
@@ -7534,7 +7539,7 @@ function registerCode(program) {
|
|
|
7534
7539
|
if (!(process.stdin.isTTY && process.stdout.isTTY)) {
|
|
7535
7540
|
throw usageError('say what to change, e.g. `vg code "add a --timeout flag to the scan command"` (guided mode needs an interactive terminal)');
|
|
7536
7541
|
}
|
|
7537
|
-
const { runInteractive } = await import('./interactive-
|
|
7542
|
+
const { runInteractive } = await import('./interactive-VQM2CTE2.js');
|
|
7538
7543
|
await runInteractive(
|
|
7539
7544
|
rootOf(global),
|
|
7540
7545
|
global,
|
|
@@ -7564,10 +7569,10 @@ function registerCode(program) {
|
|
|
7564
7569
|
}
|
|
7565
7570
|
if (!instruction && !stdinFirstTurn) throw usageError('say what to change, e.g. `vg code "add a --timeout flag to the scan command"`');
|
|
7566
7571
|
{
|
|
7567
|
-
const { ensureCodeMap } = await import('./ensure-map-
|
|
7572
|
+
const { ensureCodeMap: ensureCodeMap2 } = await import('./ensure-map-VAIYCCEJ.js');
|
|
7568
7573
|
const mapRoot = rootOf(global);
|
|
7569
7574
|
try {
|
|
7570
|
-
await
|
|
7575
|
+
await ensureCodeMap2(mapRoot, global, {
|
|
7571
7576
|
onProgress: (p) => {
|
|
7572
7577
|
if (opts.streamJson) {
|
|
7573
7578
|
process.stdout.write(
|
|
@@ -7624,7 +7629,7 @@ function registerCode(program) {
|
|
|
7624
7629
|
const ensure = await ensureManagerRuntime({
|
|
7625
7630
|
providerId: route.providers[0].id,
|
|
7626
7631
|
model: route.providers[0].model,
|
|
7627
|
-
offline: !!global.
|
|
7632
|
+
offline: !!global.offline,
|
|
7628
7633
|
// Code Mode / explicit custom selection / --yes count as consent for
|
|
7629
7634
|
// npm runtime deps (and ollama pull when the binary is already present).
|
|
7630
7635
|
consent: !!(opts.yes || codeModeActive || customSelection || process.stdin.isTTY),
|
|
@@ -7649,20 +7654,20 @@ function registerCode(program) {
|
|
|
7649
7654
|
process.stdout.write(JSON.stringify(line) + "\n");
|
|
7650
7655
|
};
|
|
7651
7656
|
try {
|
|
7652
|
-
const { RunOutcomeRecorder, newOneShotRunId, turnRunId } = await import('./run-outcome-
|
|
7657
|
+
const { RunOutcomeRecorder, newOneShotRunId, turnRunId } = await import('./run-outcome-EATWIKLR.js');
|
|
7653
7658
|
const primarySlug = route.providers[0]?.model.includes("/") ? route.providers[0].model.split("/")[0] : route.providers[0]?.id;
|
|
7654
|
-
const { runCodeStreamJson, parseHostMessage } = await import('./stream-json-
|
|
7655
|
-
const { nodeCodeFs } = await import('./session-
|
|
7659
|
+
const { runCodeStreamJson, parseHostMessage } = await import('./stream-json-GCZAHORX.js');
|
|
7660
|
+
const { nodeCodeFs } = await import('./session-N2FTYIHQ.js');
|
|
7656
7661
|
const readline6 = await import('readline');
|
|
7657
7662
|
const child = await import('child_process');
|
|
7658
7663
|
const { loadOrDiscoverFederation } = await import('./federation-M4ZNXOPX.js');
|
|
7659
7664
|
const { resolveExecutionEnv } = await import('./execution-env-4MS5AYAN.js');
|
|
7660
7665
|
const { resolveModelExecutionProfile, mergeModelExecutionProfile } = await import('./model-execution-profile-ELWF3Z6H.js');
|
|
7661
|
-
const { startCodeRuntimeSession } = await import('./runtime-session-
|
|
7662
|
-
const { resolveGraphBackend } = await import('./graph-backend-
|
|
7666
|
+
const { startCodeRuntimeSession } = await import('./runtime-session-Y2BRBXUW.js');
|
|
7667
|
+
const { resolveGraphBackend } = await import('./graph-backend-V5A2H5ZB.js');
|
|
7663
7668
|
const { detectGitRef } = await import('./git-ref-LMT3EXYY.js');
|
|
7664
7669
|
const { repositoryIdFromRoot } = await import('./paths-RVK6FKRN.js');
|
|
7665
|
-
const { vgdRequest } = await import('./vgd-
|
|
7670
|
+
const { vgdRequest } = await import('./vgd-3A2TRNP5.js');
|
|
7666
7671
|
const federation = loadOrDiscoverFederation(root3);
|
|
7667
7672
|
const modelProfile = mergeModelExecutionProfile(
|
|
7668
7673
|
resolveModelExecutionProfile({
|
|
@@ -7720,7 +7725,7 @@ ${res.stderr}` : ""),
|
|
|
7720
7725
|
exitCode: res.status ?? 1
|
|
7721
7726
|
};
|
|
7722
7727
|
};
|
|
7723
|
-
const { createVgBuiltinMcpTools, mergeExternalToolsets } = await import('./vg-mcp-bridge-
|
|
7728
|
+
const { createVgBuiltinMcpTools, mergeExternalToolsets } = await import('./vg-mcp-bridge-VDZWFEO2.js');
|
|
7724
7729
|
const vgBuiltin = createVgBuiltinMcpTools({
|
|
7725
7730
|
getGraph: () => graph,
|
|
7726
7731
|
root: root3,
|
|
@@ -7765,8 +7770,8 @@ ${res.stderr}` : ""),
|
|
|
7765
7770
|
files,
|
|
7766
7771
|
indexFile: checkpointIndex
|
|
7767
7772
|
});
|
|
7768
|
-
const { loadApprovalRules, addApprovalRule, ruleForAction, ruleLabel } = await import('./approvals-
|
|
7769
|
-
const { loadProjectRules, nodeRulesReader } = await import('./rules-
|
|
7773
|
+
const { loadApprovalRules, addApprovalRule, ruleForAction, ruleLabel } = await import('./approvals-ISJFILVK.js');
|
|
7774
|
+
const { loadProjectRules, nodeRulesReader } = await import('./rules-TLRGB5L4.js');
|
|
7770
7775
|
const projectRules = loadProjectRules(nodeRulesReader(root3));
|
|
7771
7776
|
if (projectRules.sources.length && !global.quiet) {
|
|
7772
7777
|
emitStream({
|
|
@@ -7823,8 +7828,8 @@ ${res.stderr}` : ""),
|
|
|
7823
7828
|
};
|
|
7824
7829
|
try {
|
|
7825
7830
|
if (opts.session) {
|
|
7826
|
-
const { runCodeStreamJsonSession, STREAM_JSON_PROTOCOL_VERSION } = await import('./stream-json-
|
|
7827
|
-
const { runAgent: runAgent2 } = await import('./agent-
|
|
7831
|
+
const { runCodeStreamJsonSession, STREAM_JSON_PROTOCOL_VERSION } = await import('./stream-json-GCZAHORX.js');
|
|
7832
|
+
const { runAgent: runAgent2 } = await import('./agent-5GW7C7EB.js');
|
|
7828
7833
|
const {
|
|
7829
7834
|
condenseSession,
|
|
7830
7835
|
loadLatestSession,
|
|
@@ -7835,7 +7840,7 @@ ${res.stderr}` : ""),
|
|
|
7835
7840
|
sessionsDir,
|
|
7836
7841
|
summarizeSession
|
|
7837
7842
|
} = await import('./session-store-AQUORKWB.js');
|
|
7838
|
-
const { VERSION: VERSION2 } = await import('./version-
|
|
7843
|
+
const { VERSION: VERSION2 } = await import('./version-XNZ4GLMQ.js');
|
|
7839
7844
|
const resumedRecord = typeof opts.continue === "string" ? loadSession(mainRoot, opts.continue) : opts.continue ? loadLatestSession(mainRoot) : void 0;
|
|
7840
7845
|
if (typeof opts.continue === "string" && !resumedRecord) {
|
|
7841
7846
|
emitStream({
|
|
@@ -7867,8 +7872,8 @@ ${res.stderr}` : ""),
|
|
|
7867
7872
|
if (ended) return Promise.resolve(null);
|
|
7868
7873
|
const queued = pendingTurns.shift();
|
|
7869
7874
|
if (queued !== void 0) return Promise.resolve(queued);
|
|
7870
|
-
return new Promise((
|
|
7871
|
-
wakeTurn =
|
|
7875
|
+
return new Promise((resolve25) => {
|
|
7876
|
+
wakeTurn = resolve25;
|
|
7872
7877
|
});
|
|
7873
7878
|
};
|
|
7874
7879
|
let turnIndex = 0;
|
|
@@ -8047,8 +8052,8 @@ ${res.stderr}` : ""),
|
|
|
8047
8052
|
if (!autonomous && !tty) {
|
|
8048
8053
|
throw usageError("the agent approves each edit/command \u2014 run in a terminal to approve interactively, or pass --auto to run autonomously (or --single for a one-shot diff).");
|
|
8049
8054
|
}
|
|
8050
|
-
const { agentTask } = await import('./interactive-
|
|
8051
|
-
const { nodeCodeFs } = await import('./session-
|
|
8055
|
+
const { agentTask } = await import('./interactive-VQM2CTE2.js');
|
|
8056
|
+
const { nodeCodeFs } = await import('./session-N2FTYIHQ.js');
|
|
8052
8057
|
const { TtyPrompter } = await import('./ui-K7D3BCHD.js');
|
|
8053
8058
|
let priorSummary;
|
|
8054
8059
|
let priorInstruction;
|
|
@@ -8063,7 +8068,7 @@ ${res.stderr}` : ""),
|
|
|
8063
8068
|
}
|
|
8064
8069
|
}
|
|
8065
8070
|
const prompter = tty ? new TtyPrompter() : void 0;
|
|
8066
|
-
const { createVgBuiltinMcpTools, mergeExternalToolsets } = await import('./vg-mcp-bridge-
|
|
8071
|
+
const { createVgBuiltinMcpTools, mergeExternalToolsets } = await import('./vg-mcp-bridge-VDZWFEO2.js');
|
|
8067
8072
|
const vgBuiltin = createVgBuiltinMcpTools({
|
|
8068
8073
|
getGraph: () => graph,
|
|
8069
8074
|
root: root3,
|
|
@@ -8087,11 +8092,11 @@ ${res.stderr}` : ""),
|
|
|
8087
8092
|
const { loadFederation } = await import('./federation-M4ZNXOPX.js');
|
|
8088
8093
|
const { resolveExecutionEnv } = await import('./execution-env-4MS5AYAN.js');
|
|
8089
8094
|
const { resolveModelExecutionProfile, mergeModelExecutionProfile } = await import('./model-execution-profile-ELWF3Z6H.js');
|
|
8090
|
-
const { startCodeRuntimeSession } = await import('./runtime-session-
|
|
8091
|
-
const { resolveGraphBackend } = await import('./graph-backend-
|
|
8095
|
+
const { startCodeRuntimeSession } = await import('./runtime-session-Y2BRBXUW.js');
|
|
8096
|
+
const { resolveGraphBackend } = await import('./graph-backend-V5A2H5ZB.js');
|
|
8092
8097
|
const { detectGitRef } = await import('./git-ref-LMT3EXYY.js');
|
|
8093
8098
|
const { repositoryIdFromRoot } = await import('./paths-RVK6FKRN.js');
|
|
8094
|
-
const { vgdRequest } = await import('./vgd-
|
|
8099
|
+
const { vgdRequest } = await import('./vgd-3A2TRNP5.js');
|
|
8095
8100
|
const federation = loadFederation(root3);
|
|
8096
8101
|
const modelProfile = mergeModelExecutionProfile(
|
|
8097
8102
|
resolveModelExecutionProfile({
|
|
@@ -8237,11 +8242,11 @@ function resolveSecurityTierFlag(flag, fromConfig, auto) {
|
|
|
8237
8242
|
return auto ? "L1" : "L0";
|
|
8238
8243
|
}
|
|
8239
8244
|
function confirm2(question) {
|
|
8240
|
-
return new Promise((
|
|
8245
|
+
return new Promise((resolve25) => {
|
|
8241
8246
|
const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
|
|
8242
8247
|
rl.question(`${question} [y/N] `, (answer) => {
|
|
8243
8248
|
rl.close();
|
|
8244
|
-
|
|
8249
|
+
resolve25(/^y(es)?$/i.test(answer.trim()));
|
|
8245
8250
|
});
|
|
8246
8251
|
});
|
|
8247
8252
|
}
|
|
@@ -8350,9 +8355,9 @@ function registerLib(program) {
|
|
|
8350
8355
|
const global = readGlobal(this);
|
|
8351
8356
|
const root3 = rootOf(global);
|
|
8352
8357
|
const [verb, ...rest] = args;
|
|
8353
|
-
const network = !global.
|
|
8358
|
+
const network = !global.offline;
|
|
8354
8359
|
if (!verb) return listCatalog(root3, global.json);
|
|
8355
|
-
const online = (Boolean(opts.online) || network) && !global.
|
|
8360
|
+
const online = (Boolean(opts.online) || network) && !global.offline;
|
|
8356
8361
|
if (verb === "add") return addCmd(root3, rest, { ...opts, online }, global.json);
|
|
8357
8362
|
if (verb === "publish") return publishCmd(root3, rest, opts, global, global.json);
|
|
8358
8363
|
if (verb === "resolve") return resolveCmd(root3, rest, global.json);
|
|
@@ -8399,7 +8404,7 @@ async function addCmd(root3, rest, opts, asJson) {
|
|
|
8399
8404
|
}
|
|
8400
8405
|
function readPackageVersion(root3) {
|
|
8401
8406
|
try {
|
|
8402
|
-
const pkg = JSON.parse(fs12__default.readFileSync(
|
|
8407
|
+
const pkg = JSON.parse(fs12__default.readFileSync(path51__default.join(root3, "package.json"), "utf8"));
|
|
8403
8408
|
return typeof pkg.version === "string" ? pkg.version : void 0;
|
|
8404
8409
|
} catch {
|
|
8405
8410
|
return void 0;
|
|
@@ -8414,7 +8419,7 @@ function readOptionalFile(p) {
|
|
|
8414
8419
|
}
|
|
8415
8420
|
}
|
|
8416
8421
|
async function publishCmd(root3, rest, opts, global, asJson) {
|
|
8417
|
-
if (global.
|
|
8422
|
+
if (global.offline) throw usageError("vg lib publish uploads to the hosted catalog \u2014 it needs network (remove --offline/--local)");
|
|
8418
8423
|
const name = (rest.join(" ") || opts.name || "").trim();
|
|
8419
8424
|
if (!name) throw usageError("usage: vg lib publish <name> [--version <v>] [--readme <path>] [--dts <path>] [--language <lang>]");
|
|
8420
8425
|
const dsn = resolveDsn();
|
|
@@ -8422,8 +8427,8 @@ async function publishCmd(root3, rest, opts, global, asJson) {
|
|
|
8422
8427
|
if (!parsed) throw new CliError("publishing a private library requires a DSN \u2014 run `vibgrate login` or set VIBGRATE_DSN", ExitCode.USAGE_ERROR);
|
|
8423
8428
|
const version = (opts.version || readPackageVersion(root3) || "").trim();
|
|
8424
8429
|
if (!version) throw usageError("a --version is required (or run from a package whose package.json has a version)");
|
|
8425
|
-
const readme = readOptionalFile(opts.readme ?
|
|
8426
|
-
const dts = opts.dts ? readOptionalFile(
|
|
8430
|
+
const readme = readOptionalFile(opts.readme ? path51__default.resolve(opts.readme) : path51__default.join(root3, "README.md"));
|
|
8431
|
+
const dts = opts.dts ? readOptionalFile(path51__default.resolve(opts.dts)) : void 0;
|
|
8427
8432
|
if (!readme && !dts) throw usageError("nothing to publish \u2014 no README.md found and no --dts given");
|
|
8428
8433
|
const result = await publishPrivateLibrary(
|
|
8429
8434
|
{ name, version, readme, dts, language: opts.language },
|
|
@@ -8546,7 +8551,7 @@ function registerExport(program) {
|
|
|
8546
8551
|
const content = exportGraph(format, {
|
|
8547
8552
|
graph,
|
|
8548
8553
|
deps: needsDeps ? inventory(root3).records : void 0,
|
|
8549
|
-
models: needsDeps && !global.
|
|
8554
|
+
models: needsDeps && !global.offline ? discoverModels() : void 0,
|
|
8550
8555
|
generatedAt: graph.generatedAt,
|
|
8551
8556
|
compact: opts.compact === true ? true : opts.compact === false ? false : void 0,
|
|
8552
8557
|
slim: opts.slim === true
|
|
@@ -8555,7 +8560,7 @@ function registerExport(program) {
|
|
|
8555
8560
|
out(content.trimEnd());
|
|
8556
8561
|
return;
|
|
8557
8562
|
}
|
|
8558
|
-
fs12.mkdirSync(
|
|
8563
|
+
fs12.mkdirSync(path51.dirname(path51.resolve(file)), { recursive: true });
|
|
8559
8564
|
fs12.writeFileSync(file, content);
|
|
8560
8565
|
info(`${c.green("\u2714")} wrote ${file} ${c.dim(`(${format})`)}`);
|
|
8561
8566
|
});
|
|
@@ -8566,49 +8571,49 @@ function detectFormat(file) {
|
|
|
8566
8571
|
if (lower.endsWith(".cdx.json")) return "cyclonedx";
|
|
8567
8572
|
if (lower.endsWith(".spdx.json")) return "spdx";
|
|
8568
8573
|
if (file === "-") return "json";
|
|
8569
|
-
return formatForExt(
|
|
8574
|
+
return formatForExt(path51.extname(file));
|
|
8570
8575
|
}
|
|
8571
8576
|
function registerBundle(program) {
|
|
8572
|
-
const cmd = program.command("bundle").description("build an air-gapped bundle (grammars + graph + library catalog)").option("
|
|
8577
|
+
const cmd = program.command("bundle").description("build an air-gapped bundle (grammars + graph + library catalog)").option("-o, --out <dir>", "output directory", "vg-bundle").action(function(opts) {
|
|
8573
8578
|
const global = readGlobal(this);
|
|
8574
8579
|
const root3 = rootOf(global);
|
|
8575
|
-
const outDir =
|
|
8580
|
+
const outDir = path51.resolve(root3, opts.out ?? "vg-bundle");
|
|
8576
8581
|
let grammarFiles;
|
|
8577
8582
|
try {
|
|
8578
8583
|
grammarFiles = resolvedGrammarFiles();
|
|
8579
8584
|
} catch {
|
|
8580
8585
|
throw new CliError("no grammars found to bundle (run a build first)", ExitCode.ERROR);
|
|
8581
8586
|
}
|
|
8582
|
-
fs12.mkdirSync(
|
|
8587
|
+
fs12.mkdirSync(path51.join(outDir, "grammars"), { recursive: true });
|
|
8583
8588
|
let grammarCount = 0;
|
|
8584
8589
|
for (const { fileName, absPath } of grammarFiles) {
|
|
8585
|
-
fs12.copyFileSync(absPath,
|
|
8590
|
+
fs12.copyFileSync(absPath, path51.join(outDir, "grammars", fileName));
|
|
8586
8591
|
grammarCount++;
|
|
8587
8592
|
}
|
|
8588
8593
|
for (const id of Object.keys(TOOLCHAIN_GRAMMARS).sort()) {
|
|
8589
8594
|
const absPath = resolveToolchainGrammar(id);
|
|
8590
8595
|
if (!absPath) continue;
|
|
8591
|
-
fs12.copyFileSync(absPath,
|
|
8596
|
+
fs12.copyFileSync(absPath, path51.join(outDir, "grammars", path51.basename(absPath)));
|
|
8592
8597
|
grammarCount++;
|
|
8593
8598
|
}
|
|
8594
8599
|
const included = [`grammars/ (${grammarCount} .wasm)`];
|
|
8595
8600
|
const graphSrc = resolveGraphPath(root3, global.graph);
|
|
8596
8601
|
if (fs12.existsSync(graphSrc)) {
|
|
8597
|
-
fs12.copyFileSync(graphSrc,
|
|
8602
|
+
fs12.copyFileSync(graphSrc, path51.join(outDir, "graph.json"));
|
|
8598
8603
|
included.push("graph.json");
|
|
8599
8604
|
} else {
|
|
8600
8605
|
const graph = loadGraphFileWithSnapshot(graphSrc);
|
|
8601
8606
|
if (graph) {
|
|
8602
|
-
fs12.writeFileSync(
|
|
8607
|
+
fs12.writeFileSync(path51.join(outDir, "graph.json"), serializeGraph(graph));
|
|
8603
8608
|
included.push("graph.json");
|
|
8604
8609
|
}
|
|
8605
8610
|
}
|
|
8606
8611
|
const cat = catalogPath(root3);
|
|
8607
8612
|
if (fs12.existsSync(cat)) {
|
|
8608
|
-
fs12.copyFileSync(cat,
|
|
8613
|
+
fs12.copyFileSync(cat, path51.join(outDir, "vibgrate.lib.json"));
|
|
8609
8614
|
included.push("vibgrate.lib.json");
|
|
8610
8615
|
if (fs12.existsSync(libDir(root3))) {
|
|
8611
|
-
fs12.cpSync(libDir(root3),
|
|
8616
|
+
fs12.cpSync(libDir(root3), path51.join(outDir, "lib"), { recursive: true });
|
|
8612
8617
|
included.push("lib/");
|
|
8613
8618
|
}
|
|
8614
8619
|
}
|
|
@@ -8620,15 +8625,15 @@ function registerBundle(program) {
|
|
|
8620
8625
|
included,
|
|
8621
8626
|
usage: "Run `vg build --grammars <this dir>/grammars` to build fully offline."
|
|
8622
8627
|
};
|
|
8623
|
-
fs12.writeFileSync(
|
|
8628
|
+
fs12.writeFileSync(path51.join(outDir, "MANIFEST.json"), `${stableStringify(manifest, 2)}
|
|
8624
8629
|
`);
|
|
8625
8630
|
if (global.json) {
|
|
8626
|
-
json({ out:
|
|
8631
|
+
json({ out: path51.relative(root3, outDir), ...manifest });
|
|
8627
8632
|
return;
|
|
8628
8633
|
}
|
|
8629
|
-
info(`${c.cyan("vg bundle")} \xB7 wrote ${
|
|
8634
|
+
info(`${c.cyan("vg bundle")} \xB7 wrote ${path51.relative(root3, outDir)}/`);
|
|
8630
8635
|
for (const i of included) info(` ${c.green("\u2714")} ${i}`);
|
|
8631
|
-
info(c.dim(` offline: vg build --grammars "${
|
|
8636
|
+
info(c.dim(` offline: vg build --grammars "${path51.relative(root3, outDir)}/grammars"`));
|
|
8632
8637
|
});
|
|
8633
8638
|
applyGlobalOptions(cmd);
|
|
8634
8639
|
}
|
|
@@ -8644,9 +8649,9 @@ var REACH_TIMEOUT_MS = 3e3;
|
|
|
8644
8649
|
var CONFIG_BASENAMES = ["vibgrate.config.ts", "vibgrate.config.js", "vibgrate.config.json"];
|
|
8645
8650
|
async function runDoctor(global) {
|
|
8646
8651
|
const root3 = rootOf(global);
|
|
8647
|
-
const local = global.
|
|
8652
|
+
const local = global.offline === true;
|
|
8648
8653
|
const graphPath = resolveGraphPath(root3, global.graph);
|
|
8649
|
-
const configFile = CONFIG_BASENAMES.find((f) => fs12.existsSync(
|
|
8654
|
+
const configFile = CONFIG_BASENAMES.find((f) => fs12.existsSync(path51.join(root3, f))) ?? null;
|
|
8650
8655
|
const graph = loadGraph(root3, graphPath);
|
|
8651
8656
|
let staleFiles = null;
|
|
8652
8657
|
if (graph) {
|
|
@@ -8672,13 +8677,13 @@ async function runDoctor(global) {
|
|
|
8672
8677
|
root: root3,
|
|
8673
8678
|
configFile,
|
|
8674
8679
|
map: {
|
|
8675
|
-
path:
|
|
8680
|
+
path: displayGraphPath(root3, graphPath),
|
|
8676
8681
|
built: graph !== null,
|
|
8677
8682
|
generatedAt: graph?.generatedAt ?? null,
|
|
8678
8683
|
staleFiles
|
|
8679
8684
|
},
|
|
8680
8685
|
libCatalog: {
|
|
8681
|
-
path:
|
|
8686
|
+
path: path51.relative(root3, catalogPath(root3)),
|
|
8682
8687
|
present: fs12.existsSync(catalogPath(root3)),
|
|
8683
8688
|
libraries: libCount
|
|
8684
8689
|
},
|
|
@@ -8736,7 +8741,7 @@ async function runDoctor(global) {
|
|
|
8736
8741
|
);
|
|
8737
8742
|
info(
|
|
8738
8743
|
` host pool ${li.hostPool.size} warm session(s)` + (li.hostPool.sessions[0] ? c.dim(
|
|
8739
|
-
` \xB7 last ${
|
|
8744
|
+
` \xB7 last ${path51.basename(li.hostPool.sessions[0].modelPath)} kv=${li.hostPool.sessions[0].kvBlocks}`
|
|
8740
8745
|
) : c.dim(" \xB7 none (load via vg code / vgd host-load)"))
|
|
8741
8746
|
);
|
|
8742
8747
|
if (li.dynamicOnToken || li.dynamicCustomSampler) {
|
|
@@ -8906,12 +8911,12 @@ function registerLlmHost(program) {
|
|
|
8906
8911
|
let binding = null;
|
|
8907
8912
|
const ensured = await ensurePackage("node-llama-cpp@^3", {
|
|
8908
8913
|
consent: !!opts.yes,
|
|
8909
|
-
local: !!global.
|
|
8914
|
+
local: !!global.offline,
|
|
8910
8915
|
interactive: false
|
|
8911
8916
|
});
|
|
8912
8917
|
if (ensured.module) {
|
|
8913
8918
|
binding = ensured.module;
|
|
8914
|
-
} else if (opts.yes || !global.
|
|
8919
|
+
} else if (opts.yes || !global.offline) {
|
|
8915
8920
|
if (!global.json) {
|
|
8916
8921
|
info(
|
|
8917
8922
|
c.dim(
|
|
@@ -9043,7 +9048,7 @@ async function readStdin() {
|
|
|
9043
9048
|
return Buffer.concat(chunks).toString("utf-8");
|
|
9044
9049
|
}
|
|
9045
9050
|
function readFileOrThrow(file, label) {
|
|
9046
|
-
const p =
|
|
9051
|
+
const p = path51.resolve(file);
|
|
9047
9052
|
if (!fs12.existsSync(p)) throw usageError(`${label} file not found: ${p}`);
|
|
9048
9053
|
return fs12.readFileSync(p, "utf-8");
|
|
9049
9054
|
}
|
|
@@ -9055,8 +9060,8 @@ function writeOutput(outFile, content) {
|
|
|
9055
9060
|
process.stdout.write(content.endsWith("\n") ? content : content + "\n");
|
|
9056
9061
|
return;
|
|
9057
9062
|
}
|
|
9058
|
-
const outPath =
|
|
9059
|
-
const dir =
|
|
9063
|
+
const outPath = path51.resolve(outFile);
|
|
9064
|
+
const dir = path51.dirname(outPath);
|
|
9060
9065
|
if (!fs12.existsSync(dir)) throw usageError(`output directory does not exist: ${dir}`);
|
|
9061
9066
|
fs12.writeFileSync(outPath, content.endsWith("\n") ? content : content + "\n", "utf-8");
|
|
9062
9067
|
info(c.dim(`wrote ${outPath}`));
|
|
@@ -9091,7 +9096,7 @@ function registerHcs(program) {
|
|
|
9091
9096
|
}
|
|
9092
9097
|
const statePath = opts.previous ?? opts.out;
|
|
9093
9098
|
const useIncremental = canIncremental && statePath !== void 0;
|
|
9094
|
-
const root3 =
|
|
9099
|
+
const root3 = path51.resolve(dir);
|
|
9095
9100
|
if (!fs12.existsSync(root3) || !fs12.statSync(root3).isDirectory()) {
|
|
9096
9101
|
throw usageError(`not a directory: ${root3}`);
|
|
9097
9102
|
}
|
|
@@ -9116,7 +9121,7 @@ function registerHcs(program) {
|
|
|
9116
9121
|
}
|
|
9117
9122
|
for (const entry of entries.sort((a, b) => a.name < b.name ? -1 : 1)) {
|
|
9118
9123
|
if (entry.name.startsWith(".")) continue;
|
|
9119
|
-
const absChild =
|
|
9124
|
+
const absChild = path51.join(abs, entry.name);
|
|
9120
9125
|
const relChild = rel ? `${rel}/${entry.name}` : entry.name;
|
|
9121
9126
|
if (entry.isDirectory()) {
|
|
9122
9127
|
if (!SKIP_DIRS.has(entry.name)) walk2(absChild, relChild);
|
|
@@ -9124,7 +9129,7 @@ function registerHcs(program) {
|
|
|
9124
9129
|
}
|
|
9125
9130
|
if (!entry.isFile()) continue;
|
|
9126
9131
|
if (!opts.includeTests && TEST_PATTERNS.some((p) => p.test(relChild))) continue;
|
|
9127
|
-
const lang = extToLang.get(
|
|
9132
|
+
const lang = extToLang.get(path51.extname(entry.name).toLowerCase());
|
|
9128
9133
|
if (!lang) continue;
|
|
9129
9134
|
try {
|
|
9130
9135
|
if (fs12.statSync(absChild).size > MAX_EXTRACT_FILE_BYTES) continue;
|
|
@@ -9158,7 +9163,7 @@ function registerHcs(program) {
|
|
|
9158
9163
|
writeOutput(opts.out, parts2.join("\n"));
|
|
9159
9164
|
return;
|
|
9160
9165
|
}
|
|
9161
|
-
const previousPath =
|
|
9166
|
+
const previousPath = path51.resolve(statePath);
|
|
9162
9167
|
const previousNdjson = !opts.full && fs12.existsSync(previousPath) ? fs12.readFileSync(previousPath, "utf-8") : "";
|
|
9163
9168
|
if (opts.full) info(c.dim("running a full re-extraction (--full)"));
|
|
9164
9169
|
else if (!previousNdjson) info(c.dim(`previous stream not found at ${previousPath} \u2014 running a full extraction`));
|
|
@@ -9262,12 +9267,2005 @@ function registerHcs(program) {
|
|
|
9262
9267
|
});
|
|
9263
9268
|
applyGlobalOptions(cmd);
|
|
9264
9269
|
}
|
|
9270
|
+
|
|
9271
|
+
// src/review/format.ts
|
|
9272
|
+
var DECISION_LABEL = {
|
|
9273
|
+
pass: "pass",
|
|
9274
|
+
needs_review: "needs review",
|
|
9275
|
+
fail: "fail",
|
|
9276
|
+
undetermined: "undetermined"
|
|
9277
|
+
};
|
|
9278
|
+
function colorDecision(decision) {
|
|
9279
|
+
const label = DECISION_LABEL[decision];
|
|
9280
|
+
if (decision === "pass") return c.green(label);
|
|
9281
|
+
if (decision === "fail") return c.red(label);
|
|
9282
|
+
return c.yellow(label);
|
|
9283
|
+
}
|
|
9284
|
+
function formatText(result) {
|
|
9285
|
+
const { receipt, reasons, budget } = result;
|
|
9286
|
+
const lines = [];
|
|
9287
|
+
const all = [...receipt.findings.architecture_findings, ...receipt.findings.security_findings];
|
|
9288
|
+
lines.push(`${c.cyan("vg review")} \xB7 ${colorDecision(receipt.decision)} \xB7 ${receipt.enforcement}`);
|
|
9289
|
+
lines.push(
|
|
9290
|
+
c.dim(
|
|
9291
|
+
` ${receipt.git.dirty ? "working tree" : receipt.git.head_sha.slice(0, 8)} vs ${receipt.git.base_sha.slice(0, 8)}${receipt.git.merge_base ? " (merge-base)" : ""} \xB7 ${receipt.change_class.join(", ")}`
|
|
9292
|
+
)
|
|
9293
|
+
);
|
|
9294
|
+
lines.push("");
|
|
9295
|
+
if (all.length === 0) {
|
|
9296
|
+
lines.push(c.dim(" no architecture or security-control findings"));
|
|
9297
|
+
}
|
|
9298
|
+
for (const f of all) {
|
|
9299
|
+
const badge = f.protected_finding ? c.red("protected") : severityColor(f.severity);
|
|
9300
|
+
lines.push(` ${c.bold(f.id)} ${badge} ${c.dim(f.kind)}`);
|
|
9301
|
+
lines.push(` ${f.claim}`);
|
|
9302
|
+
lines.push(c.dim(` \u2192 ${f.remediation}`));
|
|
9303
|
+
lines.push(
|
|
9304
|
+
c.dim(` alignment ${f.target_alignment} \xB7 confidence ${f.confidence.toFixed(2)} \xB7 ${f.paths.slice(0, 3).join(", ")}`)
|
|
9305
|
+
);
|
|
9306
|
+
lines.push("");
|
|
9307
|
+
}
|
|
9308
|
+
if (receipt.findings.unknowns.length > 0) {
|
|
9309
|
+
lines.push(c.yellow(" unknowns"));
|
|
9310
|
+
for (const u of receipt.findings.unknowns) lines.push(c.dim(` \xB7 ${u}`));
|
|
9311
|
+
lines.push("");
|
|
9312
|
+
}
|
|
9313
|
+
if (receipt.findings.required_checks.length > 0) {
|
|
9314
|
+
lines.push(c.dim(` required checks: ${receipt.findings.required_checks.join(", ")}`));
|
|
9315
|
+
}
|
|
9316
|
+
lines.push(c.dim(` policy: ${reasons.join("; ")}`));
|
|
9317
|
+
lines.push(
|
|
9318
|
+
c.dim(
|
|
9319
|
+
` capsule ~${budget.estimatedTokens} tokens (median target ${budget.median}, cap ${budget.cap}${budget.trimmed ? ", trimmed" : ""})`
|
|
9320
|
+
)
|
|
9321
|
+
);
|
|
9322
|
+
lines.push(
|
|
9323
|
+
c.dim(
|
|
9324
|
+
` ${receipt.versions.policy} \xB7 model ${receipt.versions.model} \xB7 graph ${receipt.versions.graph_schema} \xB7 ${receipt.digests.receipt.slice(0, 19)}\u2026`
|
|
9325
|
+
)
|
|
9326
|
+
);
|
|
9327
|
+
lines.push(c.dim(" explain one: `vg review explain <finding-id>` \xB7 machine result: `vg review --format json`"));
|
|
9328
|
+
lines.push(c.dim(" Review reports change integrity. It is not a proof of security and absence of findings is not a certification."));
|
|
9329
|
+
return lines.join("\n");
|
|
9330
|
+
}
|
|
9331
|
+
function severityColor(severity) {
|
|
9332
|
+
if (severity === "critical" || severity === "high") return c.red(severity);
|
|
9333
|
+
if (severity === "medium") return c.yellow(severity);
|
|
9334
|
+
return c.dim(severity);
|
|
9335
|
+
}
|
|
9336
|
+
function formatMarkdown(result) {
|
|
9337
|
+
const { receipt, reasons } = result;
|
|
9338
|
+
const all = [...receipt.findings.architecture_findings, ...receipt.findings.security_findings];
|
|
9339
|
+
const lines = [];
|
|
9340
|
+
lines.push(`## Vibgrate Review \u2014 \`${DECISION_LABEL[receipt.decision]}\``);
|
|
9341
|
+
lines.push("");
|
|
9342
|
+
lines.push(
|
|
9343
|
+
`\`${receipt.git.base_sha.slice(0, 8)}\` \u2192 \`${receipt.git.head_sha.slice(0, 8)}\`${receipt.git.dirty ? " (working tree)" : ""} \xB7 enforcement \`${receipt.enforcement}\``
|
|
9344
|
+
);
|
|
9345
|
+
lines.push("");
|
|
9346
|
+
lines.push(
|
|
9347
|
+
`| architecture | security | protected | unknowns |
|
|
9348
|
+
|---:|---:|---:|---:|
|
|
9349
|
+
| ${receipt.counts.architecture} | ${receipt.counts.security} | ${receipt.counts.protected} | ${receipt.counts.unknowns} |`
|
|
9350
|
+
);
|
|
9351
|
+
lines.push("");
|
|
9352
|
+
if (all.length === 0) {
|
|
9353
|
+
lines.push("No architecture or security-control findings.");
|
|
9354
|
+
} else {
|
|
9355
|
+
lines.push("| id | kind | severity | alignment | claim |");
|
|
9356
|
+
lines.push("|---|---|---|---|---|");
|
|
9357
|
+
for (const f of all) {
|
|
9358
|
+
const kind = f.protected_finding ? `${f.kind} **(protected)**` : f.kind;
|
|
9359
|
+
lines.push(`| \`${f.id}\` | ${kind} | ${f.severity} | ${f.target_alignment} | ${escapeCell(f.claim)} |`);
|
|
9360
|
+
}
|
|
9361
|
+
}
|
|
9362
|
+
if (receipt.findings.unknowns.length > 0) {
|
|
9363
|
+
lines.push("");
|
|
9364
|
+
lines.push("**Unknowns**");
|
|
9365
|
+
lines.push("");
|
|
9366
|
+
for (const u of receipt.findings.unknowns) lines.push(`- ${u}`);
|
|
9367
|
+
}
|
|
9368
|
+
lines.push("");
|
|
9369
|
+
lines.push(`<sub>${escapeCell(reasons.join("; "))}</sub>`);
|
|
9370
|
+
lines.push("");
|
|
9371
|
+
lines.push(
|
|
9372
|
+
`<sub>policy \`${receipt.versions.policy}\` \xB7 model \`${receipt.versions.model}\` \xB7 receipt \`${receipt.digests.receipt.slice(0, 23)}\u2026\`</sub>`
|
|
9373
|
+
);
|
|
9374
|
+
lines.push("");
|
|
9375
|
+
lines.push(
|
|
9376
|
+
"<sub>Vibgrate Review reports change integrity against the declared architecture and security controls. It does not prove code is secure, and absence of findings is not a certification.</sub>"
|
|
9377
|
+
);
|
|
9378
|
+
return lines.join("\n");
|
|
9379
|
+
}
|
|
9380
|
+
function escapeCell(s) {
|
|
9381
|
+
return s.replace(/\|/g, "\\|").replace(/\n/g, " ");
|
|
9382
|
+
}
|
|
9383
|
+
function formatSarif(receipt) {
|
|
9384
|
+
const findings = receipt.findings.security_findings;
|
|
9385
|
+
const rules = [...new Map(findings.map((f) => [f.kind, f])).values()].map((f) => ({
|
|
9386
|
+
id: f.kind,
|
|
9387
|
+
name: f.kind,
|
|
9388
|
+
shortDescription: { text: f.kind.replace(/_/g, " ") },
|
|
9389
|
+
fullDescription: { text: f.remediation },
|
|
9390
|
+
defaultConfiguration: { level: sarifLevel(f) },
|
|
9391
|
+
properties: { tags: ["security", "vibgrate-review"] }
|
|
9392
|
+
}));
|
|
9393
|
+
return JSON.stringify(
|
|
9394
|
+
{
|
|
9395
|
+
$schema: "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/main/sarif-2.1/schema/sarif-schema-2.1.0.json",
|
|
9396
|
+
version: "2.1.0",
|
|
9397
|
+
runs: [
|
|
9398
|
+
{
|
|
9399
|
+
tool: {
|
|
9400
|
+
driver: {
|
|
9401
|
+
name: "Vibgrate Review",
|
|
9402
|
+
version: receipt.versions.cli,
|
|
9403
|
+
informationUri: "https://vibgrate.com/review",
|
|
9404
|
+
rules
|
|
9405
|
+
}
|
|
9406
|
+
},
|
|
9407
|
+
automationDetails: { id: `vibgrate-review/${receipt.git.head_sha}` },
|
|
9408
|
+
results: findings.map((f) => ({
|
|
9409
|
+
ruleId: f.kind,
|
|
9410
|
+
level: sarifLevel(f),
|
|
9411
|
+
message: { text: f.claim },
|
|
9412
|
+
locations: f.paths.map((p) => ({
|
|
9413
|
+
physicalLocation: { artifactLocation: { uri: p } }
|
|
9414
|
+
})),
|
|
9415
|
+
properties: {
|
|
9416
|
+
findingId: f.id,
|
|
9417
|
+
confidence: f.confidence,
|
|
9418
|
+
targetAlignment: f.target_alignment,
|
|
9419
|
+
protectedFinding: f.protected_finding === true,
|
|
9420
|
+
remediation: f.remediation
|
|
9421
|
+
}
|
|
9422
|
+
}))
|
|
9423
|
+
}
|
|
9424
|
+
]
|
|
9425
|
+
},
|
|
9426
|
+
null,
|
|
9427
|
+
2
|
|
9428
|
+
);
|
|
9429
|
+
}
|
|
9430
|
+
function sarifLevel(f) {
|
|
9431
|
+
if (f.protected_finding || f.severity === "critical" || f.severity === "high") return "error";
|
|
9432
|
+
if (f.severity === "medium") return "warning";
|
|
9433
|
+
return "note";
|
|
9434
|
+
}
|
|
9435
|
+
function formatExplain(result, findingId) {
|
|
9436
|
+
const all = [...result.receipt.findings.architecture_findings, ...result.receipt.findings.security_findings];
|
|
9437
|
+
const finding = all.find((f) => f.id === findingId);
|
|
9438
|
+
if (!finding) return null;
|
|
9439
|
+
const lines = [];
|
|
9440
|
+
lines.push(`${c.bold(finding.id)} ${severityColor(finding.severity)} ${c.dim(finding.kind)}`);
|
|
9441
|
+
lines.push("");
|
|
9442
|
+
lines.push(` ${finding.claim}`);
|
|
9443
|
+
lines.push("");
|
|
9444
|
+
lines.push(c.dim(` remediation: ${finding.remediation}`));
|
|
9445
|
+
lines.push(c.dim(` alignment: ${finding.target_alignment}`));
|
|
9446
|
+
lines.push(c.dim(` confidence: ${finding.confidence.toFixed(2)}`));
|
|
9447
|
+
lines.push(c.dim(` protected: ${finding.protected_finding === true ? "yes \u2014 policy cannot pass while it is unresolved" : "no"}`));
|
|
9448
|
+
lines.push(c.dim(` produced by: ${finding.source ?? "scanner"}`));
|
|
9449
|
+
lines.push("");
|
|
9450
|
+
lines.push(c.dim(" evidence"));
|
|
9451
|
+
for (const id of finding.evidence_ids) {
|
|
9452
|
+
const e = result.capsule.evidence.find((x) => x.id === id);
|
|
9453
|
+
if (!e) {
|
|
9454
|
+
lines.push(c.red(` ${id} \u2014 not present in the capsule (this finding failed verification)`));
|
|
9455
|
+
continue;
|
|
9456
|
+
}
|
|
9457
|
+
const span = e.start_line ? `:${e.start_line}${e.end_line && e.end_line !== e.start_line ? `-${e.end_line}` : ""}` : "";
|
|
9458
|
+
lines.push(` ${c.bold(e.id)} ${c.dim(e.kind)} ${e.path ?? ""}${span}`);
|
|
9459
|
+
if (e.note) lines.push(c.dim(` ${e.note}`));
|
|
9460
|
+
}
|
|
9461
|
+
return lines.join("\n");
|
|
9462
|
+
}
|
|
9463
|
+
|
|
9464
|
+
// src/review/policy.ts
|
|
9465
|
+
var ALIGNED = /* @__PURE__ */ new Set(["target", "approved_exception"]);
|
|
9466
|
+
function isProtected(f) {
|
|
9467
|
+
return f.protected_finding === true;
|
|
9468
|
+
}
|
|
9469
|
+
function unresolvedProtected(findings, config) {
|
|
9470
|
+
const enabled = (kind) => {
|
|
9471
|
+
if (kind === "unguarded_entrypoint" || kind === "guard_removed") return config.protected.unguarded_entrypoint;
|
|
9472
|
+
if (kind === "known_vulnerable_dependency") return config.protected.known_vulnerable_dependency;
|
|
9473
|
+
if (kind === "validated_taint") return config.protected.validated_taint;
|
|
9474
|
+
return true;
|
|
9475
|
+
};
|
|
9476
|
+
return [...findings.architecture_findings, ...findings.security_findings].filter(isProtected).filter((f) => enabled(f.kind));
|
|
9477
|
+
}
|
|
9478
|
+
function applyReviewPolicy(capsule, findings, config, verification) {
|
|
9479
|
+
const reasons = [];
|
|
9480
|
+
const all = [...findings.architecture_findings, ...findings.security_findings];
|
|
9481
|
+
const protectedFindings = unresolvedProtected(findings, config);
|
|
9482
|
+
const quickPath = findings.change_class.length === 1 && findings.change_class[0] === "none";
|
|
9483
|
+
const decide = () => {
|
|
9484
|
+
if (!verification.schema_valid || !verification.evidence_ids_valid) {
|
|
9485
|
+
reasons.push("the findings document failed schema or evidence verification \u2014 nothing here can be trusted to decide on");
|
|
9486
|
+
return "undetermined";
|
|
9487
|
+
}
|
|
9488
|
+
if (protectedFindings.length > 0) {
|
|
9489
|
+
reasons.push(
|
|
9490
|
+
`${protectedFindings.length} protected finding(s) are unresolved: ${protectedFindings.map((f) => f.kind).join(", ")}`
|
|
9491
|
+
);
|
|
9492
|
+
return "fail";
|
|
9493
|
+
}
|
|
9494
|
+
if (quickPath) {
|
|
9495
|
+
reasons.push("no material architectural or security-control delta in this change set");
|
|
9496
|
+
return "pass";
|
|
9497
|
+
}
|
|
9498
|
+
const high = all.filter(
|
|
9499
|
+
(f) => (f.severity === "high" || f.severity === "critical") && !ALIGNED.has(f.target_alignment) && f.confidence >= config.high_confidence_threshold
|
|
9500
|
+
);
|
|
9501
|
+
if (high.length > 0) {
|
|
9502
|
+
reasons.push(
|
|
9503
|
+
`${high.length} high-severity finding(s) at or above the ${config.high_confidence_threshold} confidence threshold`
|
|
9504
|
+
);
|
|
9505
|
+
return config.high_severity_decision;
|
|
9506
|
+
}
|
|
9507
|
+
const blockingUnknowns = findings.unknowns.length > 0 || findings.required_checks.length > 0;
|
|
9508
|
+
const material = all.filter((f) => !ALIGNED.has(f.target_alignment));
|
|
9509
|
+
if (material.length > 0) {
|
|
9510
|
+
reasons.push(`${material.length} finding(s) need a human look`);
|
|
9511
|
+
return "needs_review";
|
|
9512
|
+
}
|
|
9513
|
+
if (blockingUnknowns) {
|
|
9514
|
+
reasons.push(
|
|
9515
|
+
`no findings, but ${findings.unknowns.length} unknown(s) and ${findings.required_checks.length} required check(s) remain`
|
|
9516
|
+
);
|
|
9517
|
+
return capsule.patterns.unknown ? "undetermined" : "needs_review";
|
|
9518
|
+
}
|
|
9519
|
+
if (all.length > 0) {
|
|
9520
|
+
reasons.push("every finding is target-aligned or an approved exception");
|
|
9521
|
+
} else {
|
|
9522
|
+
reasons.push("no architecture or security-control findings in this change set");
|
|
9523
|
+
}
|
|
9524
|
+
return "pass";
|
|
9525
|
+
};
|
|
9526
|
+
let decision = decide();
|
|
9527
|
+
let protectedFalseBless = false;
|
|
9528
|
+
if (decision === "pass" && protectedFindings.length > 0) {
|
|
9529
|
+
protectedFalseBless = true;
|
|
9530
|
+
decision = "fail";
|
|
9531
|
+
reasons.push("policy attempted `pass` with an unresolved protected finding \u2014 downgraded to `fail`");
|
|
9532
|
+
}
|
|
9533
|
+
return {
|
|
9534
|
+
decision,
|
|
9535
|
+
reasons,
|
|
9536
|
+
protectedFalseBless,
|
|
9537
|
+
quickPath: quickPath && decision === "pass",
|
|
9538
|
+
protectedCount: protectedFindings.length
|
|
9539
|
+
};
|
|
9540
|
+
}
|
|
9541
|
+
var FAIL_ON_LEVELS = ["none", "fail", "needs_review"];
|
|
9542
|
+
function exitCodeForDecision(decision, failOn) {
|
|
9543
|
+
if (failOn === "none") return 0;
|
|
9544
|
+
if (decision === "fail") return ExitCode.GATE_FAILED;
|
|
9545
|
+
if (failOn === "needs_review" && (decision === "needs_review" || decision === "undetermined")) {
|
|
9546
|
+
return ExitCode.GATE_FAILED;
|
|
9547
|
+
}
|
|
9548
|
+
return 0;
|
|
9549
|
+
}
|
|
9550
|
+
function resolveFailOn(flag, config) {
|
|
9551
|
+
if (flag) return flag;
|
|
9552
|
+
return config.enforcement === "enforced" ? config.fail_on : "none";
|
|
9553
|
+
}
|
|
9554
|
+
|
|
9555
|
+
// src/review/delta.ts
|
|
9556
|
+
var SPEC_PATTERNS = [
|
|
9557
|
+
/(?:import|export)\s+(?:type\s+)?(?:[^'"\n;]+?\s+from\s+)?['"]([^'"]+)['"]/g,
|
|
9558
|
+
/require\s*\(\s*['"]([^'"]+)['"]\s*\)/g,
|
|
9559
|
+
/from\s+['"]([^'"]+)['"]/g,
|
|
9560
|
+
/import\s*\(\s*['"]([^'"]+)['"]\s*\)/g,
|
|
9561
|
+
/(?:^|\n)\s*import\s+"([^"]+)"/g,
|
|
9562
|
+
/(?:^|\n)\s*using\s+([A-Za-z0-9_.]+)\s*;/g
|
|
9563
|
+
];
|
|
9564
|
+
var TRY_SUFFIXES = [
|
|
9565
|
+
"",
|
|
9566
|
+
".ts",
|
|
9567
|
+
".tsx",
|
|
9568
|
+
".js",
|
|
9569
|
+
".jsx",
|
|
9570
|
+
".mjs",
|
|
9571
|
+
".cjs",
|
|
9572
|
+
".mts",
|
|
9573
|
+
".cts",
|
|
9574
|
+
".cs",
|
|
9575
|
+
".py",
|
|
9576
|
+
".go",
|
|
9577
|
+
".java",
|
|
9578
|
+
".kt",
|
|
9579
|
+
"/index.ts",
|
|
9580
|
+
"/index.tsx",
|
|
9581
|
+
"/index.js"
|
|
9582
|
+
];
|
|
9583
|
+
function normalize2(p) {
|
|
9584
|
+
return p.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
9585
|
+
}
|
|
9586
|
+
function dirname9(p) {
|
|
9587
|
+
const n = normalize2(p);
|
|
9588
|
+
const i = n.lastIndexOf("/");
|
|
9589
|
+
return i <= 0 ? "" : n.slice(0, i);
|
|
9590
|
+
}
|
|
9591
|
+
function resolveRelative(fromFile, spec) {
|
|
9592
|
+
const dir = dirname9(fromFile);
|
|
9593
|
+
const parts = `${dir ? `${dir}/` : ""}${spec}`.split("/");
|
|
9594
|
+
const out2 = [];
|
|
9595
|
+
for (const part of parts) {
|
|
9596
|
+
if (!part || part === ".") continue;
|
|
9597
|
+
if (part === "..") {
|
|
9598
|
+
if (out2.length > 0) out2.pop();
|
|
9599
|
+
continue;
|
|
9600
|
+
}
|
|
9601
|
+
out2.push(part);
|
|
9602
|
+
}
|
|
9603
|
+
return out2.join("/");
|
|
9604
|
+
}
|
|
9605
|
+
function stripExt(p) {
|
|
9606
|
+
return p.replace(/\.[^.]+$/, "");
|
|
9607
|
+
}
|
|
9608
|
+
function basenameNoExt(p) {
|
|
9609
|
+
const base = normalize2(p).split("/").pop() ?? p;
|
|
9610
|
+
return stripExt(base).toLowerCase();
|
|
9611
|
+
}
|
|
9612
|
+
function dependencyHints(text, fromFile) {
|
|
9613
|
+
const hints = /* @__PURE__ */ new Set();
|
|
9614
|
+
for (const pattern of SPEC_PATTERNS) {
|
|
9615
|
+
pattern.lastIndex = 0;
|
|
9616
|
+
let m;
|
|
9617
|
+
while ((m = pattern.exec(text)) !== null) {
|
|
9618
|
+
const spec = m[1]?.trim();
|
|
9619
|
+
if (!spec) continue;
|
|
9620
|
+
const cleaned = spec.replace(/\\/g, "/").replace(/\?.*$/, "").replace(/#.*$/, "");
|
|
9621
|
+
hints.add(cleaned.toLowerCase());
|
|
9622
|
+
hints.add(basenameNoExt(cleaned));
|
|
9623
|
+
if (cleaned.startsWith(".")) {
|
|
9624
|
+
const resolved = resolveRelative(fromFile, cleaned);
|
|
9625
|
+
hints.add(resolved.toLowerCase());
|
|
9626
|
+
hints.add(stripExt(resolved).toLowerCase());
|
|
9627
|
+
}
|
|
9628
|
+
}
|
|
9629
|
+
}
|
|
9630
|
+
return hints;
|
|
9631
|
+
}
|
|
9632
|
+
function destKeys(toPath) {
|
|
9633
|
+
const n = normalize2(toPath);
|
|
9634
|
+
const noExt = stripExt(n);
|
|
9635
|
+
return [n, noExt, n.toLowerCase(), noExt.toLowerCase(), basenameNoExt(n)];
|
|
9636
|
+
}
|
|
9637
|
+
function hintsCoverDest(hints, toPath) {
|
|
9638
|
+
for (const key2 of destKeys(toPath)) {
|
|
9639
|
+
if (hints.has(key2)) return true;
|
|
9640
|
+
}
|
|
9641
|
+
const n = normalize2(toPath).toLowerCase();
|
|
9642
|
+
for (const hint of hints) {
|
|
9643
|
+
for (const suffix of TRY_SUFFIXES) {
|
|
9644
|
+
if (hint + suffix === n || hint === n + suffix) return true;
|
|
9645
|
+
}
|
|
9646
|
+
}
|
|
9647
|
+
return false;
|
|
9648
|
+
}
|
|
9649
|
+
function fileReferencesDest(text, fromFile, toPath) {
|
|
9650
|
+
return hintsCoverDest(dependencyHints(text, fromFile), toPath);
|
|
9651
|
+
}
|
|
9652
|
+
function isIntroducedEdge(op, fromFile, toPath, baseText) {
|
|
9653
|
+
if (op === "added") return true;
|
|
9654
|
+
if (op === "removed") return false;
|
|
9655
|
+
if (baseText === void 0) return false;
|
|
9656
|
+
return !fileReferencesDest(baseText, fromFile, toPath);
|
|
9657
|
+
}
|
|
9658
|
+
function pathShapedCover(hints, toPath) {
|
|
9659
|
+
const n = normalize2(toPath).toLowerCase();
|
|
9660
|
+
const noExt = stripExt(n);
|
|
9661
|
+
if (hints.has(n) || hints.has(noExt)) return true;
|
|
9662
|
+
for (const hint of hints) {
|
|
9663
|
+
if (!hint.includes("/")) continue;
|
|
9664
|
+
for (const suffix of TRY_SUFFIXES) {
|
|
9665
|
+
if (hint + suffix === n || hint === n + suffix) return true;
|
|
9666
|
+
}
|
|
9667
|
+
}
|
|
9668
|
+
return false;
|
|
9669
|
+
}
|
|
9670
|
+
function removedDestinations(fromFile, baseText, headText, knownFiles) {
|
|
9671
|
+
const oldHints = dependencyHints(baseText, fromFile);
|
|
9672
|
+
const newHints = dependencyHints(headText, fromFile);
|
|
9673
|
+
const out2 = [];
|
|
9674
|
+
for (const dest of knownFiles) {
|
|
9675
|
+
const n = normalize2(dest);
|
|
9676
|
+
if (!pathShapedCover(oldHints, n)) continue;
|
|
9677
|
+
if (pathShapedCover(newHints, n)) continue;
|
|
9678
|
+
out2.push(n);
|
|
9679
|
+
}
|
|
9680
|
+
return out2.sort();
|
|
9681
|
+
}
|
|
9682
|
+
|
|
9683
|
+
// src/review/capsule.ts
|
|
9684
|
+
var BOUNDARY_EDGE_KINDS = /* @__PURE__ */ new Set(["import", "call", "references"]);
|
|
9685
|
+
var EXEMPT_LAYERS = /* @__PURE__ */ new Set(["config", "shared", "testing"]);
|
|
9686
|
+
function normalize3(p) {
|
|
9687
|
+
return p.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
9688
|
+
}
|
|
9689
|
+
function nodeTouchedByChange(node2, file) {
|
|
9690
|
+
if (file.hunks.length === 0) return true;
|
|
9691
|
+
return file.hunks.some((h) => node2.span.start <= h.end && node2.span.end >= h.start);
|
|
9692
|
+
}
|
|
9693
|
+
function estimateTokens(value) {
|
|
9694
|
+
return Math.ceil(JSON.stringify(value).length / 4);
|
|
9695
|
+
}
|
|
9696
|
+
function compileCapsule(input) {
|
|
9697
|
+
const { graph, change, config, profile } = input;
|
|
9698
|
+
const unknowns = [];
|
|
9699
|
+
const evidence = [];
|
|
9700
|
+
const seenEvidence = /* @__PURE__ */ new Set();
|
|
9701
|
+
const addEvidence = (e) => {
|
|
9702
|
+
if (!seenEvidence.has(e.id)) {
|
|
9703
|
+
seenEvidence.add(e.id);
|
|
9704
|
+
evidence.push(e);
|
|
9705
|
+
}
|
|
9706
|
+
return e.id;
|
|
9707
|
+
};
|
|
9708
|
+
const changedPaths = new Set(change.files.map((f) => normalize3(f.path)));
|
|
9709
|
+
const changedByPath = new Map(change.files.map((f) => [normalize3(f.path), f]));
|
|
9710
|
+
const layerOf = /* @__PURE__ */ new Map();
|
|
9711
|
+
const classify = (p) => {
|
|
9712
|
+
const cached2 = layerOf.get(p);
|
|
9713
|
+
if (cached2) return cached2;
|
|
9714
|
+
const cls = classifyFile(p, "unknown");
|
|
9715
|
+
if (!cls) return null;
|
|
9716
|
+
const value = { layer: cls.layer, confidence: cls.confidence, role: cls.role };
|
|
9717
|
+
layerOf.set(p, value);
|
|
9718
|
+
return value;
|
|
9719
|
+
};
|
|
9720
|
+
const symbols = [];
|
|
9721
|
+
const changedNodeIds = /* @__PURE__ */ new Set();
|
|
9722
|
+
const nodesByFile = /* @__PURE__ */ new Map();
|
|
9723
|
+
if (graph) {
|
|
9724
|
+
for (const node2 of graph.nodes) {
|
|
9725
|
+
const file = normalize3(node2.file);
|
|
9726
|
+
let bucket = nodesByFile.get(file);
|
|
9727
|
+
if (!bucket) nodesByFile.set(file, bucket = []);
|
|
9728
|
+
bucket.push(node2);
|
|
9729
|
+
}
|
|
9730
|
+
for (const path53 of [...changedPaths].sort()) {
|
|
9731
|
+
const file = changedByPath.get(path53);
|
|
9732
|
+
for (const node2 of nodesByFile.get(path53) ?? []) {
|
|
9733
|
+
if (node2.kind === "file" || node2.kind === "document") continue;
|
|
9734
|
+
if (!nodeTouchedByChange(node2, file)) continue;
|
|
9735
|
+
changedNodeIds.add(node2.id);
|
|
9736
|
+
symbols.push({
|
|
9737
|
+
node_id: node2.id,
|
|
9738
|
+
name: node2.name,
|
|
9739
|
+
kind: node2.kind,
|
|
9740
|
+
path: path53,
|
|
9741
|
+
start_line: node2.span.start,
|
|
9742
|
+
end_line: node2.span.end
|
|
9743
|
+
});
|
|
9744
|
+
}
|
|
9745
|
+
}
|
|
9746
|
+
}
|
|
9747
|
+
const addedEdges = [];
|
|
9748
|
+
const removedEdges = [];
|
|
9749
|
+
const nodeById = /* @__PURE__ */ new Map();
|
|
9750
|
+
if (graph) for (const n of graph.nodes) nodeById.set(n.id, n);
|
|
9751
|
+
const edgeEvidenceId = (e) => `edge:${e.id.slice(0, 12)}`;
|
|
9752
|
+
const seenAdded = /* @__PURE__ */ new Set();
|
|
9753
|
+
if (graph) {
|
|
9754
|
+
for (const edge of graph.edges) {
|
|
9755
|
+
if (!BOUNDARY_EDGE_KINDS.has(edge.kind)) continue;
|
|
9756
|
+
const src = nodeById.get(edge.src);
|
|
9757
|
+
const dst = nodeById.get(edge.dst);
|
|
9758
|
+
if (!src || !dst) continue;
|
|
9759
|
+
const fromPath = normalize3(src.file);
|
|
9760
|
+
const toPath = normalize3(dst.file);
|
|
9761
|
+
if (fromPath === toPath) continue;
|
|
9762
|
+
if (!changedNodeIds.has(edge.src)) continue;
|
|
9763
|
+
const file = changedByPath.get(fromPath);
|
|
9764
|
+
const op = file?.op ?? "modified";
|
|
9765
|
+
const baseText = input.baseFileText?.get(fromPath);
|
|
9766
|
+
if (!isIntroducedEdge(op, fromPath, toPath, baseText)) continue;
|
|
9767
|
+
const pair = `${fromPath}\0${toPath}\0${edge.kind}`;
|
|
9768
|
+
if (seenAdded.has(pair)) continue;
|
|
9769
|
+
seenAdded.add(pair);
|
|
9770
|
+
const from = classify(fromPath);
|
|
9771
|
+
const to = classify(toPath);
|
|
9772
|
+
const id = addEvidence({
|
|
9773
|
+
id: edgeEvidenceId(edge),
|
|
9774
|
+
kind: "graph_edge",
|
|
9775
|
+
path: fromPath,
|
|
9776
|
+
start_line: src.span.start,
|
|
9777
|
+
end_line: src.span.end,
|
|
9778
|
+
protected_finding: false,
|
|
9779
|
+
note: `${edge.kind} \u2192 ${toPath}`
|
|
9780
|
+
});
|
|
9781
|
+
addedEdges.push({
|
|
9782
|
+
evidence_id: id,
|
|
9783
|
+
kind: edge.kind,
|
|
9784
|
+
from_path: fromPath,
|
|
9785
|
+
to_path: toPath,
|
|
9786
|
+
from_layer: from?.layer,
|
|
9787
|
+
to_layer: to?.layer
|
|
9788
|
+
});
|
|
9789
|
+
}
|
|
9790
|
+
const knownFiles = [...nodesByFile.keys()];
|
|
9791
|
+
let removedN = 0;
|
|
9792
|
+
for (const file of change.files) {
|
|
9793
|
+
const fromPath = normalize3(file.path);
|
|
9794
|
+
const baseText = input.baseFileText?.get(fromPath);
|
|
9795
|
+
const headText = input.headFileText?.get(fromPath);
|
|
9796
|
+
if (baseText === void 0 || headText === void 0) continue;
|
|
9797
|
+
for (const toPath of removedDestinations(fromPath, baseText, headText, knownFiles)) {
|
|
9798
|
+
removedN++;
|
|
9799
|
+
const from = classify(fromPath);
|
|
9800
|
+
const to = classify(toPath);
|
|
9801
|
+
const id = addEvidence({
|
|
9802
|
+
id: `edge:removed:${removedN}`,
|
|
9803
|
+
kind: "graph_edge",
|
|
9804
|
+
path: fromPath,
|
|
9805
|
+
protected_finding: false,
|
|
9806
|
+
note: `removed \u2192 ${toPath}`
|
|
9807
|
+
});
|
|
9808
|
+
removedEdges.push({
|
|
9809
|
+
evidence_id: id,
|
|
9810
|
+
kind: "import",
|
|
9811
|
+
from_path: fromPath,
|
|
9812
|
+
to_path: toPath,
|
|
9813
|
+
from_layer: from?.layer,
|
|
9814
|
+
to_layer: to?.layer
|
|
9815
|
+
});
|
|
9816
|
+
}
|
|
9817
|
+
}
|
|
9818
|
+
}
|
|
9819
|
+
const roles = [];
|
|
9820
|
+
const roleCounter = /* @__PURE__ */ new Map();
|
|
9821
|
+
const roleEvidenceId = (role) => {
|
|
9822
|
+
const n = (roleCounter.get(role) ?? 0) + 1;
|
|
9823
|
+
roleCounter.set(role, n);
|
|
9824
|
+
return `role:${role}:${n}`;
|
|
9825
|
+
};
|
|
9826
|
+
const emitRole = (path53, changed) => {
|
|
9827
|
+
const cls = classify(path53);
|
|
9828
|
+
if (!cls) return;
|
|
9829
|
+
const role = cls.role ?? cls.layer;
|
|
9830
|
+
const id = addEvidence({
|
|
9831
|
+
id: roleEvidenceId(role),
|
|
9832
|
+
kind: "role",
|
|
9833
|
+
path: path53,
|
|
9834
|
+
protected_finding: false,
|
|
9835
|
+
note: `${role} (${cls.layer}, confidence ${cls.confidence.toFixed(2)})`
|
|
9836
|
+
});
|
|
9837
|
+
roles.push({ evidence_id: id, path: path53, role, layer: cls.layer, confidence: cls.confidence, changed });
|
|
9838
|
+
};
|
|
9839
|
+
for (const path53 of [...changedPaths].sort()) emitRole(path53, true);
|
|
9840
|
+
const peerPool = graph ? [...nodesByFile.keys()].sort() : [];
|
|
9841
|
+
const changedRoles = new Set(roles.filter((r) => r.changed).map((r) => r.role));
|
|
9842
|
+
for (const role of [...changedRoles].sort()) {
|
|
9843
|
+
let taken = 0;
|
|
9844
|
+
for (const path53 of peerPool) {
|
|
9845
|
+
if (taken >= MAX_ROLE_PEERS) break;
|
|
9846
|
+
if (changedPaths.has(path53)) continue;
|
|
9847
|
+
const cls = classify(path53);
|
|
9848
|
+
if (!cls) continue;
|
|
9849
|
+
if ((cls.role ?? cls.layer) !== role) continue;
|
|
9850
|
+
emitRole(path53, false);
|
|
9851
|
+
taken++;
|
|
9852
|
+
}
|
|
9853
|
+
}
|
|
9854
|
+
const outgoingLayers = /* @__PURE__ */ new Map();
|
|
9855
|
+
if (graph) {
|
|
9856
|
+
for (const edge of graph.edges) {
|
|
9857
|
+
if (!BOUNDARY_EDGE_KINDS.has(edge.kind)) continue;
|
|
9858
|
+
const src = nodeById.get(edge.src);
|
|
9859
|
+
const dst = nodeById.get(edge.dst);
|
|
9860
|
+
if (!src || !dst) continue;
|
|
9861
|
+
const from = normalize3(src.file);
|
|
9862
|
+
const to = normalize3(dst.file);
|
|
9863
|
+
if (from === to) continue;
|
|
9864
|
+
const toLayer = classify(to)?.layer;
|
|
9865
|
+
if (!toLayer) continue;
|
|
9866
|
+
let set = outgoingLayers.get(from);
|
|
9867
|
+
if (!set) outgoingLayers.set(from, set = /* @__PURE__ */ new Set());
|
|
9868
|
+
set.add(toLayer);
|
|
9869
|
+
}
|
|
9870
|
+
}
|
|
9871
|
+
const areaOfFile = /* @__PURE__ */ new Map();
|
|
9872
|
+
if (graph) {
|
|
9873
|
+
for (const area of graph.areas) {
|
|
9874
|
+
for (const member of area.members) {
|
|
9875
|
+
const node2 = nodeById.get(member);
|
|
9876
|
+
if (node2) areaOfFile.set(normalize3(node2.file), area.id);
|
|
9877
|
+
}
|
|
9878
|
+
}
|
|
9879
|
+
}
|
|
9880
|
+
const dataAccess = /* @__PURE__ */ new Map();
|
|
9881
|
+
const candidates = [];
|
|
9882
|
+
for (const filePath of graph ? [...nodesByFile.keys()].sort() : []) {
|
|
9883
|
+
const cls = classify(filePath);
|
|
9884
|
+
if (!cls) continue;
|
|
9885
|
+
const pattern = classifyDataAccess(cls.layer, [...outgoingLayers.get(filePath) ?? []]);
|
|
9886
|
+
if (!pattern) continue;
|
|
9887
|
+
dataAccess.set(filePath, pattern);
|
|
9888
|
+
const areaId = areaOfFile.get(filePath);
|
|
9889
|
+
const role = cls.role ?? cls.layer;
|
|
9890
|
+
candidates.push({
|
|
9891
|
+
path: filePath,
|
|
9892
|
+
pattern,
|
|
9893
|
+
role,
|
|
9894
|
+
areaGroup: areaId !== void 0 ? `area:${areaId}:${role}` : null,
|
|
9895
|
+
days: input.recencyDays?.get(filePath) ?? null
|
|
9896
|
+
});
|
|
9897
|
+
}
|
|
9898
|
+
const areaCounts = /* @__PURE__ */ new Map();
|
|
9899
|
+
for (const c2 of candidates) {
|
|
9900
|
+
if (c2.areaGroup) areaCounts.set(c2.areaGroup, (areaCounts.get(c2.areaGroup) ?? 0) + 1);
|
|
9901
|
+
}
|
|
9902
|
+
const peerFiles = candidates.map((c2) => {
|
|
9903
|
+
const useArea = c2.areaGroup !== null && (areaCounts.get(c2.areaGroup) ?? 0) >= MIN_GROUP_SIZE;
|
|
9904
|
+
return {
|
|
9905
|
+
path: c2.path,
|
|
9906
|
+
pattern: c2.pattern,
|
|
9907
|
+
group: useArea ? c2.areaGroup : `role:${c2.role}`,
|
|
9908
|
+
groupKind: useArea ? "area" : "role",
|
|
9909
|
+
daysSinceCommit: c2.days
|
|
9910
|
+
};
|
|
9911
|
+
});
|
|
9912
|
+
const votes = voteAll(peerFiles, { declaredPatterns: input.intent?.patterns });
|
|
9913
|
+
const observed = observedDominantPattern(roles);
|
|
9914
|
+
const declared = config.target_pattern ?? input.intent?.patterns[0] ?? null;
|
|
9915
|
+
const patterns = {
|
|
9916
|
+
observed_dominant_pattern: observed,
|
|
9917
|
+
declared_target_pattern: declared,
|
|
9918
|
+
approved_exceptions: [...config.approved_exceptions].sort(),
|
|
9919
|
+
legacy_pattern: observed && declared && observed !== declared ? observed : null,
|
|
9920
|
+
unknown: observed === null && declared === null
|
|
9921
|
+
};
|
|
9922
|
+
const changedGroups = new Set(
|
|
9923
|
+
peerFiles.filter((f) => changedPaths.has(f.path)).map((f) => f.group)
|
|
9924
|
+
);
|
|
9925
|
+
for (const vote of votes) {
|
|
9926
|
+
if (!changedGroups.has(vote.group)) continue;
|
|
9927
|
+
addEvidence({
|
|
9928
|
+
id: `vote:${vote.group}`,
|
|
9929
|
+
kind: "policy",
|
|
9930
|
+
protected_finding: false,
|
|
9931
|
+
note: vote.reason === "dominant" ? `${vote.size} peers, ${(vote.share * 100).toFixed(0)}% ${vote.dominant} (entropy ${vote.entropy.toFixed(2)})` : `${vote.size} peers, no convention (${vote.reason}, entropy ${vote.entropy.toFixed(2)})`
|
|
9932
|
+
});
|
|
9933
|
+
}
|
|
9934
|
+
for (const citation of input.intent?.citations.slice(0, 4) ?? []) {
|
|
9935
|
+
addEvidence({
|
|
9936
|
+
id: `intent:${citation.file}:${citation.line}`,
|
|
9937
|
+
kind: "policy",
|
|
9938
|
+
path: citation.file,
|
|
9939
|
+
start_line: citation.line,
|
|
9940
|
+
end_line: citation.line,
|
|
9941
|
+
protected_finding: false,
|
|
9942
|
+
note: citation.text
|
|
9943
|
+
});
|
|
9944
|
+
}
|
|
9945
|
+
const profileForRules = declared ?? observed ?? void 0;
|
|
9946
|
+
const policies = boundaryProfileRules(profileForRules).map((rule, i) => ({
|
|
9947
|
+
evidence_id: addEvidence({
|
|
9948
|
+
id: `policy:layering:${i + 1}`,
|
|
9949
|
+
kind: "policy",
|
|
9950
|
+
protected_finding: false,
|
|
9951
|
+
note: rule
|
|
9952
|
+
}),
|
|
9953
|
+
id: `layering-${i + 1}`,
|
|
9954
|
+
rule,
|
|
9955
|
+
source: declared ? "review.toml" : "derived"
|
|
9956
|
+
}));
|
|
9957
|
+
const crossesLayers = addedEdges.some(
|
|
9958
|
+
(e) => e.from_layer && e.to_layer && e.from_layer !== e.to_layer && !EXEMPT_LAYERS.has(e.from_layer) && !EXEMPT_LAYERS.has(e.to_layer)
|
|
9959
|
+
);
|
|
9960
|
+
if (policies.length === 0 && crossesLayers) {
|
|
9961
|
+
unknowns.push("This change crosses a layer boundary and no layering rules are enforced for this repository \u2014 set `target_pattern` in .vibgrate/review.toml to declare one.");
|
|
9962
|
+
}
|
|
9963
|
+
if (patterns.unknown && crossesLayers) {
|
|
9964
|
+
unknowns.push("This change crosses a layer boundary and neither a declared target pattern nor an observed dominant pattern could be established \u2014 alignment is reported as unknown, not as a pass.");
|
|
9965
|
+
}
|
|
9966
|
+
const paths = addedEdges.filter((e) => e.from_layer && e.to_layer && e.from_layer !== e.to_layer).filter((e) => !EXEMPT_LAYERS.has(e.from_layer) && !EXEMPT_LAYERS.has(e.to_layer)).map((e) => ({
|
|
9967
|
+
evidence_id: e.evidence_id,
|
|
9968
|
+
from: e.from_path,
|
|
9969
|
+
to: e.to_path,
|
|
9970
|
+
hops: [e.from_layer, e.to_layer],
|
|
9971
|
+
// Guard observation needs taint/authorization analysis this slice does
|
|
9972
|
+
// not run. `null` says "not observed" — never `true`.
|
|
9973
|
+
guarded: null
|
|
9974
|
+
}));
|
|
9975
|
+
if (paths.length > 0) {
|
|
9976
|
+
unknowns.push("Guard presence along the changed call paths was not observed \u2014 no runtime authorization test was supplied.");
|
|
9977
|
+
}
|
|
9978
|
+
const verification = changedFileCoverage(graph, changedPaths).map((v, i) => ({
|
|
9979
|
+
evidence_id: addEvidence({
|
|
9980
|
+
id: `verify:${v.kind}:${i + 1}`,
|
|
9981
|
+
kind: "graph_node",
|
|
9982
|
+
path: v.path,
|
|
9983
|
+
protected_finding: false,
|
|
9984
|
+
note: v.detail
|
|
9985
|
+
}),
|
|
9986
|
+
kind: v.kind,
|
|
9987
|
+
path: v.path,
|
|
9988
|
+
detail: v.detail
|
|
9989
|
+
}));
|
|
9990
|
+
const areas = graph ? graph.areas.map((a) => ({
|
|
9991
|
+
id: a.id,
|
|
9992
|
+
label: a.label,
|
|
9993
|
+
size: a.size,
|
|
9994
|
+
changed_members: a.members.filter((m) => changedNodeIds.has(m)).length
|
|
9995
|
+
})).filter((a) => a.changed_members > 0).sort((a, b) => b.changed_members - a.changed_members).slice(0, 8) : [];
|
|
9996
|
+
const capsule = {
|
|
9997
|
+
schema_version: CAPSULE_SCHEMA,
|
|
9998
|
+
identity: {
|
|
9999
|
+
repo_pseudonym: input.repoPseudonym,
|
|
10000
|
+
language: dominantLanguage(graph, change),
|
|
10001
|
+
graph_schema: graph?.schemaVersion ?? "none",
|
|
10002
|
+
analyzer_versions: { graph: graph?.provenance.version ?? "none", scanners: VERSION },
|
|
10003
|
+
profile
|
|
10004
|
+
},
|
|
10005
|
+
change: {
|
|
10006
|
+
base_sha: change.baseSha,
|
|
10007
|
+
head_sha: change.headSha,
|
|
10008
|
+
dirty: change.dirty,
|
|
10009
|
+
dirty_tree_hash: change.dirtyTreeHash,
|
|
10010
|
+
symbols,
|
|
10011
|
+
ops: change.files.map((f) => ({
|
|
10012
|
+
path: normalize3(f.path),
|
|
10013
|
+
op: f.op,
|
|
10014
|
+
added_lines: f.addedLines,
|
|
10015
|
+
removed_lines: f.removedLines
|
|
10016
|
+
})),
|
|
10017
|
+
added_edges: addedEdges,
|
|
10018
|
+
removed_edges: removedEdges,
|
|
10019
|
+
contract_changes: []
|
|
10020
|
+
},
|
|
10021
|
+
roles,
|
|
10022
|
+
areas,
|
|
10023
|
+
patterns,
|
|
10024
|
+
paths,
|
|
10025
|
+
security: [],
|
|
10026
|
+
policies,
|
|
10027
|
+
verification,
|
|
10028
|
+
evidence
|
|
10029
|
+
};
|
|
10030
|
+
const budget = CAPSULE_BUDGETS[profile];
|
|
10031
|
+
const trimmed = trimToBudget(capsule, budget.cap);
|
|
10032
|
+
return {
|
|
10033
|
+
capsule,
|
|
10034
|
+
votes,
|
|
10035
|
+
dataAccess,
|
|
10036
|
+
unknowns,
|
|
10037
|
+
estimatedTokens: estimateTokens(capsule),
|
|
10038
|
+
budget,
|
|
10039
|
+
trimmed
|
|
10040
|
+
};
|
|
10041
|
+
}
|
|
10042
|
+
function trimToBudget(capsule, cap) {
|
|
10043
|
+
let trimmed = false;
|
|
10044
|
+
const drop = () => {
|
|
10045
|
+
if (capsule.roles.some((r) => !r.changed)) {
|
|
10046
|
+
const keep = new Set(capsule.roles.filter((r) => r.changed).map((r) => r.evidence_id));
|
|
10047
|
+
const dropped = capsule.roles.filter((r) => !r.changed).map((r) => r.evidence_id);
|
|
10048
|
+
capsule.roles = capsule.roles.filter((r) => r.changed || keep.has(r.evidence_id));
|
|
10049
|
+
capsule.evidence = capsule.evidence.filter((e) => !dropped.includes(e.id) || e.protected_finding);
|
|
10050
|
+
return true;
|
|
10051
|
+
}
|
|
10052
|
+
if (capsule.areas.length > 0) {
|
|
10053
|
+
capsule.areas = [];
|
|
10054
|
+
return true;
|
|
10055
|
+
}
|
|
10056
|
+
return false;
|
|
10057
|
+
};
|
|
10058
|
+
while (estimateTokens(capsule) > cap) {
|
|
10059
|
+
if (!drop()) break;
|
|
10060
|
+
trimmed = true;
|
|
10061
|
+
}
|
|
10062
|
+
return trimmed;
|
|
10063
|
+
}
|
|
10064
|
+
function observedDominantPattern(roles) {
|
|
10065
|
+
const layers = new Set(roles.map((r) => r.layer).filter((l) => !EXEMPT_LAYERS.has(l)));
|
|
10066
|
+
if (layers.has("domain") && (layers.has("data-access") || layers.has("infrastructure")) && layers.has("services")) {
|
|
10067
|
+
return "clean";
|
|
10068
|
+
}
|
|
10069
|
+
if (layers.size >= 3) return "layered";
|
|
10070
|
+
return null;
|
|
10071
|
+
}
|
|
10072
|
+
function dominantLanguage(graph, change) {
|
|
10073
|
+
if (graph && graph.meta.languages.length > 0) return graph.meta.languages[0];
|
|
10074
|
+
const counts = /* @__PURE__ */ new Map();
|
|
10075
|
+
for (const f of change.files) {
|
|
10076
|
+
const ext = f.path.split(".").pop() ?? "";
|
|
10077
|
+
counts.set(ext, (counts.get(ext) ?? 0) + 1);
|
|
10078
|
+
}
|
|
10079
|
+
const top = [...counts.entries()].sort((a, b) => b[1] - a[1] || (a[0] < b[0] ? -1 : 1))[0];
|
|
10080
|
+
return top?.[0] ?? "unknown";
|
|
10081
|
+
}
|
|
10082
|
+
function changedFileCoverage(graph, changedPaths) {
|
|
10083
|
+
if (!graph) return [];
|
|
10084
|
+
const testedFiles = /* @__PURE__ */ new Set();
|
|
10085
|
+
const nodeById = new Map(graph.nodes.map((n) => [n.id, n]));
|
|
10086
|
+
for (const edge of graph.edges) {
|
|
10087
|
+
if (edge.kind !== "test" && edge.kind !== "coverage") continue;
|
|
10088
|
+
const dst = nodeById.get(edge.dst);
|
|
10089
|
+
if (dst) testedFiles.add(normalize3(dst.file));
|
|
10090
|
+
}
|
|
10091
|
+
const out2 = [];
|
|
10092
|
+
for (const path53 of [...changedPaths].sort()) {
|
|
10093
|
+
if (/(^|\/)(__tests__|tests?|spec)\//i.test(path53) || /\.(test|spec)\.[^.]+$/i.test(path53)) continue;
|
|
10094
|
+
out2.push(
|
|
10095
|
+
testedFiles.has(path53) ? { kind: "test_covering_change", path: path53, detail: "a test edge reaches this file" } : { kind: "no_test_covering_change", path: path53, detail: "no test edge reaches this file in the map" }
|
|
10096
|
+
);
|
|
10097
|
+
}
|
|
10098
|
+
return out2.slice(0, 25);
|
|
10099
|
+
}
|
|
10100
|
+
function repoPseudonym(remote, root3) {
|
|
10101
|
+
return digestString(`vg.review.pseudonym\0${remote ?? `local:${root3}`}`);
|
|
10102
|
+
}
|
|
10103
|
+
var REVIEW_CONFIG_PATH = ".vibgrate/review.toml";
|
|
10104
|
+
var DEFAULT_REVIEW_CONFIG = {
|
|
10105
|
+
enforcement: "advisory",
|
|
10106
|
+
fail_on: "fail",
|
|
10107
|
+
target_pattern: null,
|
|
10108
|
+
approved_exceptions: [],
|
|
10109
|
+
protected: {
|
|
10110
|
+
unguarded_entrypoint: true,
|
|
10111
|
+
known_vulnerable_dependency: true,
|
|
10112
|
+
validated_taint: true
|
|
10113
|
+
},
|
|
10114
|
+
high_confidence_threshold: 0.8,
|
|
10115
|
+
high_severity_decision: "fail",
|
|
10116
|
+
source: "defaults"
|
|
10117
|
+
};
|
|
10118
|
+
function bool(value, fallback) {
|
|
10119
|
+
return typeof value === "boolean" ? value : fallback;
|
|
10120
|
+
}
|
|
10121
|
+
function str(value, allowed, fallback) {
|
|
10122
|
+
return typeof value === "string" && allowed.includes(value) ? value : fallback;
|
|
10123
|
+
}
|
|
10124
|
+
function parseReviewConfig(text, source) {
|
|
10125
|
+
const doc = parseToml(text);
|
|
10126
|
+
if (!doc) return { ...DEFAULT_REVIEW_CONFIG, source };
|
|
10127
|
+
const review = doc.review ?? {};
|
|
10128
|
+
const prot = review.protected ?? {};
|
|
10129
|
+
const exceptions = Array.isArray(review.approved_exceptions) ? review.approved_exceptions.filter((e) => typeof e === "string") : [];
|
|
10130
|
+
const threshold = typeof review.high_confidence_threshold === "number" ? Math.min(1, Math.max(0, review.high_confidence_threshold)) : DEFAULT_REVIEW_CONFIG.high_confidence_threshold;
|
|
10131
|
+
return {
|
|
10132
|
+
enforcement: str(review.enforcement, ["advisory", "enforced"], DEFAULT_REVIEW_CONFIG.enforcement),
|
|
10133
|
+
fail_on: str(review.fail_on, ["none", "fail", "needs_review"], DEFAULT_REVIEW_CONFIG.fail_on),
|
|
10134
|
+
target_pattern: typeof review.target_pattern === "string" ? review.target_pattern : null,
|
|
10135
|
+
approved_exceptions: exceptions,
|
|
10136
|
+
protected: {
|
|
10137
|
+
unguarded_entrypoint: bool(prot.unguarded_entrypoint, DEFAULT_REVIEW_CONFIG.protected.unguarded_entrypoint),
|
|
10138
|
+
known_vulnerable_dependency: bool(
|
|
10139
|
+
prot.known_vulnerable_dependency,
|
|
10140
|
+
DEFAULT_REVIEW_CONFIG.protected.known_vulnerable_dependency
|
|
10141
|
+
),
|
|
10142
|
+
validated_taint: bool(prot.validated_taint, DEFAULT_REVIEW_CONFIG.protected.validated_taint)
|
|
10143
|
+
},
|
|
10144
|
+
high_confidence_threshold: threshold,
|
|
10145
|
+
high_severity_decision: str(
|
|
10146
|
+
review.high_severity_decision,
|
|
10147
|
+
["fail", "needs_review"],
|
|
10148
|
+
DEFAULT_REVIEW_CONFIG.high_severity_decision
|
|
10149
|
+
),
|
|
10150
|
+
source
|
|
10151
|
+
};
|
|
10152
|
+
}
|
|
10153
|
+
function loadReviewConfig(root3, base, run) {
|
|
10154
|
+
const fromRef = (ref) => {
|
|
10155
|
+
const res = run(["show", `${ref}:${REVIEW_CONFIG_PATH}`], root3);
|
|
10156
|
+
return res.status === 0 && res.stdout.trim() ? res.stdout : null;
|
|
10157
|
+
};
|
|
10158
|
+
if (base) {
|
|
10159
|
+
const text = fromRef(base);
|
|
10160
|
+
if (text) return parseReviewConfig(text, "base-branch");
|
|
10161
|
+
}
|
|
10162
|
+
const head = fromRef("HEAD");
|
|
10163
|
+
if (head) return parseReviewConfig(head, "head");
|
|
10164
|
+
const local = path51.join(root3, REVIEW_CONFIG_PATH);
|
|
10165
|
+
if (fs12.existsSync(local)) {
|
|
10166
|
+
try {
|
|
10167
|
+
return parseReviewConfig(fs12.readFileSync(local, "utf8"), "working-tree");
|
|
10168
|
+
} catch {
|
|
10169
|
+
}
|
|
10170
|
+
}
|
|
10171
|
+
return { ...DEFAULT_REVIEW_CONFIG };
|
|
10172
|
+
}
|
|
10173
|
+
|
|
10174
|
+
// src/review/layers.ts
|
|
10175
|
+
var LAYERED_STACK = [
|
|
10176
|
+
"presentation",
|
|
10177
|
+
"routing",
|
|
10178
|
+
"middleware",
|
|
10179
|
+
"services",
|
|
10180
|
+
"domain",
|
|
10181
|
+
"data-access",
|
|
10182
|
+
"infrastructure"
|
|
10183
|
+
];
|
|
10184
|
+
var RANK = new Map(LAYERED_STACK.map((l, i) => [l, i]));
|
|
10185
|
+
var SKIP_PROFILES = /* @__PURE__ */ new Set(["layered", "mvc", "mvvm"]);
|
|
10186
|
+
var CLEAN_PROFILES = /* @__PURE__ */ new Set(["clean", "hexagonal", "onion"]);
|
|
10187
|
+
var OUTER_LAYERS = /* @__PURE__ */ new Set(["presentation", "routing", "middleware"]);
|
|
10188
|
+
var PERSISTENCE_LAYERS = /* @__PURE__ */ new Set(["data-access", "infrastructure"]);
|
|
10189
|
+
var NO_SKIP = { skipped: false, bypassed: [], rule: "" };
|
|
10190
|
+
function evaluateLayerSkip(profile, from, to) {
|
|
10191
|
+
if (CLEAN_PROFILES.has(profile)) {
|
|
10192
|
+
if (OUTER_LAYERS.has(from) && PERSISTENCE_LAYERS.has(to)) {
|
|
10193
|
+
return {
|
|
10194
|
+
skipped: true,
|
|
10195
|
+
bypassed: ["services"],
|
|
10196
|
+
rule: `${profile}:outer-ring:${from}\u2192${to}`
|
|
10197
|
+
};
|
|
10198
|
+
}
|
|
10199
|
+
return NO_SKIP;
|
|
10200
|
+
}
|
|
10201
|
+
if (!SKIP_PROFILES.has(profile)) return NO_SKIP;
|
|
10202
|
+
const a = RANK.get(from);
|
|
10203
|
+
const b = RANK.get(to);
|
|
10204
|
+
if (a === void 0 || b === void 0) return NO_SKIP;
|
|
10205
|
+
if (b <= a + 1) return NO_SKIP;
|
|
10206
|
+
const bypassed = LAYERED_STACK.slice(a + 1, b).filter((l) => l !== "domain");
|
|
10207
|
+
if (bypassed.length === 0) return NO_SKIP;
|
|
10208
|
+
return { skipped: true, bypassed, rule: `${profile}:skip:${from}\u2192${to}` };
|
|
10209
|
+
}
|
|
10210
|
+
|
|
10211
|
+
// src/review/scanners.ts
|
|
10212
|
+
var EXEMPT_LAYERS2 = /* @__PURE__ */ new Set(["config", "shared", "testing"]);
|
|
10213
|
+
var DEPENDENCY_MANIFESTS = /(^|\/)(package\.json|pnpm-lock\.yaml|package-lock\.json|yarn\.lock|.*\.csproj|packages\.lock\.json|Directory\.Packages\.props|requirements.*\.txt|pyproject\.toml|poetry\.lock|go\.mod|go\.sum|Cargo\.toml|Cargo\.lock|pom\.xml|build\.gradle(\.kts)?|Gemfile(\.lock)?|composer\.(json|lock))$/i;
|
|
10214
|
+
var GUARD_PATTERNS = [
|
|
10215
|
+
/\b(authorize|authorise|authenticate|requireAuth|requireUser|ensureAuth|checkPermission|hasPermission|can|verifyToken|verifyJwt|assertScope|requireScope|guard)\s*\(/i,
|
|
10216
|
+
/\[\s*Authorize\b/,
|
|
10217
|
+
// C# attribute
|
|
10218
|
+
/@(PreAuthorize|Secured|RolesAllowed|UseGuards)\b/,
|
|
10219
|
+
// Spring / Nest
|
|
10220
|
+
/\buse(Auth|Guard|Session)\b/,
|
|
10221
|
+
/\b(csrf|rateLimit|validateRequest|zod|schema)\.(parse|safeParse|validate)\s*\(/i
|
|
10222
|
+
];
|
|
10223
|
+
var ENTRYPOINT_LAYERS = /* @__PURE__ */ new Set(["routing", "middleware"]);
|
|
10224
|
+
var ENTRYPOINT_ROLES = /* @__PURE__ */ new Set(["controller", "handler", "router"]);
|
|
10225
|
+
function alignment(capsule, fromLayer, toLayer) {
|
|
10226
|
+
const pair = `${fromLayer}->${toLayer}`;
|
|
10227
|
+
if (capsule.patterns.approved_exceptions.includes(pair)) return "approved_exception";
|
|
10228
|
+
if (capsule.patterns.unknown) return "unknown";
|
|
10229
|
+
if (capsule.patterns.declared_target_pattern) return "regression";
|
|
10230
|
+
return "legacy_consistent";
|
|
10231
|
+
}
|
|
10232
|
+
function runScanners(input) {
|
|
10233
|
+
const { capsule, config } = input;
|
|
10234
|
+
const architecture = [];
|
|
10235
|
+
const security = [];
|
|
10236
|
+
const unknowns = [];
|
|
10237
|
+
const requiredChecks = [];
|
|
10238
|
+
const profile = capsule.patterns.declared_target_pattern ?? capsule.patterns.observed_dominant_pattern;
|
|
10239
|
+
if (profile) {
|
|
10240
|
+
let n = 0;
|
|
10241
|
+
for (const edge of capsule.change.added_edges) {
|
|
10242
|
+
const from = edge.from_layer;
|
|
10243
|
+
const to = edge.to_layer;
|
|
10244
|
+
if (!from || !to) continue;
|
|
10245
|
+
if (EXEMPT_LAYERS2.has(from) || EXEMPT_LAYERS2.has(to)) continue;
|
|
10246
|
+
const illegal = evaluateLayerBoundary(profile, from, to, edge.from_path, edge.to_path);
|
|
10247
|
+
const skip = capsule.patterns.declared_target_pattern ? evaluateLayerSkip(profile, from, to) : { skipped: false, bypassed: []};
|
|
10248
|
+
if (!illegal.illegal && !skip.skipped) continue;
|
|
10249
|
+
const align = alignment(capsule, from, to);
|
|
10250
|
+
const declared = Boolean(capsule.patterns.declared_target_pattern);
|
|
10251
|
+
n++;
|
|
10252
|
+
architecture.push({
|
|
10253
|
+
id: `arch-${String(n).padStart(2, "0")}`,
|
|
10254
|
+
kind: "boundary_bypass",
|
|
10255
|
+
// A declared target makes this a regression against a stated intent;
|
|
10256
|
+
// without one it is an observation about the repo's own majority.
|
|
10257
|
+
severity: declared ? "high" : "medium",
|
|
10258
|
+
confidence: declared ? 0.93 : 0.6,
|
|
10259
|
+
claim: skip.skipped ? `A changed ${from} file depends directly on ${to}, skipping ${skip.bypassed.join(", ")} (${edge.from_path} \u2192 ${edge.to_path}).` : `A changed ${from} file depends on ${to}, against the ${illegal.rule} rule (${edge.from_path} \u2192 ${edge.to_path}).`,
|
|
10260
|
+
evidence_ids: [edge.evidence_id, ...policyEvidenceIds(capsule)],
|
|
10261
|
+
target_alignment: align,
|
|
10262
|
+
remediation: `Route the operation through the ${intermediateLayer(from, to)} layer instead of calling ${to} directly.`,
|
|
10263
|
+
paths: [edge.from_path, edge.to_path],
|
|
10264
|
+
protected_finding: false,
|
|
10265
|
+
source: "scanner"
|
|
10266
|
+
});
|
|
10267
|
+
}
|
|
10268
|
+
}
|
|
10269
|
+
let sec = 0;
|
|
10270
|
+
if (config.protected.unguarded_entrypoint) {
|
|
10271
|
+
for (const [filePath, lines] of [...input.removedLines.entries()].sort()) {
|
|
10272
|
+
const removedGuards = lines.filter((l) => GUARD_PATTERNS.some((p) => p.test(l)));
|
|
10273
|
+
if (removedGuards.length === 0) continue;
|
|
10274
|
+
const stillGuarded = GUARD_PATTERNS.some((p) => p.test(input.fileText.get(filePath) ?? ""));
|
|
10275
|
+
if (stillGuarded) continue;
|
|
10276
|
+
sec++;
|
|
10277
|
+
security.push({
|
|
10278
|
+
id: `sec-${String(sec).padStart(2, "0")}`,
|
|
10279
|
+
kind: "guard_removed",
|
|
10280
|
+
severity: "high",
|
|
10281
|
+
confidence: 0.9,
|
|
10282
|
+
claim: `An authorization or validation guard was removed from ${filePath} and no equivalent guard remains in the file.`,
|
|
10283
|
+
evidence_ids: [roleEvidenceFor(capsule, filePath) ?? sourceSpanEvidence(capsule, filePath)],
|
|
10284
|
+
target_alignment: "regression",
|
|
10285
|
+
remediation: "Restore the guard, or move it to a middleware the changed path provably passes through.",
|
|
10286
|
+
paths: [filePath],
|
|
10287
|
+
protected_finding: true,
|
|
10288
|
+
source: "scanner"
|
|
10289
|
+
});
|
|
10290
|
+
}
|
|
10291
|
+
}
|
|
10292
|
+
if (config.protected.unguarded_entrypoint) {
|
|
10293
|
+
const classified = [];
|
|
10294
|
+
for (const [filePath, text] of [...input.fileText.entries()].sort()) {
|
|
10295
|
+
for (const route of routesForFile(filePath, text)) classified.push(classifyRoute(route));
|
|
10296
|
+
}
|
|
10297
|
+
for (const [filePath, text] of [...input.peerFileText?.entries() ?? []].sort()) {
|
|
10298
|
+
if (input.fileText.has(filePath)) continue;
|
|
10299
|
+
for (const route of routesForFile(filePath, text)) classified.push(classifyRoute(route));
|
|
10300
|
+
}
|
|
10301
|
+
const changedFiles = input.changedPaths ?? /* @__PURE__ */ new Set();
|
|
10302
|
+
for (const vote of voteAllAuth(classified)) {
|
|
10303
|
+
for (const route of vote.deviators) {
|
|
10304
|
+
if (!changedFiles.has(route.file)) continue;
|
|
10305
|
+
sec++;
|
|
10306
|
+
security.push({
|
|
10307
|
+
id: `sec-${String(sec).padStart(2, "0")}`,
|
|
10308
|
+
kind: "unguarded_entrypoint",
|
|
10309
|
+
severity: vote.aboveFloor ? "high" : "medium",
|
|
10310
|
+
confidence: vote.aboveFloor ? Math.min(0.95, vote.share) : 0.5,
|
|
10311
|
+
claim: `${route.method} ${route.path || route.file} has no authorization guard in scope, while ${vote.guarded} of ${vote.classified} mutating routes in ${vote.group} do (${route.rule}).`,
|
|
10312
|
+
evidence_ids: [roleEvidenceFor(capsule, route.file) ?? sourceSpanEvidence(capsule, route.file)],
|
|
10313
|
+
target_alignment: "regression",
|
|
10314
|
+
remediation: "Add an authorization check, or register the route behind a middleware it provably passes through.",
|
|
10315
|
+
paths: [route.file],
|
|
10316
|
+
// Below the peer floor this is a real observation but not a claim
|
|
10317
|
+
// strong enough to block a merge, so it must not be protected.
|
|
10318
|
+
protected_finding: vote.aboveFloor,
|
|
10319
|
+
source: "scanner"
|
|
10320
|
+
});
|
|
10321
|
+
if (vote.aboveFloor) requiredChecks.push("authz-test");
|
|
10322
|
+
}
|
|
10323
|
+
const changedUnsure = vote.unsure.filter((r) => changedFiles.has(r.file));
|
|
10324
|
+
if (changedUnsure.length > 0) {
|
|
10325
|
+
unknowns.push(
|
|
10326
|
+
`${changedUnsure.length} changed route(s) in ${vote.group} could not be classified as guarded or open (${[...new Set(changedUnsure.map((r) => r.rule))].join(", ")}) \u2014 neither confirmed nor excluded.`
|
|
10327
|
+
);
|
|
10328
|
+
}
|
|
10329
|
+
}
|
|
10330
|
+
}
|
|
10331
|
+
const dependencyChanges = capsule.change.ops.filter((o) => DEPENDENCY_MANIFESTS.test(o.path));
|
|
10332
|
+
if (config.protected.known_vulnerable_dependency && dependencyChanges.length > 0) {
|
|
10333
|
+
if (input.vulnerablePackages === null) {
|
|
10334
|
+
unknowns.push(
|
|
10335
|
+
`${dependencyChanges.length} dependency manifest(s) changed and no advisory data was available \u2014 run \`vg scan --vulns\` so \`vg review\` can check them.`
|
|
10336
|
+
);
|
|
10337
|
+
requiredChecks.push("dependency-advisory-check");
|
|
10338
|
+
} else {
|
|
10339
|
+
for (const vuln of input.vulnerablePackages) {
|
|
10340
|
+
const declaredIn = dependencyChanges.find(
|
|
10341
|
+
(o) => (input.fileText.get(o.path) ?? "").includes(vuln.package)
|
|
10342
|
+
);
|
|
10343
|
+
if (!declaredIn) continue;
|
|
10344
|
+
sec++;
|
|
10345
|
+
security.push({
|
|
10346
|
+
id: `sec-${String(sec).padStart(2, "0")}`,
|
|
10347
|
+
kind: "known_vulnerable_dependency",
|
|
10348
|
+
severity: "high",
|
|
10349
|
+
confidence: 1,
|
|
10350
|
+
claim: `A changed dependency manifest declares ${vuln.package}, which has a known advisory: ${vuln.detail}`,
|
|
10351
|
+
evidence_ids: [dependencyEvidence(capsule, declaredIn.path)],
|
|
10352
|
+
target_alignment: "regression",
|
|
10353
|
+
remediation: "Upgrade to a fixed version, or record a scoped, expiry-bound exception.",
|
|
10354
|
+
paths: [declaredIn.path],
|
|
10355
|
+
protected_finding: true,
|
|
10356
|
+
source: "scanner"
|
|
10357
|
+
});
|
|
10358
|
+
}
|
|
10359
|
+
}
|
|
10360
|
+
}
|
|
10361
|
+
const hasTaintSurface = capsule.paths.length > 0 || capsule.roles.some(
|
|
10362
|
+
(r) => r.changed && (ENTRYPOINT_ROLES.has(r.role) || ENTRYPOINT_LAYERS.has(r.layer))
|
|
10363
|
+
);
|
|
10364
|
+
if (config.protected.validated_taint && hasTaintSurface) {
|
|
10365
|
+
unknowns.push("This change touches an entrypoint or a cross-layer path and taint validation was not run \u2014 dataflow analysis is not part of this review slice, so tainted-input findings are neither confirmed nor excluded.");
|
|
10366
|
+
}
|
|
10367
|
+
for (const vote of input.votes ?? []) {
|
|
10368
|
+
if (vote.reason !== "dominant" || !vote.dominant) continue;
|
|
10369
|
+
const changedDeviators = vote.deviators.filter((p) => input.changedPaths?.has(p));
|
|
10370
|
+
if (changedDeviators.length === 0) continue;
|
|
10371
|
+
for (const filePath of changedDeviators) {
|
|
10372
|
+
const actual = input.dataAccess?.get(filePath);
|
|
10373
|
+
if (!actual) continue;
|
|
10374
|
+
if (!isRegression(actual, vote.dominant)) continue;
|
|
10375
|
+
const declared = Boolean(capsule.patterns.declared_target_pattern);
|
|
10376
|
+
architecture.push({
|
|
10377
|
+
id: `arch-${String(architecture.length + 1).padStart(2, "0")}`,
|
|
10378
|
+
kind: "peer_deviation",
|
|
10379
|
+
severity: declared ? "high" : "medium",
|
|
10380
|
+
// The vote's own share is the calibration: a 100%-consistent group of
|
|
10381
|
+
// 20 peers is far stronger evidence than a bare 70% of 3.
|
|
10382
|
+
confidence: Math.min(0.95, vote.share * Math.min(1, vote.size / 8)),
|
|
10383
|
+
claim: `${filePath} ${describePattern(actual)}, while ${(vote.share * 100).toFixed(0)}% of its ${vote.size} ${vote.groupKind} peers ${describePattern(vote.dominant)}.`,
|
|
10384
|
+
evidence_ids: [`vote:${vote.group}`, ...policyEvidenceIds(capsule)].filter(
|
|
10385
|
+
(id) => capsule.evidence.some((e) => e.id === id)
|
|
10386
|
+
),
|
|
10387
|
+
target_alignment: alignment(capsule, void 0, void 0),
|
|
10388
|
+
remediation: `Follow the pattern its peers use \u2014 see ${vote.exemplars.slice(0, 2).join(", ") || "the peer group"}.`,
|
|
10389
|
+
paths: [filePath],
|
|
10390
|
+
protected_finding: false,
|
|
10391
|
+
source: "scanner"
|
|
10392
|
+
});
|
|
10393
|
+
}
|
|
10394
|
+
}
|
|
10395
|
+
if (input.similarity && input.changedBodies) {
|
|
10396
|
+
const changedIds = new Set(input.changedBodies.map((b) => b.id));
|
|
10397
|
+
const reported = /* @__PURE__ */ new Set();
|
|
10398
|
+
for (const body of input.changedBodies) {
|
|
10399
|
+
if (!isComparable(body.file)) continue;
|
|
10400
|
+
const hits = input.similarity.find(body, changedIds).filter((h) => isComparable(h.file));
|
|
10401
|
+
if (hits.length === 0) continue;
|
|
10402
|
+
const key2 = `${body.file}:${body.name}`;
|
|
10403
|
+
if (reported.has(key2)) continue;
|
|
10404
|
+
reported.add(key2);
|
|
10405
|
+
const best = hits[0];
|
|
10406
|
+
architecture.push({
|
|
10407
|
+
id: `arch-${String(architecture.length + 1).padStart(2, "0")}`,
|
|
10408
|
+
kind: "duplicate_implementation",
|
|
10409
|
+
severity: "medium",
|
|
10410
|
+
confidence: Math.min(0.9, best.score),
|
|
10411
|
+
claim: `${body.name} in ${body.file} is structurally ${(best.score * 100).toFixed(0)}% the same as ${best.name} in ${best.file}:${best.startLine}` + (hits.length > 1 ? ` (and ${hits.length - 1} other near-match(es))` : "") + ".",
|
|
10412
|
+
evidence_ids: [duplicateEvidence(capsule, body, best)],
|
|
10413
|
+
// Reusing what exists is what the architecture wants; this is a
|
|
10414
|
+
// deviation from that, but never a *security* regression.
|
|
10415
|
+
target_alignment: capsule.patterns.declared_target_pattern ? "regression" : "unknown",
|
|
10416
|
+
remediation: `Call ${best.name} instead, or extract the shared behaviour if the two genuinely differ.`,
|
|
10417
|
+
paths: [body.file, best.file],
|
|
10418
|
+
protected_finding: false,
|
|
10419
|
+
source: "scanner"
|
|
10420
|
+
});
|
|
10421
|
+
}
|
|
10422
|
+
}
|
|
10423
|
+
const untested = capsule.verification.filter((v) => v.kind === "no_test_covering_change");
|
|
10424
|
+
if (untested.length > 0) {
|
|
10425
|
+
architecture.push({
|
|
10426
|
+
id: `arch-${String(architecture.length + 1).padStart(2, "0")}`,
|
|
10427
|
+
kind: "unverified_change",
|
|
10428
|
+
severity: "medium",
|
|
10429
|
+
confidence: 0.7,
|
|
10430
|
+
claim: `${untested.length} changed file(s) have no test edge reaching them in the code map.`,
|
|
10431
|
+
evidence_ids: untested.slice(0, 8).map((v) => v.evidence_id),
|
|
10432
|
+
target_alignment: "unknown",
|
|
10433
|
+
remediation: "Add a test that exercises the changed call path, or point `vg build` at the coverage report that already covers it.",
|
|
10434
|
+
paths: untested.map((v) => v.path).slice(0, 25),
|
|
10435
|
+
protected_finding: false,
|
|
10436
|
+
source: "scanner"
|
|
10437
|
+
});
|
|
10438
|
+
requiredChecks.push("changed-call-path-test");
|
|
10439
|
+
}
|
|
10440
|
+
return {
|
|
10441
|
+
architecture,
|
|
10442
|
+
security,
|
|
10443
|
+
unknowns,
|
|
10444
|
+
requiredChecks: [...new Set(requiredChecks)].sort()
|
|
10445
|
+
};
|
|
10446
|
+
}
|
|
10447
|
+
function policyEvidenceIds(capsule) {
|
|
10448
|
+
return capsule.policies.slice(0, 2).map((p) => p.evidence_id);
|
|
10449
|
+
}
|
|
10450
|
+
function roleEvidenceFor(capsule, filePath) {
|
|
10451
|
+
return capsule.roles.find((r) => r.path === filePath)?.evidence_id ?? null;
|
|
10452
|
+
}
|
|
10453
|
+
function sourceSpanEvidence(capsule, filePath) {
|
|
10454
|
+
const existing = capsule.evidence.find((e) => e.path === filePath);
|
|
10455
|
+
if (existing) return existing.id;
|
|
10456
|
+
const id = `source_span:${capsule.evidence.length + 1}`;
|
|
10457
|
+
capsule.evidence.push({ id, kind: "source_span", path: filePath, protected_finding: true });
|
|
10458
|
+
return id;
|
|
10459
|
+
}
|
|
10460
|
+
function duplicateEvidence(capsule, body, match) {
|
|
10461
|
+
const id = `duplicate:${body.file}:${body.name}`;
|
|
10462
|
+
if (!capsule.evidence.some((e) => e.id === id)) {
|
|
10463
|
+
capsule.evidence.push({
|
|
10464
|
+
id,
|
|
10465
|
+
kind: "graph_node",
|
|
10466
|
+
path: match.file,
|
|
10467
|
+
start_line: match.startLine,
|
|
10468
|
+
end_line: match.endLine,
|
|
10469
|
+
protected_finding: false,
|
|
10470
|
+
note: `${body.name} (${body.file}:${body.startLine}) ~= ${match.name} (${match.file}:${match.startLine})`
|
|
10471
|
+
});
|
|
10472
|
+
}
|
|
10473
|
+
return id;
|
|
10474
|
+
}
|
|
10475
|
+
function dependencyEvidence(capsule, filePath) {
|
|
10476
|
+
const existing = capsule.evidence.find((e) => e.kind === "dependency" && e.path === filePath);
|
|
10477
|
+
if (existing) return existing.id;
|
|
10478
|
+
const id = `dependency:${filePath}`;
|
|
10479
|
+
capsule.evidence.push({ id, kind: "dependency", path: filePath, protected_finding: true });
|
|
10480
|
+
return id;
|
|
10481
|
+
}
|
|
10482
|
+
function intermediateLayer(from, to) {
|
|
10483
|
+
if (to === "data-access" || to === "infrastructure") return "application service";
|
|
10484
|
+
if (from === "domain") return "port / interface";
|
|
10485
|
+
return "service";
|
|
10486
|
+
}
|
|
10487
|
+
function removedLinesFromDiff(diff2) {
|
|
10488
|
+
const map = /* @__PURE__ */ new Map();
|
|
10489
|
+
let current = null;
|
|
10490
|
+
for (const line of diff2.split("\n")) {
|
|
10491
|
+
if (line.startsWith("--- ")) continue;
|
|
10492
|
+
if (line.startsWith("+++ ")) {
|
|
10493
|
+
const p = line.slice(4).trim();
|
|
10494
|
+
current = p === "/dev/null" ? null : p.replace(/^b\//, "");
|
|
10495
|
+
if (current && !map.has(current)) map.set(current, []);
|
|
10496
|
+
continue;
|
|
10497
|
+
}
|
|
10498
|
+
if (current && line.startsWith("-") && !line.startsWith("---")) {
|
|
10499
|
+
map.get(current).push(line.slice(1));
|
|
10500
|
+
}
|
|
10501
|
+
}
|
|
10502
|
+
return map;
|
|
10503
|
+
}
|
|
10504
|
+
function vulnerablePackagesFromScan(root3) {
|
|
10505
|
+
const file = path51.join(root3, ".vibgrate", "scan_result.json");
|
|
10506
|
+
if (!fs12.existsSync(file)) return null;
|
|
10507
|
+
try {
|
|
10508
|
+
const artifact = JSON.parse(fs12.readFileSync(file, "utf8"));
|
|
10509
|
+
const findings = (artifact.findings ?? []).filter((f) => f.ruleId === "vibgrate/vulnerability");
|
|
10510
|
+
return findings.map((f) => ({
|
|
10511
|
+
package: String(f.details?.package ?? f.location),
|
|
10512
|
+
detail: f.message
|
|
10513
|
+
}));
|
|
10514
|
+
} catch {
|
|
10515
|
+
return null;
|
|
10516
|
+
}
|
|
10517
|
+
}
|
|
10518
|
+
|
|
10519
|
+
// src/review/verify.ts
|
|
10520
|
+
var FORBIDDEN_CLAIMS = [
|
|
10521
|
+
{ pattern: /\b(is|are|remains?|stays?)\s+secure\b/i, label: "secure" },
|
|
10522
|
+
{ pattern: /\bno\s+vulnerabilit(y|ies)\b/i, label: "no vulnerability" },
|
|
10523
|
+
{ pattern: /\b(is|are)\s+safe\b/i, label: "safe" },
|
|
10524
|
+
{ pattern: /\b(is|are)\s+approved\b/i, label: "approved" },
|
|
10525
|
+
{ pattern: /\bvulnerability[- ]free\b/i, label: "vulnerability-free" }
|
|
10526
|
+
];
|
|
10527
|
+
var RUNTIME_CLAIM = /\b(at runtime|when (?:it |this )?runs|in production|will (?:always |never )?(?:throw|fail|return|execute)|executes?)\b/i;
|
|
10528
|
+
var UNCERTAINTY = /\b(may|might|could|appears?|likely|unlikely|not observed|unverified|no runtime|static(?:ally)?)\b/i;
|
|
10529
|
+
var SEVERITIES = /* @__PURE__ */ new Set(["low", "medium", "high", "critical"]);
|
|
10530
|
+
function verifyFinding(finding, domain, evidenceIds, errors, invalidEvidence) {
|
|
10531
|
+
const where = `${domain} finding ${finding.id || "(no id)"}`;
|
|
10532
|
+
if (typeof finding.id !== "string" || !finding.id) errors.push(`${where}: missing id`);
|
|
10533
|
+
if (typeof finding.kind !== "string" || !finding.kind) errors.push(`${where}: missing kind`);
|
|
10534
|
+
if (!SEVERITIES.has(finding.severity)) errors.push(`${where}: invalid severity "${String(finding.severity)}"`);
|
|
10535
|
+
if (typeof finding.confidence !== "number" || finding.confidence < 0 || finding.confidence > 1) {
|
|
10536
|
+
errors.push(`${where}: confidence must be a number in 0..1`);
|
|
10537
|
+
}
|
|
10538
|
+
if (typeof finding.claim !== "string" || !finding.claim.trim()) errors.push(`${where}: missing claim`);
|
|
10539
|
+
if (!TARGET_ALIGNMENTS.includes(finding.target_alignment)) {
|
|
10540
|
+
errors.push(`${where}: invalid target_alignment "${String(finding.target_alignment)}"`);
|
|
10541
|
+
}
|
|
10542
|
+
if (typeof finding.remediation !== "string" || !finding.remediation.trim()) {
|
|
10543
|
+
errors.push(`${where}: missing remediation`);
|
|
10544
|
+
}
|
|
10545
|
+
if (!Array.isArray(finding.paths)) errors.push(`${where}: paths must be an array`);
|
|
10546
|
+
if (!Array.isArray(finding.evidence_ids) || finding.evidence_ids.length === 0) {
|
|
10547
|
+
errors.push(`${where}: claims a flow with no supporting graph or scanner fact`);
|
|
10548
|
+
invalidEvidence.value = true;
|
|
10549
|
+
} else {
|
|
10550
|
+
for (const id of finding.evidence_ids) {
|
|
10551
|
+
if (!evidenceIds.has(id)) {
|
|
10552
|
+
errors.push(`${where}: cites evidence id "${id}" that is not in the capsule`);
|
|
10553
|
+
invalidEvidence.value = true;
|
|
10554
|
+
}
|
|
10555
|
+
}
|
|
10556
|
+
}
|
|
10557
|
+
const prose = `${finding.claim ?? ""} ${finding.remediation ?? ""}`;
|
|
10558
|
+
for (const { pattern, label } of FORBIDDEN_CLAIMS) {
|
|
10559
|
+
if (pattern.test(prose)) errors.push(`${where}: makes the absolute claim "${label}"`);
|
|
10560
|
+
}
|
|
10561
|
+
if (RUNTIME_CLAIM.test(prose) && !UNCERTAINTY.test(prose)) {
|
|
10562
|
+
errors.push(`${where}: asserts runtime behaviour without stating the uncertainty (nothing was executed)`);
|
|
10563
|
+
}
|
|
10564
|
+
}
|
|
10565
|
+
function verifyFindings(findings, capsule) {
|
|
10566
|
+
const errors = [];
|
|
10567
|
+
const invalidEvidence = { value: false };
|
|
10568
|
+
if (findings?.schema_version !== FINDINGS_SCHEMA) {
|
|
10569
|
+
errors.push(`expected schema_version ${FINDINGS_SCHEMA}, got ${String(findings?.schema_version)}`);
|
|
10570
|
+
return { schema_valid: false, evidence_ids_valid: false, errors };
|
|
10571
|
+
}
|
|
10572
|
+
if ("decision" in findings) {
|
|
10573
|
+
errors.push("findings carry a `decision` field \u2014 only policy may decide");
|
|
10574
|
+
}
|
|
10575
|
+
if (!Array.isArray(findings.architecture_findings)) errors.push("architecture_findings must be an array");
|
|
10576
|
+
if (!Array.isArray(findings.security_findings)) errors.push("security_findings must be an array");
|
|
10577
|
+
if (!Array.isArray(findings.unknowns)) errors.push("unknowns must be an array");
|
|
10578
|
+
if (!Array.isArray(findings.required_checks)) errors.push("required_checks must be an array");
|
|
10579
|
+
if (!Array.isArray(findings.change_class)) errors.push("change_class must be an array");
|
|
10580
|
+
const evidenceIds = new Set(capsule.evidence.map((e) => e.id));
|
|
10581
|
+
for (const f of findings.architecture_findings ?? []) {
|
|
10582
|
+
verifyFinding(f, "architecture", evidenceIds, errors, invalidEvidence);
|
|
10583
|
+
}
|
|
10584
|
+
for (const f of findings.security_findings ?? []) {
|
|
10585
|
+
verifyFinding(f, "security", evidenceIds, errors, invalidEvidence);
|
|
10586
|
+
}
|
|
10587
|
+
const ids = [...findings.architecture_findings ?? [], ...findings.security_findings ?? []].map((f) => f.id);
|
|
10588
|
+
const dupes = ids.filter((id, i) => ids.indexOf(id) !== i);
|
|
10589
|
+
for (const id of new Set(dupes)) errors.push(`duplicate finding id "${id}"`);
|
|
10590
|
+
return {
|
|
10591
|
+
schema_valid: errors.length === 0,
|
|
10592
|
+
evidence_ids_valid: !invalidEvidence.value,
|
|
10593
|
+
errors
|
|
10594
|
+
};
|
|
10595
|
+
}
|
|
10596
|
+
|
|
10597
|
+
// src/review/run.ts
|
|
10598
|
+
function defaultRun2(args, cwd) {
|
|
10599
|
+
const res = spawnSync("git", args, {
|
|
10600
|
+
cwd,
|
|
10601
|
+
encoding: "utf8",
|
|
10602
|
+
timeout: 2e4,
|
|
10603
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
10604
|
+
windowsHide: true
|
|
10605
|
+
});
|
|
10606
|
+
return { stdout: res.stdout ?? "", status: res.status ?? 1 };
|
|
10607
|
+
}
|
|
10608
|
+
var NON_CODE = /\.(md|mdx|markdown|txt|rst|adoc|png|jpe?g|gif|svg|ico|webp|woff2?|ttf|eot|pdf|csv|snap|lock)$/i;
|
|
10609
|
+
var NON_CODE_DIRS = /(^|\/)(docs?|\.github\/ISSUE_TEMPLATE|changelog|marketing)\//i;
|
|
10610
|
+
var DEPENDENCY_MANIFEST = /(^|\/)(package\.json|pnpm-lock\.yaml|package-lock\.json|yarn\.lock|.*\.csproj|packages\.lock\.json|Directory\.Packages\.props|requirements.*\.txt|pyproject\.toml|poetry\.lock|go\.mod|go\.sum|Cargo\.toml|Cargo\.lock|pom\.xml|build\.gradle(\.kts)?|Gemfile(\.lock)?|composer\.(json|lock))$/i;
|
|
10611
|
+
var EXEMPT_LAYERS3 = /* @__PURE__ */ new Set(["config", "shared", "testing"]);
|
|
10612
|
+
function changeClassOf(capsule, findings) {
|
|
10613
|
+
const classes = [];
|
|
10614
|
+
const roleByPath = new Map(capsule.roles.filter((r) => r.changed).map((r) => [r.path, r]));
|
|
10615
|
+
const architecturalPaths = capsule.change.ops.filter((op) => {
|
|
10616
|
+
if (DEPENDENCY_MANIFEST.test(op.path)) return false;
|
|
10617
|
+
if (NON_CODE.test(op.path) || NON_CODE_DIRS.test(op.path)) return false;
|
|
10618
|
+
const role = roleByPath.get(op.path);
|
|
10619
|
+
return role === void 0 || !EXEMPT_LAYERS3.has(role.layer);
|
|
10620
|
+
});
|
|
10621
|
+
if (findings.architecture > 0 || capsule.change.added_edges.length > 0 || architecturalPaths.length > 0) {
|
|
10622
|
+
classes.push("architecture");
|
|
10623
|
+
}
|
|
10624
|
+
const dependencyChanges = capsule.change.ops.filter((op) => DEPENDENCY_MANIFEST.test(op.path));
|
|
10625
|
+
if (findings.security > 0 || capsule.security.length > 0 || dependencyChanges.length > 0) {
|
|
10626
|
+
classes.push("security");
|
|
10627
|
+
}
|
|
10628
|
+
return classes.length > 0 ? classes : ["none"];
|
|
10629
|
+
}
|
|
10630
|
+
async function runReview(opts) {
|
|
10631
|
+
const root3 = path51.resolve(opts.root);
|
|
10632
|
+
const run = opts.run ?? defaultRun2;
|
|
10633
|
+
if (!isGitRepo(root3, run)) {
|
|
10634
|
+
throw new CliError(
|
|
10635
|
+
`\`vg review\` needs a git repository \u2014 ${root3} is not one (or git is not on PATH)`,
|
|
10636
|
+
ExitCode.USAGE_ERROR
|
|
10637
|
+
);
|
|
10638
|
+
}
|
|
10639
|
+
const change = collectChangeSet(root3, opts.base, run);
|
|
10640
|
+
const repoRoot = change.topLevel;
|
|
10641
|
+
const config = loadReviewConfig(repoRoot, opts.base, run);
|
|
10642
|
+
const graph = loadGraph(root3, opts.graphPath);
|
|
10643
|
+
if (!graph) {
|
|
10644
|
+
throw new CliError(
|
|
10645
|
+
"no code map found \u2014 run `vg` in this repository first, then `vg review`" + (opts.graphPath ? ` (looked at ${opts.graphPath})` : ""),
|
|
10646
|
+
ExitCode.ENGINE_UNAVAILABLE
|
|
10647
|
+
);
|
|
10648
|
+
}
|
|
10649
|
+
const recencyDays = fileRecencyDays(repoRoot, {}, run);
|
|
10650
|
+
const intent = readDeclaredIntent(repoRoot);
|
|
10651
|
+
const changedPathSet = new Set(change.files.map((f) => f.path.replace(/\\/g, "/")));
|
|
10652
|
+
const fileText = /* @__PURE__ */ new Map();
|
|
10653
|
+
for (const file of change.files) {
|
|
10654
|
+
if (file.op === "removed") continue;
|
|
10655
|
+
const abs = path51.join(repoRoot, file.path);
|
|
10656
|
+
try {
|
|
10657
|
+
const stat2 = fs12.statSync(abs);
|
|
10658
|
+
if (stat2.isFile() && stat2.size <= 2 * 1024 * 1024) {
|
|
10659
|
+
fileText.set(file.path.replace(/\\/g, "/"), fs12.readFileSync(abs, "utf8"));
|
|
10660
|
+
}
|
|
10661
|
+
} catch {
|
|
10662
|
+
}
|
|
10663
|
+
}
|
|
10664
|
+
const baseFileText = readBaseFileTexts(change, run);
|
|
10665
|
+
const profile = opts.base ? "ci-wide" : "interactive-narrow";
|
|
10666
|
+
const compiled = compileCapsule({
|
|
10667
|
+
graph,
|
|
10668
|
+
change,
|
|
10669
|
+
config,
|
|
10670
|
+
profile,
|
|
10671
|
+
repoPseudonym: repoPseudonym(change.remote, repoRoot),
|
|
10672
|
+
recencyDays,
|
|
10673
|
+
intent,
|
|
10674
|
+
baseFileText,
|
|
10675
|
+
headFileText: fileText
|
|
10676
|
+
});
|
|
10677
|
+
const capsule = compiled.capsule;
|
|
10678
|
+
const diffArgs = opts.base ? ["diff", "-U3", "-M", `${change.baseSha}..HEAD`] : ["diff", "-U3", "-M", "HEAD"];
|
|
10679
|
+
const removedLines = removedLinesFromDiff(run(diffArgs, repoRoot).stdout);
|
|
10680
|
+
const peerFileText = /* @__PURE__ */ new Map();
|
|
10681
|
+
const ROUTE_LAYERS = /* @__PURE__ */ new Set(["routing", "middleware", "presentation"]);
|
|
10682
|
+
for (const node2 of graph.nodes) {
|
|
10683
|
+
const rel = node2.file.replace(/\\/g, "/");
|
|
10684
|
+
if (peerFileText.has(rel) || fileText.has(rel)) continue;
|
|
10685
|
+
if (peerFileText.size >= 300) break;
|
|
10686
|
+
const cls = classifyFile(rel, "unknown");
|
|
10687
|
+
if (!cls || !ROUTE_LAYERS.has(cls.layer)) continue;
|
|
10688
|
+
try {
|
|
10689
|
+
const abs = path51.join(repoRoot, rel);
|
|
10690
|
+
const stat2 = fs12.statSync(abs);
|
|
10691
|
+
if (stat2.isFile() && stat2.size <= 512 * 1024) peerFileText.set(rel, fs12.readFileSync(abs, "utf8"));
|
|
10692
|
+
} catch {
|
|
10693
|
+
}
|
|
10694
|
+
}
|
|
10695
|
+
const similarity = new SimilarityIndex();
|
|
10696
|
+
const changedBodies = [];
|
|
10697
|
+
const FUNCTION_KINDS = /* @__PURE__ */ new Set(["function", "method"]);
|
|
10698
|
+
const sourceCache = /* @__PURE__ */ new Map();
|
|
10699
|
+
const linesOf = (rel) => {
|
|
10700
|
+
if (sourceCache.has(rel)) return sourceCache.get(rel);
|
|
10701
|
+
let value = null;
|
|
10702
|
+
try {
|
|
10703
|
+
const abs = path51.join(repoRoot, rel);
|
|
10704
|
+
const stat2 = fs12.statSync(abs);
|
|
10705
|
+
if (stat2.isFile() && stat2.size <= 1024 * 1024) value = fs12.readFileSync(abs, "utf8").split("\n");
|
|
10706
|
+
} catch {
|
|
10707
|
+
value = null;
|
|
10708
|
+
}
|
|
10709
|
+
sourceCache.set(rel, value);
|
|
10710
|
+
return value;
|
|
10711
|
+
};
|
|
10712
|
+
for (const node2 of graph.nodes) {
|
|
10713
|
+
if (!FUNCTION_KINDS.has(node2.kind)) continue;
|
|
10714
|
+
const rel = node2.file.replace(/\\/g, "/");
|
|
10715
|
+
if (!isComparable(rel)) continue;
|
|
10716
|
+
const lines = linesOf(rel);
|
|
10717
|
+
if (!lines) continue;
|
|
10718
|
+
const text = lines.slice(node2.span.start - 1, node2.span.end).join("\n");
|
|
10719
|
+
if (!text.trim()) continue;
|
|
10720
|
+
const body = {
|
|
10721
|
+
id: node2.id,
|
|
10722
|
+
name: node2.name,
|
|
10723
|
+
file: rel,
|
|
10724
|
+
startLine: node2.span.start,
|
|
10725
|
+
endLine: node2.span.end,
|
|
10726
|
+
text
|
|
10727
|
+
};
|
|
10728
|
+
if (changedPathSet.has(rel)) changedBodies.push(body);
|
|
10729
|
+
else similarity.add(body);
|
|
10730
|
+
}
|
|
10731
|
+
const scan = runScanners({
|
|
10732
|
+
capsule,
|
|
10733
|
+
config,
|
|
10734
|
+
removedLines,
|
|
10735
|
+
fileText,
|
|
10736
|
+
vulnerablePackages: vulnerablePackagesFromScan(repoRoot),
|
|
10737
|
+
votes: compiled.votes,
|
|
10738
|
+
peerFileText,
|
|
10739
|
+
similarity,
|
|
10740
|
+
changedBodies,
|
|
10741
|
+
dataAccess: compiled.dataAccess,
|
|
10742
|
+
changedPaths: changedPathSet
|
|
10743
|
+
});
|
|
10744
|
+
let findings = {
|
|
10745
|
+
schema_version: FINDINGS_SCHEMA,
|
|
10746
|
+
change_class: changeClassOf(capsule, {
|
|
10747
|
+
architecture: scan.architecture.length,
|
|
10748
|
+
security: scan.security.length
|
|
10749
|
+
}),
|
|
10750
|
+
architecture_findings: scan.architecture,
|
|
10751
|
+
security_findings: scan.security,
|
|
10752
|
+
unknowns: [.../* @__PURE__ */ new Set([...compiled.unknowns, ...scan.unknowns])],
|
|
10753
|
+
required_checks: scan.requiredChecks
|
|
10754
|
+
};
|
|
10755
|
+
let modelId = "none";
|
|
10756
|
+
let quantization = null;
|
|
10757
|
+
if (opts.explain) {
|
|
10758
|
+
const explainImpl = opts.explainImpl ?? (await import('./explain-EXPJTCB5.js')).explainFindings;
|
|
10759
|
+
const explained = await explainImpl(capsule, findings, { offline: opts.offline });
|
|
10760
|
+
findings = explained.findings;
|
|
10761
|
+
modelId = explained.model;
|
|
10762
|
+
quantization = explained.quantization;
|
|
10763
|
+
}
|
|
10764
|
+
const verification = verifyFindings(findings, capsule);
|
|
10765
|
+
const policy = applyReviewPolicy(capsule, findings, config, verification);
|
|
10766
|
+
const createdAt = opts.generatedAt ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
10767
|
+
const capsuleDigest = digest(capsule);
|
|
10768
|
+
const findingsDigest = digest(findings);
|
|
10769
|
+
const evidenceDigest = digest(capsule.evidence);
|
|
10770
|
+
const protectedCount = [...findings.architecture_findings, ...findings.security_findings].filter(
|
|
10771
|
+
(f) => f.protected_finding === true
|
|
10772
|
+
).length;
|
|
10773
|
+
const receipt = {
|
|
10774
|
+
schema_version: RECEIPT_SCHEMA,
|
|
10775
|
+
receipt_id: receiptId(Date.parse(createdAt) || 0, `${capsuleDigest}${findingsDigest}`),
|
|
10776
|
+
created_at: createdAt,
|
|
10777
|
+
workspace_id: opts.workspaceId ?? null,
|
|
10778
|
+
repo: {
|
|
10779
|
+
name: change.remote ? change.remote.split("/").slice(-2).join("/") : path51.basename(repoRoot),
|
|
10780
|
+
remote: change.remote,
|
|
10781
|
+
repo_key: repoKey(change.remote, repoRoot)
|
|
10782
|
+
},
|
|
10783
|
+
git: {
|
|
10784
|
+
base_sha: change.baseSha,
|
|
10785
|
+
head_sha: change.headSha,
|
|
10786
|
+
merge_base: change.mergeBase,
|
|
10787
|
+
ref: change.ref,
|
|
10788
|
+
dirty: change.dirty,
|
|
10789
|
+
dirty_tree_hash: change.dirtyTreeHash
|
|
10790
|
+
},
|
|
10791
|
+
decision: policy.decision,
|
|
10792
|
+
enforcement: config.enforcement,
|
|
10793
|
+
quick_path: policy.quickPath,
|
|
10794
|
+
change_class: findings.change_class,
|
|
10795
|
+
counts: {
|
|
10796
|
+
architecture: findings.architecture_findings.length,
|
|
10797
|
+
security: findings.security_findings.length,
|
|
10798
|
+
protected: protectedCount,
|
|
10799
|
+
unknowns: findings.unknowns.length
|
|
10800
|
+
},
|
|
10801
|
+
findings,
|
|
10802
|
+
versions: {
|
|
10803
|
+
cli: VERSION,
|
|
10804
|
+
graph_schema: graph?.schemaVersion ?? "none",
|
|
10805
|
+
policy: REVIEW_POLICY_VERSION,
|
|
10806
|
+
model: modelId,
|
|
10807
|
+
quantization,
|
|
10808
|
+
capsule_schema: CAPSULE_SCHEMA
|
|
10809
|
+
},
|
|
10810
|
+
digests: {
|
|
10811
|
+
capsule: capsuleDigest,
|
|
10812
|
+
findings: findingsDigest,
|
|
10813
|
+
evidence: evidenceDigest,
|
|
10814
|
+
receipt: ""
|
|
10815
|
+
},
|
|
10816
|
+
verification: {
|
|
10817
|
+
protected_false_bless: policy.protectedFalseBless,
|
|
10818
|
+
schema_valid: verification.schema_valid,
|
|
10819
|
+
evidence_ids_valid: verification.evidence_ids_valid
|
|
10820
|
+
},
|
|
10821
|
+
signature: null
|
|
10822
|
+
};
|
|
10823
|
+
receipt.digests.receipt = receiptDigest(receipt);
|
|
10824
|
+
return {
|
|
10825
|
+
receipt,
|
|
10826
|
+
capsule,
|
|
10827
|
+
config,
|
|
10828
|
+
verification,
|
|
10829
|
+
reasons: policy.reasons,
|
|
10830
|
+
votes: compiled.votes,
|
|
10831
|
+
intent: {
|
|
10832
|
+
target: capsule.patterns.declared_target_pattern,
|
|
10833
|
+
sources: intent.sources
|
|
10834
|
+
},
|
|
10835
|
+
repoRoot,
|
|
10836
|
+
budget: {
|
|
10837
|
+
estimatedTokens: compiled.estimatedTokens,
|
|
10838
|
+
median: compiled.budget.median,
|
|
10839
|
+
cap: compiled.budget.cap,
|
|
10840
|
+
trimmed: compiled.trimmed
|
|
10841
|
+
}
|
|
10842
|
+
};
|
|
10843
|
+
}
|
|
10844
|
+
var CONTEXT_PATH = ".vibgrate/review-context.md";
|
|
10845
|
+
var BLOCK_START = "<!-- vibgrate:review-context:start -->";
|
|
10846
|
+
var BLOCK_END = "<!-- vibgrate:review-context:end -->";
|
|
10847
|
+
var MAX_CONVENTIONS = 8;
|
|
10848
|
+
var MAX_OPEN_FINDINGS = 10;
|
|
10849
|
+
function renderContext(input) {
|
|
10850
|
+
const { receipt, votes, declaredTarget } = input;
|
|
10851
|
+
const findings = [...receipt.findings.architecture_findings, ...receipt.findings.security_findings];
|
|
10852
|
+
const lines = [];
|
|
10853
|
+
lines.push("# Review context");
|
|
10854
|
+
lines.push("");
|
|
10855
|
+
lines.push(
|
|
10856
|
+
"Generated by `vg review --write-context`. It records what Vibgrate Review knows about this repository so an agent does not have to rediscover it. Commit it."
|
|
10857
|
+
);
|
|
10858
|
+
lines.push("");
|
|
10859
|
+
lines.push("## The architecture this repository is aiming at");
|
|
10860
|
+
lines.push("");
|
|
10861
|
+
if (declaredTarget) {
|
|
10862
|
+
lines.push(`**Declared target: \`${declaredTarget}\`.**`);
|
|
10863
|
+
if (input.intentSources.length > 0) {
|
|
10864
|
+
lines.push("");
|
|
10865
|
+
lines.push(`Declared in ${input.intentSources.map((s) => `\`${s}\``).join(", ")}.`);
|
|
10866
|
+
}
|
|
10867
|
+
lines.push("");
|
|
10868
|
+
lines.push(
|
|
10869
|
+
"Write new code to match this target, **not** to match whatever most of the existing code does. Where the two disagree, the target wins and the existing code is the thing being migrated."
|
|
10870
|
+
);
|
|
10871
|
+
} else {
|
|
10872
|
+
lines.push("**No target architecture has been declared.**");
|
|
10873
|
+
lines.push("");
|
|
10874
|
+
lines.push(
|
|
10875
|
+
"The conventions below are what the code currently does. They are an observation, not an instruction \u2014 they may be exactly what the team is trying to move away from. Set `target_pattern` in `.vibgrate/review.toml` to make the intended architecture explicit, and Review will hold changes to it."
|
|
10876
|
+
);
|
|
10877
|
+
}
|
|
10878
|
+
lines.push("");
|
|
10879
|
+
const decided = votes.filter((v) => v.reason === "dominant" && v.dominant).slice(0, MAX_CONVENTIONS);
|
|
10880
|
+
lines.push("## What the existing code does");
|
|
10881
|
+
lines.push("");
|
|
10882
|
+
if (decided.length === 0) {
|
|
10883
|
+
lines.push("No peer group was consistent enough to establish a convention.");
|
|
10884
|
+
} else {
|
|
10885
|
+
lines.push("| Peer group | Convention | Share | Follow the example in |");
|
|
10886
|
+
lines.push("|---|---|---:|---|");
|
|
10887
|
+
for (const vote of decided) {
|
|
10888
|
+
lines.push(
|
|
10889
|
+
`| \`${vote.group}\` | ${describePattern(vote.dominant)} | ${(vote.share * 100).toFixed(0)}% | ${vote.exemplars.slice(0, 2).map((e) => `\`${e}\``).join(", ") || "\u2014"} |`
|
|
10890
|
+
);
|
|
10891
|
+
}
|
|
10892
|
+
}
|
|
10893
|
+
const noConvention = votes.filter((v) => v.reason === "no_convention");
|
|
10894
|
+
if (noConvention.length > 0) {
|
|
10895
|
+
lines.push("");
|
|
10896
|
+
lines.push(
|
|
10897
|
+
`${noConvention.length} peer group(s) have **no** convention \u2014 the patterns are too evenly split for one to be normal. Pick the one that matches the declared target rather than copying the nearest file.`
|
|
10898
|
+
);
|
|
10899
|
+
}
|
|
10900
|
+
lines.push("");
|
|
10901
|
+
lines.push("## Open findings on the last reviewed change");
|
|
10902
|
+
lines.push("");
|
|
10903
|
+
lines.push(
|
|
10904
|
+
`Decision: \`${receipt.decision}\` \xB7 ${receipt.counts.architecture} architecture \xB7 ${receipt.counts.security} security \xB7 ${receipt.counts.protected} protected \xB7 at \`${receipt.git.head_sha.slice(0, 8)}\`.`
|
|
10905
|
+
);
|
|
10906
|
+
lines.push("");
|
|
10907
|
+
if (findings.length === 0) {
|
|
10908
|
+
lines.push("None.");
|
|
10909
|
+
} else {
|
|
10910
|
+
for (const f of findings.slice(0, MAX_OPEN_FINDINGS)) {
|
|
10911
|
+
const badge = f.protected_finding ? " **(protected \u2014 cannot be waived)**" : "";
|
|
10912
|
+
lines.push(`- \`${f.kind}\`${badge} \u2014 ${f.claim} \u2192 ${f.remediation}`);
|
|
10913
|
+
}
|
|
10914
|
+
if (findings.length > MAX_OPEN_FINDINGS) {
|
|
10915
|
+
lines.push(`- \u2026and ${findings.length - MAX_OPEN_FINDINGS} more (\`vg review\`).`);
|
|
10916
|
+
}
|
|
10917
|
+
}
|
|
10918
|
+
lines.push("");
|
|
10919
|
+
lines.push("## If you are an AI agent working in this repository");
|
|
10920
|
+
lines.push("");
|
|
10921
|
+
lines.push(
|
|
10922
|
+
declaredTarget ? `1. Match the declared \`${declaredTarget}\` architecture above, even where existing code does not.` : "1. There is no declared architecture. Match the nearest convention above, and say so in your PR description."
|
|
10923
|
+
);
|
|
10924
|
+
lines.push("2. Before writing a new function, check whether one already does the job \u2014 `vg review` reports re-implementations.");
|
|
10925
|
+
lines.push("3. A new mutating route needs an authorization guard unless the surrounding routes are deliberately public.");
|
|
10926
|
+
lines.push("4. Run `vg review` before you finish. A `protected` finding cannot be waived \u2014 it must be fixed.");
|
|
10927
|
+
lines.push("");
|
|
10928
|
+
lines.push(
|
|
10929
|
+
"<sub>Review reports change integrity. It does not prove code is secure, and absence of findings is not a certification.</sub>"
|
|
10930
|
+
);
|
|
10931
|
+
return lines.join("\n");
|
|
10932
|
+
}
|
|
10933
|
+
function writeContextFile(root3, input) {
|
|
10934
|
+
const target = path51.join(root3, CONTEXT_PATH);
|
|
10935
|
+
fs12.mkdirSync(path51.dirname(target), { recursive: true });
|
|
10936
|
+
fs12.writeFileSync(target, `${renderContext(input)}
|
|
10937
|
+
`, "utf8");
|
|
10938
|
+
return CONTEXT_PATH;
|
|
10939
|
+
}
|
|
10940
|
+
function injectContextBlock(existing, body) {
|
|
10941
|
+
const block = `${BLOCK_START}
|
|
10942
|
+
${body}
|
|
10943
|
+
${BLOCK_END}`;
|
|
10944
|
+
const startAt = existing.indexOf(BLOCK_START);
|
|
10945
|
+
const endAt = existing.indexOf(BLOCK_END);
|
|
10946
|
+
if (startAt !== -1 && endAt !== -1 && endAt > startAt) {
|
|
10947
|
+
return existing.slice(0, startAt) + block + existing.slice(endAt + BLOCK_END.length);
|
|
10948
|
+
}
|
|
10949
|
+
const separator = existing.endsWith("\n") ? "\n" : "\n\n";
|
|
10950
|
+
return `${existing}${separator}${block}
|
|
10951
|
+
`;
|
|
10952
|
+
}
|
|
10953
|
+
var PREPARE_LOCK_STALE_MS = 10 * 60 * 1e3;
|
|
10954
|
+
async function ensureCodeMap(opts) {
|
|
10955
|
+
const { root: root3, graphPath } = opts;
|
|
10956
|
+
const haveMap = loadGraph(root3, graphPath) !== null;
|
|
10957
|
+
if (opts.autoBuild === false) {
|
|
10958
|
+
return { action: haveMap ? "up-to-date" : "skipped", reason: haveMap ? void 0 : "auto-build disabled" };
|
|
10959
|
+
}
|
|
10960
|
+
if (graphPath) {
|
|
10961
|
+
return { action: haveMap ? "up-to-date" : "skipped", reason: haveMap ? void 0 : "an explicit --graph path was given" };
|
|
10962
|
+
}
|
|
10963
|
+
const bar = opts.quiet ? void 0 : new ProgressBar("mapping the repository");
|
|
10964
|
+
const onParseProgress = (done, total) => {
|
|
10965
|
+
opts.onProgress?.(done, total);
|
|
10966
|
+
bar?.update(done, total);
|
|
10967
|
+
};
|
|
10968
|
+
try {
|
|
10969
|
+
if (haveMap) {
|
|
10970
|
+
const outcome = await refreshIfStale(root3, { onParseProgress });
|
|
10971
|
+
if (outcome.status === "refreshed") {
|
|
10972
|
+
return { action: "refreshed", files: outcome.totalFiles, ms: outcome.ms };
|
|
10973
|
+
}
|
|
10974
|
+
if (outcome.status === "error") {
|
|
10975
|
+
return { action: "skipped", reason: outcome.message };
|
|
10976
|
+
}
|
|
10977
|
+
return { action: "up-to-date" };
|
|
10978
|
+
}
|
|
10979
|
+
return await firstBuild(root3, onParseProgress);
|
|
10980
|
+
} finally {
|
|
10981
|
+
bar?.done();
|
|
10982
|
+
}
|
|
10983
|
+
}
|
|
10984
|
+
async function firstBuild(root3, onParseProgress) {
|
|
10985
|
+
const lock = path51.join(cacheDir(root3), "refresh.lock");
|
|
10986
|
+
if (!acquireLock(lock, PREPARE_LOCK_STALE_MS)) {
|
|
10987
|
+
return { action: "skipped", reason: "another vg process is building the map" };
|
|
10988
|
+
}
|
|
10989
|
+
const start = Date.now();
|
|
10990
|
+
try {
|
|
10991
|
+
const exclude = mergeExcludes(root3, void 0);
|
|
10992
|
+
const result = await buildGraph({ root: root3, exclude, onParseProgress });
|
|
10993
|
+
writeArtifacts(result.graph, { root: root3, html: false, report: false });
|
|
10994
|
+
writeSnapshot(root3, result.graph.provenance.corpusHash, result.fileStats, { exclude });
|
|
10995
|
+
return { action: "built", files: result.totalFiles, ms: Date.now() - start };
|
|
10996
|
+
} catch (err) {
|
|
10997
|
+
return { action: "skipped", reason: err.message };
|
|
10998
|
+
} finally {
|
|
10999
|
+
releaseLock(lock);
|
|
11000
|
+
}
|
|
11001
|
+
}
|
|
11002
|
+
function reviewPolicyState(root3, base, run) {
|
|
11003
|
+
const inRef = (ref) => {
|
|
11004
|
+
const res = run(["show", `${ref}:${REVIEW_CONFIG_PATH}`], root3);
|
|
11005
|
+
return res.status === 0 && res.stdout.trim().length > 0;
|
|
11006
|
+
};
|
|
11007
|
+
if (inRef("HEAD")) return { present: true, where: "head" };
|
|
11008
|
+
if (fs12.existsSync(path51.join(root3, REVIEW_CONFIG_PATH))) {
|
|
11009
|
+
return { present: true, where: "working-tree" };
|
|
11010
|
+
}
|
|
11011
|
+
return { present: false, where: null };
|
|
11012
|
+
}
|
|
11013
|
+
function seedReviewPolicy(opts) {
|
|
11014
|
+
const target = path51.join(opts.root, REVIEW_CONFIG_PATH);
|
|
11015
|
+
const pattern = opts.observedPattern ?? null;
|
|
11016
|
+
if (fs12.existsSync(target)) return { written: false, path: REVIEW_CONFIG_PATH, targetPattern: pattern };
|
|
11017
|
+
fs12.mkdirSync(path51.dirname(target), { recursive: true });
|
|
11018
|
+
fs12.writeFileSync(target, renderReviewPolicy(pattern), "utf8");
|
|
11019
|
+
return { written: true, path: REVIEW_CONFIG_PATH, targetPattern: pattern };
|
|
11020
|
+
}
|
|
11021
|
+
function renderReviewPolicy(observedPattern) {
|
|
11022
|
+
const lines = [
|
|
11023
|
+
"# Vibgrate Review policy \u2014 written by `vg review` on its first run.",
|
|
11024
|
+
"# Commit this file: Review reads it from the *base branch*, so a pull",
|
|
11025
|
+
"# request cannot weaken the policy that judges it.",
|
|
11026
|
+
"",
|
|
11027
|
+
"[review]",
|
|
11028
|
+
'# "advisory" reports without ever gating. Switch to "enforced" when you',
|
|
11029
|
+
"# want `fail_on` to decide the exit code in CI.",
|
|
11030
|
+
'enforcement = "advisory"',
|
|
11031
|
+
'fail_on = "fail"',
|
|
11032
|
+
""
|
|
11033
|
+
];
|
|
11034
|
+
if (observedPattern) {
|
|
11035
|
+
lines.push(
|
|
11036
|
+
`# Derived from the layering this repository already exhibits. Change it`,
|
|
11037
|
+
`# to the shape you want \u2014 Review judges changes against this, not against`,
|
|
11038
|
+
`# the majority.`,
|
|
11039
|
+
`target_pattern = "${observedPattern}"`
|
|
11040
|
+
);
|
|
11041
|
+
} else {
|
|
11042
|
+
lines.push(
|
|
11043
|
+
"# No single layering shape dominates this repository yet, so nothing is",
|
|
11044
|
+
'# declared. Set one (e.g. "clean", "layered", "hexagonal") to have Review',
|
|
11045
|
+
"# judge layer traversals instead of reporting them as unknown.",
|
|
11046
|
+
'# target_pattern = "clean"'
|
|
11047
|
+
);
|
|
11048
|
+
}
|
|
11049
|
+
lines.push(
|
|
11050
|
+
"",
|
|
11051
|
+
"# Layer pairs an author may traverse without it counting as a regression.",
|
|
11052
|
+
"approved_exceptions = []",
|
|
11053
|
+
"",
|
|
11054
|
+
"# Protected findings can never be blessed into a pass.",
|
|
11055
|
+
"[review.protected]",
|
|
11056
|
+
"unguarded_entrypoint = true",
|
|
11057
|
+
"known_vulnerable_dependency = true",
|
|
11058
|
+
"validated_taint = true",
|
|
11059
|
+
""
|
|
11060
|
+
);
|
|
11061
|
+
return `${lines.join("\n")}`;
|
|
11062
|
+
}
|
|
11063
|
+
|
|
11064
|
+
// src/commands/review.ts
|
|
11065
|
+
var FORMATS = ["text", "json", "sarif", "md"];
|
|
11066
|
+
function registerReview(program) {
|
|
11067
|
+
const cmd = program.command("review").description("Vibgrate Review \u2014 architecture + security-control review of the current change, locally").option("--base <ref>", "review HEAD against the merge-base with <ref> (e.g. origin/main)").option("--format <fmt>", `output format (${FORMATS.join(" | ")})`, "text").option("-o, --out <file>", "write the formatted result to a file").option("--push", "send the receipt to Vibgrate Cloud (needs a DSN)").option(
|
|
11068
|
+
"--fail-on <level>",
|
|
11069
|
+
'gate CI on this decision level (none | fail | needs_review). Omit to use .vibgrate/review.toml; gating is off unless enforcement = "enforced"'
|
|
11070
|
+
).option("--explain", "add local-model explanations (requires a local model; fails closed without one)").option("--dsn <dsn>", "DSN token for --push (or use VIBGRATE_DSN / `vg login`)").option("--strict", "fail the command when --push fails").option("--include-spans", "include evidence line ranges in the pushed receipt").option("--include-snippets", "include capped source snippets in the pushed receipt (explicit opt-in)").option("--no-auto-build", "do not build or refresh the code map \u2014 fail with exit 6 when it is missing or stale").option("--no-setup", "do not write a starter .vibgrate/review.toml when the repository has no review policy").option("--write-context", "write .vibgrate/review-context.md \u2014 committed agent memory").option(
|
|
11071
|
+
"--inject-context [file]",
|
|
11072
|
+
"update the managed review block inside an instruction file (default: CLAUDE.md)"
|
|
11073
|
+
).action(async function(opts) {
|
|
11074
|
+
const global = readGlobal(this);
|
|
11075
|
+
const root3 = rootOf(global);
|
|
11076
|
+
if (!FORMATS.includes(opts.format)) {
|
|
11077
|
+
throw new CliError(`unknown --format "${opts.format}" (expected ${FORMATS.join(", ")})`, ExitCode.USAGE_ERROR);
|
|
11078
|
+
}
|
|
11079
|
+
const prepared = await ensureCodeMap({
|
|
11080
|
+
root: root3,
|
|
11081
|
+
graphPath: global.graph,
|
|
11082
|
+
autoBuild: opts.autoBuild,
|
|
11083
|
+
quiet: Boolean(global.quiet) || Boolean(global.json)
|
|
11084
|
+
});
|
|
11085
|
+
reportPrepare(prepared, Boolean(global.quiet) || Boolean(global.json));
|
|
11086
|
+
const result = await runReview({
|
|
11087
|
+
root: root3,
|
|
11088
|
+
base: opts.base,
|
|
11089
|
+
explain: opts.explain,
|
|
11090
|
+
offline: global.offline,
|
|
11091
|
+
graphPath: global.graph,
|
|
11092
|
+
generatedAt: global.generatedAt
|
|
11093
|
+
});
|
|
11094
|
+
if (opts.failOn && !FAIL_ON_LEVELS.includes(opts.failOn)) {
|
|
11095
|
+
throw new CliError(
|
|
11096
|
+
`unknown --fail-on "${opts.failOn}" (expected ${FAIL_ON_LEVELS.join(", ")})`,
|
|
11097
|
+
ExitCode.USAGE_ERROR
|
|
11098
|
+
);
|
|
11099
|
+
}
|
|
11100
|
+
const failOn = resolveFailOn(opts.failOn, result.config);
|
|
11101
|
+
const format = global.json ? "json" : opts.format;
|
|
11102
|
+
const rendered = render(result, format);
|
|
11103
|
+
if (opts.out) {
|
|
11104
|
+
const target = path51.resolve(root3, opts.out);
|
|
11105
|
+
fs12.mkdirSync(path51.dirname(target), { recursive: true });
|
|
11106
|
+
fs12.writeFileSync(target, `${rendered}
|
|
11107
|
+
`, "utf8");
|
|
11108
|
+
if (!global.quiet) {
|
|
11109
|
+
info(formatText(result));
|
|
11110
|
+
info(c.dim(` ${format} written to ${opts.out}`));
|
|
11111
|
+
}
|
|
11112
|
+
} else if (format === "text") {
|
|
11113
|
+
if (!global.quiet) info(rendered);
|
|
11114
|
+
} else {
|
|
11115
|
+
out(rendered);
|
|
11116
|
+
}
|
|
11117
|
+
maybeSeedPolicy(result, opts, Boolean(global.quiet) || Boolean(global.json));
|
|
11118
|
+
if (opts.writeContext || opts.injectContext) writeAgentContext(result, opts, Boolean(global.quiet));
|
|
11119
|
+
if (opts.push) {
|
|
11120
|
+
rejectPushWhenOffline(global.offline);
|
|
11121
|
+
await doPush(root3, result, opts, Boolean(global.quiet));
|
|
11122
|
+
}
|
|
11123
|
+
const code = exitCodeForDecision(result.receipt.decision, failOn);
|
|
11124
|
+
if (code !== 0) {
|
|
11125
|
+
info(c.red(`
|
|
11126
|
+
Failing: review decision is \`${result.receipt.decision}\` and the gate is \`${failOn}\`.`));
|
|
11127
|
+
process.exitCode = code;
|
|
11128
|
+
}
|
|
11129
|
+
});
|
|
11130
|
+
applyGlobalOptions(cmd);
|
|
11131
|
+
const explain = cmd.command("explain").description("show the evidence behind one finding from the current change").argument("<finding-id>", "a finding id from the last `vg review` run (e.g. arch-01)").option("--base <ref>", "review HEAD against the merge-base with <ref>").action(async function(findingId, opts) {
|
|
11132
|
+
const global = readGlobal(this);
|
|
11133
|
+
const explainRoot = rootOf(global);
|
|
11134
|
+
reportPrepare(
|
|
11135
|
+
await ensureCodeMap({
|
|
11136
|
+
root: explainRoot,
|
|
11137
|
+
graphPath: global.graph,
|
|
11138
|
+
quiet: Boolean(global.quiet) || Boolean(global.json)
|
|
11139
|
+
}),
|
|
11140
|
+
Boolean(global.quiet) || Boolean(global.json)
|
|
11141
|
+
);
|
|
11142
|
+
const result = await runReview({
|
|
11143
|
+
root: explainRoot,
|
|
11144
|
+
base: opts.base,
|
|
11145
|
+
offline: global.offline,
|
|
11146
|
+
graphPath: global.graph,
|
|
11147
|
+
generatedAt: global.generatedAt
|
|
11148
|
+
});
|
|
11149
|
+
const text = formatExplain(result, findingId);
|
|
11150
|
+
if (!text) {
|
|
11151
|
+
throw new CliError(
|
|
11152
|
+
`no finding "${findingId}" in this change set \u2014 run \`vg review\` to list the current findings`,
|
|
11153
|
+
ExitCode.NOT_FOUND
|
|
11154
|
+
);
|
|
11155
|
+
}
|
|
11156
|
+
if (global.json) {
|
|
11157
|
+
const all = [...result.receipt.findings.architecture_findings, ...result.receipt.findings.security_findings];
|
|
11158
|
+
const finding = all.find((f) => f.id === findingId);
|
|
11159
|
+
out(
|
|
11160
|
+
JSON.stringify(
|
|
11161
|
+
{
|
|
11162
|
+
finding,
|
|
11163
|
+
evidence: result.capsule.evidence.filter((e) => finding.evidence_ids.includes(e.id))
|
|
11164
|
+
},
|
|
11165
|
+
null,
|
|
11166
|
+
2
|
|
11167
|
+
)
|
|
11168
|
+
);
|
|
11169
|
+
return;
|
|
11170
|
+
}
|
|
11171
|
+
info(text);
|
|
11172
|
+
});
|
|
11173
|
+
applyGlobalOptions(explain);
|
|
11174
|
+
}
|
|
11175
|
+
function reportPrepare(prepared, quiet) {
|
|
11176
|
+
if (quiet) return;
|
|
11177
|
+
const secs = prepared.ms === void 0 ? "" : ` in ${(prepared.ms / 1e3).toFixed(1)}s`;
|
|
11178
|
+
if (prepared.action === "built") {
|
|
11179
|
+
info(c.dim(` code map built \u2014 ${prepared.files} files${secs}`));
|
|
11180
|
+
} else if (prepared.action === "refreshed") {
|
|
11181
|
+
info(c.dim(` code map refreshed \u2014 ${prepared.files} files${secs}`));
|
|
11182
|
+
} else if (prepared.action === "skipped" && prepared.reason) {
|
|
11183
|
+
info(c.yellow(` code map not built: ${prepared.reason}`));
|
|
11184
|
+
}
|
|
11185
|
+
}
|
|
11186
|
+
function maybeSeedPolicy(result, opts, quiet) {
|
|
11187
|
+
if (opts.setup === false || opts.base) return;
|
|
11188
|
+
if (result.config.source !== "defaults") return;
|
|
11189
|
+
if (reviewPolicyState(result.repoRoot, void 0, defaultRun).present) return;
|
|
11190
|
+
const seeded = seedReviewPolicy({
|
|
11191
|
+
root: result.repoRoot,
|
|
11192
|
+
observedPattern: result.capsule.patterns.observed_dominant_pattern
|
|
11193
|
+
});
|
|
11194
|
+
if (!seeded.written || quiet) return;
|
|
11195
|
+
info(
|
|
11196
|
+
c.green(`
|
|
11197
|
+
Review baseline ready \u2014 wrote ${seeded.path}`) + c.dim(
|
|
11198
|
+
seeded.targetPattern ? `
|
|
11199
|
+
target_pattern = "${seeded.targetPattern}" (derived from this repository). Edit it, commit it,
|
|
11200
|
+
and future changes are judged against a declared architecture rather than a guess.` : "\n No layering shape dominates yet, so target_pattern is left commented out.\n Set one and commit the file to have future changes judged against it."
|
|
11201
|
+
)
|
|
11202
|
+
);
|
|
11203
|
+
}
|
|
11204
|
+
function render(result, format) {
|
|
11205
|
+
if (format === "json") return JSON.stringify(result.receipt, null, 2);
|
|
11206
|
+
if (format === "sarif") return formatSarif(result.receipt);
|
|
11207
|
+
if (format === "md") return formatMarkdown(result);
|
|
11208
|
+
return formatText(result);
|
|
11209
|
+
}
|
|
11210
|
+
async function doPush(root3, result, opts, quiet) {
|
|
11211
|
+
const dsn = resolveDsn(opts.dsn);
|
|
11212
|
+
if (!dsn) {
|
|
11213
|
+
const message = "no DSN for --push \u2014 run `vg login`, set VIBGRATE_DSN, or pass --dsn";
|
|
11214
|
+
if (opts.strict) throw new CliError(message, ExitCode.USAGE_ERROR);
|
|
11215
|
+
info(c.yellow(` ${message}`));
|
|
11216
|
+
return;
|
|
11217
|
+
}
|
|
11218
|
+
const parsed = parseDsn$1(dsn);
|
|
11219
|
+
if (!parsed) {
|
|
11220
|
+
const message = "invalid DSN format (expected vibgrate+https://<key>:<secret>@<host>/<workspace>)";
|
|
11221
|
+
if (opts.strict) throw new CliError(message, ExitCode.USAGE_ERROR);
|
|
11222
|
+
info(c.yellow(` ${message}`));
|
|
11223
|
+
return;
|
|
11224
|
+
}
|
|
11225
|
+
const envelope = buildEnvelope(result.receipt, {
|
|
11226
|
+
workspaceId: parsed.workspaceId,
|
|
11227
|
+
pushedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
11228
|
+
});
|
|
11229
|
+
const body = { ...envelope };
|
|
11230
|
+
const spans = collectSpans(root3, result.receipt, result.capsule, {
|
|
11231
|
+
includeSpans: opts.includeSpans,
|
|
11232
|
+
includeSnippets: opts.includeSnippets
|
|
11233
|
+
});
|
|
11234
|
+
if (spans) body.spans = spans;
|
|
11235
|
+
const res = await pushReceipt(parsed, body);
|
|
11236
|
+
if (!res.ok) {
|
|
11237
|
+
const message = `push failed (${res.status}) \u2014 ${res.detail ?? ""}`;
|
|
11238
|
+
if (opts.strict) throw new CliError(message, ExitCode.ERROR);
|
|
11239
|
+
info(c.yellow(` ${message}`));
|
|
11240
|
+
return;
|
|
11241
|
+
}
|
|
11242
|
+
if (!quiet) info(c.green("\u2714") + ` receipt pushed to ${res.host}`);
|
|
11243
|
+
}
|
|
11244
|
+
function writeAgentContext(result, opts, quiet) {
|
|
11245
|
+
const input = {
|
|
11246
|
+
receipt: result.receipt,
|
|
11247
|
+
votes: result.votes,
|
|
11248
|
+
declaredTarget: result.intent.target,
|
|
11249
|
+
intentSources: result.intent.sources
|
|
11250
|
+
};
|
|
11251
|
+
if (opts.writeContext) {
|
|
11252
|
+
const written = writeContextFile(result.repoRoot, input);
|
|
11253
|
+
if (!quiet) info(c.dim(` agent context written to ${written}`));
|
|
11254
|
+
}
|
|
11255
|
+
if (opts.injectContext) {
|
|
11256
|
+
const target = typeof opts.injectContext === "string" ? opts.injectContext : "CLAUDE.md";
|
|
11257
|
+
const abs = path51.resolve(result.repoRoot, target);
|
|
11258
|
+
const existing = fs12.existsSync(abs) ? fs12.readFileSync(abs, "utf8") : "";
|
|
11259
|
+
fs12.writeFileSync(abs, injectContextBlock(existing, renderContext(input)), "utf8");
|
|
11260
|
+
if (!quiet) info(c.dim(` managed review block updated in ${target}`));
|
|
11261
|
+
}
|
|
11262
|
+
}
|
|
9265
11263
|
var initCommand = new Command("init").description("Initialize vibgrate in a project").argument("[path]", "Path to initialize", ".").option("--baseline", "Create initial baseline after init").option("--yes", "Skip confirmation prompts").action(async (targetPath, opts) => {
|
|
9266
|
-
const rootDir =
|
|
9267
|
-
const vibgrateDir2 =
|
|
11264
|
+
const rootDir = path51.resolve(targetPath);
|
|
11265
|
+
const vibgrateDir2 = path51.join(rootDir, ".vibgrate");
|
|
9268
11266
|
await ensureDir(vibgrateDir2);
|
|
9269
11267
|
console.log(chalk7.green("\u2714") + ` Created ${chalk7.bold(".vibgrate/")} directory`);
|
|
9270
|
-
const configPath =
|
|
11268
|
+
const configPath = path51.join(rootDir, "vibgrate.config.ts");
|
|
9271
11269
|
if (await pathExists(configPath)) {
|
|
9272
11270
|
console.log(chalk7.dim(" vibgrate.config.ts already exists, skipping"));
|
|
9273
11271
|
} else {
|
|
@@ -9275,7 +11273,7 @@ var initCommand = new Command("init").description("Initialize vibgrate in a proj
|
|
|
9275
11273
|
console.log(chalk7.green("\u2714") + ` Created ${chalk7.bold("vibgrate.config.ts")}`);
|
|
9276
11274
|
}
|
|
9277
11275
|
if (opts.baseline) {
|
|
9278
|
-
const { runBaseline } = await import('./baseline-
|
|
11276
|
+
const { runBaseline } = await import('./baseline-YL2IZZ4Y.js');
|
|
9279
11277
|
await runBaseline(rootDir);
|
|
9280
11278
|
}
|
|
9281
11279
|
console.log("");
|
|
@@ -9378,7 +11376,7 @@ function collectPackageImports(graph, ecosystem, pkg) {
|
|
|
9378
11376
|
};
|
|
9379
11377
|
}
|
|
9380
11378
|
async function firstReferenceLine(rootDir, relFile, identifier, readFile3) {
|
|
9381
|
-
const content = await readFile3(
|
|
11379
|
+
const content = await readFile3(path51.join(rootDir, relFile));
|
|
9382
11380
|
if (!content) return null;
|
|
9383
11381
|
const escaped = identifier.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
9384
11382
|
const re = new RegExp(`\\b${escaped}\\b`);
|
|
@@ -9416,9 +11414,9 @@ function sitesFromImportFiles(files, limit = 10) {
|
|
|
9416
11414
|
}
|
|
9417
11415
|
async function defaultReadFile(absPath) {
|
|
9418
11416
|
try {
|
|
9419
|
-
const stat2 = await
|
|
11417
|
+
const stat2 = await fs32.stat(absPath);
|
|
9420
11418
|
if (!stat2.isFile() || stat2.size > MAX_FILE_BYTES) return null;
|
|
9421
|
-
return await
|
|
11419
|
+
return await fs32.readFile(absPath, "utf8");
|
|
9422
11420
|
} catch {
|
|
9423
11421
|
return null;
|
|
9424
11422
|
}
|
|
@@ -9614,7 +11612,7 @@ var AI_ASSISTANT_DETECTORS = [
|
|
|
9614
11612
|
async function detectAiAssistant(rootDir) {
|
|
9615
11613
|
for (const detector of AI_ASSISTANT_DETECTORS) {
|
|
9616
11614
|
for (const file of detector.files) {
|
|
9617
|
-
if (await pathExists$1(
|
|
11615
|
+
if (await pathExists$1(path51.join(rootDir, file))) {
|
|
9618
11616
|
return detector.name;
|
|
9619
11617
|
}
|
|
9620
11618
|
}
|
|
@@ -9808,7 +11806,7 @@ function reportStandards(rootDir) {
|
|
|
9808
11806
|
}
|
|
9809
11807
|
async function printRuntimeSnapshot(rootDir, opts, already) {
|
|
9810
11808
|
try {
|
|
9811
|
-
const { attachVgd: attachVgd2 } = await import('./attach-
|
|
11809
|
+
const { attachVgd: attachVgd2 } = await import('./attach-JUSALQ75.js');
|
|
9812
11810
|
const { vgdRequest } = await import('./client-P5XSRFV4.js');
|
|
9813
11811
|
const { formatSemanticProgress } = await import('./semantic-status-2JFLIQQJ.js');
|
|
9814
11812
|
let repositoryId = already?.repositoryId;
|
|
@@ -9850,7 +11848,7 @@ var scanCommand = new Command("scan").description("Scan a project for upgrade dr
|
|
|
9850
11848
|
collectExcludes,
|
|
9851
11849
|
[]
|
|
9852
11850
|
).option("--concurrency <n>", "Max concurrent registry lookups", "8").option("--push", "Auto-push results to Vibgrate API after scan").option("--dsn <dsn>", "DSN token for push (or use VIBGRATE_DSN env)").option("--region <region>", "Override data residency region for push (us, eu)").option("--strict", "Fail on push errors").option("--ui-purpose", "Enable optional UI purpose evidence extraction (slower)").option("--no-local-artifacts", "Do not write .vibgrate JSON artifacts to disk").option("--max-privacy", "Enable strongest privacy mode (minimal scanners, no local artifacts)").option("--offline", "Run without network calls; do not upload results").option("--full", "Comprehensive scan: turns on known-vulnerability detection (= --vulns) and, when a standards policy exists, a banned-dependency report \u2014 on top of drift scoring and the code map").option("--vulns", "Also scan installed dependencies for known vulnerabilities (OSV online, or advisories from --package-manifest when offline)").option("--package-manifest <file>", "Use local package-version manifest JSON/ZIP (for offline mode)").option("--project-scan-timeout <seconds>", "Per-project scan timeout in seconds (default: 180)").option("--drift-budget <score>", "Fail if DriftScore is above budget (0-100)").option("--drift-worsening <percent>", "Fail if drift worsens by more than % since baseline").option("--repository-name <name>", "Override the repository name recorded for this scan (defaults to the directory or package.json name)").option("--force", "Always create a fresh scan ingest, even if the repository is unchanged since the last scan (skips the unchanged/reuse optimization). Used by scheduled and dashboard-triggered scans.").option("--no-graph", "Skip building the local code map (the AI/docs index) that scan produces after scoring drift").option("--no-daemon", "Never auto-start or use the local runtime (vgd) \u2014 run everything in this process").option("--quiet", "Suppress promotional output (the free-plan tracking panel and the AI Context install prompt); scan results are unaffected").action(async (targetPath, opts) => {
|
|
9853
|
-
const rootDir =
|
|
11851
|
+
const rootDir = path51.resolve(targetPath);
|
|
9854
11852
|
if (!await pathExists$1(rootDir)) {
|
|
9855
11853
|
console.error(chalk7.red(`Path does not exist: ${rootDir}`));
|
|
9856
11854
|
process.exit(1);
|
|
@@ -9858,7 +11856,7 @@ var scanCommand = new Command("scan").description("Scan a project for upgrade dr
|
|
|
9858
11856
|
const hasDsn = !!resolveDsn(opts.dsn);
|
|
9859
11857
|
let willPush = !opts.offline && (opts.push || hasDsn);
|
|
9860
11858
|
const noLocalArtifacts = opts.localArtifacts === false;
|
|
9861
|
-
const isFirstRun = !await pathExists$1(
|
|
11859
|
+
const isFirstRun = !await pathExists$1(path51.join(rootDir, ".vibgrate", "scan_result.json"));
|
|
9862
11860
|
let pinnedRegion;
|
|
9863
11861
|
let planTier;
|
|
9864
11862
|
let upgradeUrl;
|
|
@@ -10017,7 +12015,7 @@ var scanCommand = new Command("scan").description("Scan a project for upgrade dr
|
|
|
10017
12015
|
const advanced = await loadAdvancedScanHook();
|
|
10018
12016
|
const artifact = await runCoreScan(rootDir, scanOpts, advanced);
|
|
10019
12017
|
if (wantGraph && builtGraph) {
|
|
10020
|
-
const { attachVgd: attachVgd2 } = await import('./attach-
|
|
12018
|
+
const { attachVgd: attachVgd2 } = await import('./attach-JUSALQ75.js');
|
|
10021
12019
|
const { ActivityLog: ActivityLog2 } = await import('./activity-MF2GNYAI.js');
|
|
10022
12020
|
const activity = new ActivityLog2();
|
|
10023
12021
|
const attached = await activity.time(
|
|
@@ -10185,11 +12183,11 @@ function scanStaleness(rootDir, artifactPath) {
|
|
|
10185
12183
|
scanned++;
|
|
10186
12184
|
if (scanned > MAX_ENTRIES2) break;
|
|
10187
12185
|
if (e.isDirectory()) {
|
|
10188
|
-
if (!SKIP_DIRS2.has(e.name) && !e.name.startsWith(".")) walk2(
|
|
12186
|
+
if (!SKIP_DIRS2.has(e.name) && !e.name.startsWith(".")) walk2(path51.join(dir, e.name), depth + 1);
|
|
10189
12187
|
continue;
|
|
10190
12188
|
}
|
|
10191
12189
|
if (!WATCHED_FILES.has(e.name) && !WATCHED_EXT.test(e.name)) continue;
|
|
10192
|
-
const abs =
|
|
12190
|
+
const abs = path51.join(dir, e.name);
|
|
10193
12191
|
let mtimeMs;
|
|
10194
12192
|
try {
|
|
10195
12193
|
mtimeMs = fs12.statSync(abs).mtimeMs;
|
|
@@ -10198,7 +12196,7 @@ function scanStaleness(rootDir, artifactPath) {
|
|
|
10198
12196
|
}
|
|
10199
12197
|
if (mtimeMs > newest) {
|
|
10200
12198
|
newest = mtimeMs;
|
|
10201
|
-
newestChanged =
|
|
12199
|
+
newestChanged = path51.relative(rootDir, abs) || e.name;
|
|
10202
12200
|
}
|
|
10203
12201
|
}
|
|
10204
12202
|
};
|
|
@@ -10338,13 +12336,13 @@ function analyzeTree(root3, packages) {
|
|
|
10338
12336
|
if (filesSeen >= MAX_FILES) return;
|
|
10339
12337
|
if (entry.isDirectory()) {
|
|
10340
12338
|
if (SKIP_DIRS3.has(entry.name) || entry.name.startsWith(".")) continue;
|
|
10341
|
-
walk2(
|
|
12339
|
+
walk2(path51.join(dir, entry.name), depth + 1);
|
|
10342
12340
|
continue;
|
|
10343
12341
|
}
|
|
10344
|
-
const fileEco = EXT_ECOSYSTEM[
|
|
12342
|
+
const fileEco = EXT_ECOSYSTEM[path51.extname(entry.name)];
|
|
10345
12343
|
if (!fileEco || fileEco === "unknown") continue;
|
|
10346
12344
|
filesSeen++;
|
|
10347
|
-
const full =
|
|
12345
|
+
const full = path51.join(dir, entry.name);
|
|
10348
12346
|
let content;
|
|
10349
12347
|
try {
|
|
10350
12348
|
if (fs12.statSync(full).size > MAX_FILE_BYTES2) continue;
|
|
@@ -10620,7 +12618,7 @@ function pmCommandFor(ecosystem, pkg, version, pm = "npm", opts = {}) {
|
|
|
10620
12618
|
return null;
|
|
10621
12619
|
}
|
|
10622
12620
|
}
|
|
10623
|
-
function
|
|
12621
|
+
function defaultRun3(cmd, cwd) {
|
|
10624
12622
|
const res = spawnSync(cmd.cmd, cmd.args, { cwd, stdio: "inherit", shell: false });
|
|
10625
12623
|
if (res.error) {
|
|
10626
12624
|
if (res.error.code === "ENOENT") {
|
|
@@ -10633,7 +12631,7 @@ function defaultRun(cmd, cwd) {
|
|
|
10633
12631
|
}
|
|
10634
12632
|
var ROOT_TARGET = [{ dir: ".", isWorkspaceRoot: false }];
|
|
10635
12633
|
function applyPlan(rootDir, upgrades, opts = {}) {
|
|
10636
|
-
const run = opts.run ??
|
|
12634
|
+
const run = opts.run ?? defaultRun3;
|
|
10637
12635
|
const resolveTargets = opts.resolveTargets ?? (() => ROOT_TARGET);
|
|
10638
12636
|
const results = [];
|
|
10639
12637
|
for (const u of upgrades) {
|
|
@@ -10659,7 +12657,7 @@ function applyPlan(rootDir, upgrades, opts = {}) {
|
|
|
10659
12657
|
}
|
|
10660
12658
|
const outcomes = effective.map((t) => {
|
|
10661
12659
|
const c2 = pmCommandFor(u.ecosystem, u.package, u.to, opts.packageManager, { workspaceRoot: t.isWorkspaceRoot });
|
|
10662
|
-
return { t, res: run(c2,
|
|
12660
|
+
return { t, res: run(c2, path51.join(rootDir, t.dir)) };
|
|
10663
12661
|
});
|
|
10664
12662
|
const failures = outcomes.filter((o) => !o.res.ok);
|
|
10665
12663
|
if (failures.length === 0) {
|
|
@@ -10688,8 +12686,8 @@ function applyPlan(rootDir, upgrades, opts = {}) {
|
|
|
10688
12686
|
return results;
|
|
10689
12687
|
}
|
|
10690
12688
|
var REGISTRY_URL = "https://registry.npmjs.org/@vibgrate%2fcli/latest";
|
|
10691
|
-
var CACHE_DIR =
|
|
10692
|
-
var CACHE_FILE =
|
|
12689
|
+
var CACHE_DIR = path51.join(os.homedir(), ".vibgrate");
|
|
12690
|
+
var CACHE_FILE = path51.join(CACHE_DIR, "update-check.json");
|
|
10693
12691
|
async function fetchLatestVersion() {
|
|
10694
12692
|
try {
|
|
10695
12693
|
const controller = new AbortController();
|
|
@@ -10716,8 +12714,8 @@ async function fetchLatestVersion() {
|
|
|
10716
12714
|
}
|
|
10717
12715
|
async function writeCache(data) {
|
|
10718
12716
|
try {
|
|
10719
|
-
await
|
|
10720
|
-
await
|
|
12717
|
+
await fs32.mkdir(CACHE_DIR, { recursive: true });
|
|
12718
|
+
await fs32.writeFile(CACHE_FILE, JSON.stringify(data), "utf-8");
|
|
10721
12719
|
} catch {
|
|
10722
12720
|
}
|
|
10723
12721
|
}
|
|
@@ -10752,8 +12750,8 @@ function buildDeferredUpdateScript(opts) {
|
|
|
10752
12750
|
}
|
|
10753
12751
|
function scheduleDeferredUpdate(command, pid = process.pid, dir = os.tmpdir()) {
|
|
10754
12752
|
const stamp = `${pid}-${process.hrtime.bigint().toString(36)}`;
|
|
10755
|
-
const scriptPath =
|
|
10756
|
-
const logPath =
|
|
12753
|
+
const scriptPath = path51.join(dir, `vg-update-${stamp}.cmd`);
|
|
12754
|
+
const logPath = path51.join(dir, `vg-update-${stamp}.log`);
|
|
10757
12755
|
try {
|
|
10758
12756
|
fs12.writeFileSync(scriptPath, buildDeferredUpdateScript({ pid, command, logPath }), "utf8");
|
|
10759
12757
|
const child = spawn(process.env.ComSpec || "cmd.exe", ["/d", "/s", "/c", scriptPath], {
|
|
@@ -10809,14 +12807,14 @@ async function findGlobalInstall(pkg = PACKAGE_NAME, run = defaultRunner) {
|
|
|
10809
12807
|
pm: "yarn",
|
|
10810
12808
|
root: () => {
|
|
10811
12809
|
const dir = tryRun("yarn global dir");
|
|
10812
|
-
return dir ?
|
|
12810
|
+
return dir ? path51.join(dir, "node_modules") : null;
|
|
10813
12811
|
}
|
|
10814
12812
|
},
|
|
10815
|
-
{ pm: "bun", root: () =>
|
|
12813
|
+
{ pm: "bun", root: () => path51.join(os.homedir(), ".bun", "install", "global", "node_modules") }
|
|
10816
12814
|
];
|
|
10817
12815
|
for (const probe of probes) {
|
|
10818
12816
|
const root3 = probe.root();
|
|
10819
|
-
if (root3 && await pathExists(
|
|
12817
|
+
if (root3 && await pathExists(path51.join(root3, pkg))) return probe.pm;
|
|
10820
12818
|
}
|
|
10821
12819
|
return null;
|
|
10822
12820
|
}
|
|
@@ -10835,15 +12833,15 @@ function getGlobalUpdateCommand(pm, pkg, version) {
|
|
|
10835
12833
|
}
|
|
10836
12834
|
}
|
|
10837
12835
|
async function detectPackageManager(cwd) {
|
|
10838
|
-
if (await pathExists(
|
|
10839
|
-
if (await pathExists(
|
|
10840
|
-
if (await pathExists(
|
|
12836
|
+
if (await pathExists(path51.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
12837
|
+
if (await pathExists(path51.join(cwd, "bun.lockb"))) return "bun";
|
|
12838
|
+
if (await pathExists(path51.join(cwd, "yarn.lock"))) return "yarn";
|
|
10841
12839
|
return "npm";
|
|
10842
12840
|
}
|
|
10843
12841
|
async function detectWorkspaceRoot(cwd) {
|
|
10844
|
-
if (await pathExists(
|
|
12842
|
+
if (await pathExists(path51.join(cwd, "pnpm-workspace.yaml"))) return true;
|
|
10845
12843
|
try {
|
|
10846
|
-
const pkgPath =
|
|
12844
|
+
const pkgPath = path51.join(cwd, "package.json");
|
|
10847
12845
|
const raw = await (await import('fs/promises')).readFile(pkgPath, "utf-8");
|
|
10848
12846
|
const pkg = JSON.parse(raw);
|
|
10849
12847
|
return pkg.workspaces != null;
|
|
@@ -10870,7 +12868,7 @@ function getInstallCommand(pm, pkg, version, isDev, opts = {}) {
|
|
|
10870
12868
|
}
|
|
10871
12869
|
async function getLocalDependencyState(cwd, pkg = PACKAGE_NAME) {
|
|
10872
12870
|
try {
|
|
10873
|
-
const pkgPath =
|
|
12871
|
+
const pkgPath = path51.join(cwd, "package.json");
|
|
10874
12872
|
const raw = await (await import('fs/promises')).readFile(pkgPath, "utf-8");
|
|
10875
12873
|
const parsed = JSON.parse(raw);
|
|
10876
12874
|
if (parsed.devDependencies?.[pkg]) return "dev";
|
|
@@ -10885,18 +12883,18 @@ async function confirmWorkspaceRoot(pm) {
|
|
|
10885
12883
|
async function confirm3(question) {
|
|
10886
12884
|
if (!(process.stdin.isTTY && process.stdout.isTTY)) return false;
|
|
10887
12885
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
10888
|
-
return new Promise((
|
|
12886
|
+
return new Promise((resolve25) => {
|
|
10889
12887
|
rl.question(chalk7.yellow(question), (ans) => {
|
|
10890
12888
|
rl.close();
|
|
10891
12889
|
const t = ans.trim().toLowerCase();
|
|
10892
|
-
|
|
12890
|
+
resolve25(t === "y" || t === "yes");
|
|
10893
12891
|
});
|
|
10894
12892
|
});
|
|
10895
12893
|
}
|
|
10896
12894
|
var CAPTURED_OUTPUT_LIMIT = 64e3;
|
|
10897
12895
|
async function runInstall(cmd, cwd) {
|
|
10898
12896
|
const { spawn: spawn4 } = await import('child_process');
|
|
10899
|
-
return new Promise((
|
|
12897
|
+
return new Promise((resolve25) => {
|
|
10900
12898
|
const child = spawn4(cmd, {
|
|
10901
12899
|
cwd,
|
|
10902
12900
|
shell: true,
|
|
@@ -10915,15 +12913,15 @@ async function runInstall(cmd, cwd) {
|
|
|
10915
12913
|
};
|
|
10916
12914
|
tee(child.stdout, process.stdout);
|
|
10917
12915
|
tee(child.stderr, process.stderr);
|
|
10918
|
-
child.on("error", (err) =>
|
|
12916
|
+
child.on("error", (err) => resolve25({ ok: false, output: `${output}
|
|
10919
12917
|
${String(err)}` }));
|
|
10920
|
-
child.on("close", (code) =>
|
|
12918
|
+
child.on("close", (code) => resolve25({ ok: code === 0, output }));
|
|
10921
12919
|
});
|
|
10922
12920
|
}
|
|
10923
12921
|
async function releaseWindowsFileLocks(cwd, keepVersion) {
|
|
10924
12922
|
reapAndReport(cwd, keepVersion);
|
|
10925
12923
|
try {
|
|
10926
|
-
const { vgdSocketPath } = await import('./vgd-
|
|
12924
|
+
const { vgdSocketPath } = await import('./vgd-3A2TRNP5.js');
|
|
10927
12925
|
const result = await stopVgd(vgdSocketPath());
|
|
10928
12926
|
if (result === "stopped") {
|
|
10929
12927
|
console.log(chalk7.dim("Stopped the vgd daemon so its files can be replaced \u2014 it restarts on the new build after the install."));
|
|
@@ -11054,7 +13052,7 @@ var updateCommand = new Command("update").description("Update vibgrate to the la
|
|
|
11054
13052
|
);
|
|
11055
13053
|
async function rewarmVgdAndReport(cwd) {
|
|
11056
13054
|
try {
|
|
11057
|
-
const { vgdIsRunning, vgdRequest } = await import('./vgd-
|
|
13055
|
+
const { vgdIsRunning, vgdRequest } = await import('./vgd-3A2TRNP5.js');
|
|
11058
13056
|
if (!await vgdIsRunning()) return;
|
|
11059
13057
|
const loaded = await vgdRequest({ op: "load-graph", root: cwd });
|
|
11060
13058
|
if (!loaded.ok || !("stored" in loaded)) return;
|
|
@@ -11165,13 +13163,13 @@ function collectCandidates(artifact) {
|
|
|
11165
13163
|
return out2;
|
|
11166
13164
|
}
|
|
11167
13165
|
var defaultFixScanner = async (rootDir) => {
|
|
11168
|
-
const scratch =
|
|
13166
|
+
const scratch = path51.join(os.tmpdir(), `vg-fix-scan-${process.pid}.json`);
|
|
11169
13167
|
const scanOpts = { vibgrateVersion: VERSION, format: "json", out: scratch, quiet: true, concurrency: 8 };
|
|
11170
13168
|
const advanced = await loadAdvancedScanHook();
|
|
11171
13169
|
return runCoreScan(rootDir, scanOpts, advanced);
|
|
11172
13170
|
};
|
|
11173
13171
|
async function loadArtifact(rootDir, inFile, scan = defaultFixScanner) {
|
|
11174
|
-
const artifactPath =
|
|
13172
|
+
const artifactPath = path51.isAbsolute(inFile) ? inFile : path51.join(rootDir, inFile);
|
|
11175
13173
|
if (await pathExists(artifactPath)) {
|
|
11176
13174
|
const staleness = scanStaleness(rootDir, artifactPath);
|
|
11177
13175
|
if (!staleness.stale) {
|
|
@@ -11198,7 +13196,7 @@ async function repositoryIdentity(rootDir, override) {
|
|
|
11198
13196
|
}
|
|
11199
13197
|
}
|
|
11200
13198
|
var fixCommand = new Command("fix").description("Get ranked, risk-tiered upgrade plans from the hosted planner and optionally apply them (prompts before changing files; --yes applies non-interactively, --dry-run previews, --no-apply only prints). Requires login/DSN.").argument("[path]", "Path to analyse", ".").option("--in <file>", "Scan artifact to read", ".vibgrate/scan_result.json").option("--format <format>", "Output format (text|json|md)", "text").option("--dsn <dsn>", 'DSN token (or use VIBGRATE_DSN env / "vg login")').option("--region <region>", "Override data residency region (us, eu)").option("--repository-name <name>", "Override the repository name recorded for this plan").option("--plan <tier>", "Apply a specific plan non-interactively (safe|balanced|aggressive)").option("--yes", "Apply the recommended plan without prompting").option("--dry-run", "Preview the recommended plan (or --plan <tier>) without applying; never prompts").option("--no-apply", "Only print the plans; never modify the project").option("--fail-on-vulns <severity>", "Exit non-zero if the recommended plan leaves an advisory at/above this severity unresolved (low|moderate|high|critical)").action(async (targetPath, opts) => {
|
|
11201
|
-
const rootDir =
|
|
13199
|
+
const rootDir = path51.resolve(targetPath);
|
|
11202
13200
|
if (!await pathExists(rootDir)) {
|
|
11203
13201
|
console.error(chalk7.red(`Path does not exist: ${rootDir}`));
|
|
11204
13202
|
process.exit(1);
|
|
@@ -11359,7 +13357,7 @@ function emit(report, format) {
|
|
|
11359
13357
|
}
|
|
11360
13358
|
}
|
|
11361
13359
|
function relProjectDir(rootDir, projectPath) {
|
|
11362
|
-
const rel =
|
|
13360
|
+
const rel = path51.isAbsolute(projectPath) ? path51.relative(rootDir, projectPath) : projectPath;
|
|
11363
13361
|
return rel === "" ? "." : rel;
|
|
11364
13362
|
}
|
|
11365
13363
|
async function buildTargetResolver(rootDir, artifact) {
|
|
@@ -11400,14 +13398,14 @@ function promptPlanSelection(response) {
|
|
|
11400
13398
|
const defaultIdx = plans.findIndex((p) => p.tier === response.recommended);
|
|
11401
13399
|
const def = defaultIdx >= 0 ? defaultIdx + 1 : 1;
|
|
11402
13400
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
11403
|
-
return new Promise((
|
|
13401
|
+
return new Promise((resolve25) => {
|
|
11404
13402
|
rl.question(`Plan [1-${plans.length}, default ${def}, q to cancel]: `, (ans) => {
|
|
11405
13403
|
rl.close();
|
|
11406
13404
|
const t = ans.trim().toLowerCase();
|
|
11407
|
-
if (t === "q" || t === "quit") return
|
|
13405
|
+
if (t === "q" || t === "quit") return resolve25(null);
|
|
11408
13406
|
const n = t === "" ? def : Number.parseInt(t, 10);
|
|
11409
|
-
if (!Number.isInteger(n) || n < 1 || n > plans.length) return
|
|
11410
|
-
|
|
13407
|
+
if (!Number.isInteger(n) || n < 1 || n > plans.length) return resolve25(null);
|
|
13408
|
+
resolve25(plans[n - 1].tier);
|
|
11411
13409
|
});
|
|
11412
13410
|
});
|
|
11413
13411
|
}
|
|
@@ -11485,7 +13483,7 @@ Applied the ${chosen.label} plan:`));
|
|
|
11485
13483
|
}
|
|
11486
13484
|
if (failed.length) process.exitCode = 2;
|
|
11487
13485
|
}
|
|
11488
|
-
function
|
|
13486
|
+
function formatText2(artifact) {
|
|
11489
13487
|
const lines = [];
|
|
11490
13488
|
const teal2 = chalk7.hex("#3FB0A4");
|
|
11491
13489
|
const mint2 = chalk7.hex("#4FE3C1");
|
|
@@ -11838,10 +13836,29 @@ function formatArchitectureDiagram(arch) {
|
|
|
11838
13836
|
lines.push(chalk7.dim(` + ${arch.folders.length - 12} more`));
|
|
11839
13837
|
}
|
|
11840
13838
|
}
|
|
13839
|
+
if (arch.coverage) {
|
|
13840
|
+
const pct = Math.round(arch.coverage.ratio * 100);
|
|
13841
|
+
lines.push(
|
|
13842
|
+
` Coverage: ${pct}% (${arch.coverage.classified} classified, ${arch.coverage.unclassified} unclassified)`
|
|
13843
|
+
);
|
|
13844
|
+
const attributed = Object.entries(arch.coverage.bySource).filter(([, n]) => n > 0).sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).map(([src, n]) => `${src} ${n}`);
|
|
13845
|
+
if (attributed.length > 0) {
|
|
13846
|
+
lines.push(chalk7.dim(` by source: ${attributed.join(", ")}`));
|
|
13847
|
+
}
|
|
13848
|
+
}
|
|
11841
13849
|
if (arch.unclassifiedFiles && arch.unclassifiedFiles.length > 0) {
|
|
11842
13850
|
const extra = arch.unclassified > arch.unclassifiedFiles.length ? ` of ${arch.unclassified}` : "";
|
|
11843
13851
|
lines.push(` Unclassified source (sample): ${arch.unclassifiedFiles.length}${extra}`);
|
|
11844
13852
|
}
|
|
13853
|
+
if (arch.unclassifiedFolders && arch.unclassifiedFolders.length > 0) {
|
|
13854
|
+
lines.push(" Unclassified by folder:");
|
|
13855
|
+
for (const folder of arch.unclassifiedFolders.slice(0, 8)) {
|
|
13856
|
+
lines.push(` ${String(folder.count).padStart(4)} ${folder.path}`);
|
|
13857
|
+
}
|
|
13858
|
+
if (arch.unclassifiedFolders.length > 8) {
|
|
13859
|
+
lines.push(chalk7.dim(` + ${arch.unclassifiedFolders.length - 8} more`));
|
|
13860
|
+
}
|
|
13861
|
+
}
|
|
11845
13862
|
lines.push("");
|
|
11846
13863
|
if (arch.layers.length > 0) {
|
|
11847
13864
|
for (const layer of arch.layers) {
|
|
@@ -12129,7 +14146,7 @@ function generatePriorityActions(artifact) {
|
|
|
12129
14146
|
}
|
|
12130
14147
|
|
|
12131
14148
|
// src/reporting/formatters/markdown.ts
|
|
12132
|
-
function
|
|
14149
|
+
function formatMarkdown2(artifact) {
|
|
12133
14150
|
const lines = [];
|
|
12134
14151
|
lines.push("# Vibgrate Drift Report");
|
|
12135
14152
|
lines.push("");
|
|
@@ -12242,7 +14259,7 @@ function formatMarkdown(artifact) {
|
|
|
12242
14259
|
|
|
12243
14260
|
// src/reporting/commands/report.ts
|
|
12244
14261
|
var reportCommand = new Command("report").description("Generate a drift report from a scan artifact").option("--in <file>", "Input artifact file", ".vibgrate/scan_result.json").option("--format <format>", "Output format (md|text|json)", "text").action(async (opts) => {
|
|
12245
|
-
const artifactPath =
|
|
14262
|
+
const artifactPath = path51.resolve(opts.in);
|
|
12246
14263
|
if (!await pathExists(artifactPath)) {
|
|
12247
14264
|
console.error(chalk7.red(`Artifact not found: ${artifactPath}`));
|
|
12248
14265
|
console.error(chalk7.dim('Run "vibgrate scan" first to generate a scan artifact.'));
|
|
@@ -12251,14 +14268,14 @@ var reportCommand = new Command("report").description("Generate a drift report f
|
|
|
12251
14268
|
const artifact = await readJsonFile(artifactPath);
|
|
12252
14269
|
switch (opts.format) {
|
|
12253
14270
|
case "md":
|
|
12254
|
-
console.log(
|
|
14271
|
+
console.log(formatMarkdown2(artifact));
|
|
12255
14272
|
break;
|
|
12256
14273
|
case "json":
|
|
12257
14274
|
console.log(JSON.stringify(artifact, null, 2));
|
|
12258
14275
|
break;
|
|
12259
14276
|
case "text":
|
|
12260
14277
|
default:
|
|
12261
|
-
console.log(
|
|
14278
|
+
console.log(formatText2(artifact));
|
|
12262
14279
|
break;
|
|
12263
14280
|
}
|
|
12264
14281
|
});
|
|
@@ -12289,10 +14306,10 @@ function openUrl(url) {
|
|
|
12289
14306
|
}
|
|
12290
14307
|
|
|
12291
14308
|
// src/reporting/commands/login.ts
|
|
12292
|
-
var delay = (ms) => new Promise((
|
|
14309
|
+
var delay = (ms) => new Promise((resolve25) => setTimeout(resolve25, ms));
|
|
12293
14310
|
function isInside(dir, file) {
|
|
12294
|
-
const rel =
|
|
12295
|
-
return !!rel && !rel.startsWith("..") && !
|
|
14311
|
+
const rel = path51.relative(dir, file);
|
|
14312
|
+
return !!rel && !rel.startsWith("..") && !path51.isAbsolute(rel);
|
|
12296
14313
|
}
|
|
12297
14314
|
var loginCommand = new Command("login").description("Authenticate the CLI with your Vibgrate workspace via the browser").option("--ingest <url>", "Ingest API URL (overrides --region)").option("--region <region>", `Data residency region (${availableRegionIds().join(", ")})`, "us").option("--no-browser", "Do not attempt to open a browser automatically").option(
|
|
12298
14315
|
"--local",
|
|
@@ -12460,8 +14477,8 @@ var logoutCommand = new Command("logout").description("Clear stored Vibgrate log
|
|
|
12460
14477
|
});
|
|
12461
14478
|
|
|
12462
14479
|
// src/reporting/commands/locale/client.ts
|
|
12463
|
-
function buildUrl(scheme, host,
|
|
12464
|
-
const url = new URL(`${scheme}://${host}/v1/locale${
|
|
14480
|
+
function buildUrl(scheme, host, path53, query) {
|
|
14481
|
+
const url = new URL(`${scheme}://${host}/v1/locale${path53}`);
|
|
12465
14482
|
for (const [key2, value] of Object.entries(query ?? {})) {
|
|
12466
14483
|
if (value !== void 0 && value !== "") url.searchParams.set(key2, String(value));
|
|
12467
14484
|
}
|
|
@@ -12509,8 +14526,8 @@ function createLocaleClient(dsn) {
|
|
|
12509
14526
|
ExitCode.ERROR
|
|
12510
14527
|
);
|
|
12511
14528
|
}
|
|
12512
|
-
const request = async (method,
|
|
12513
|
-
const url = buildUrl(parsed.scheme, parsed.host,
|
|
14529
|
+
const request = async (method, path53, opts = {}) => {
|
|
14530
|
+
const url = buildUrl(parsed.scheme, parsed.host, path53, opts.query);
|
|
12514
14531
|
const body = opts.body === void 0 ? void 0 : JSON.stringify(opts.body);
|
|
12515
14532
|
const response = await fetch(url, {
|
|
12516
14533
|
method,
|
|
@@ -12526,16 +14543,16 @@ function createLocaleClient(dsn) {
|
|
|
12526
14543
|
};
|
|
12527
14544
|
return {
|
|
12528
14545
|
workspaceId: parsed.workspaceId,
|
|
12529
|
-
get: (
|
|
12530
|
-
post: (
|
|
12531
|
-
patch: (
|
|
12532
|
-
del: (
|
|
14546
|
+
get: (path53, query) => request("GET", path53, { query }),
|
|
14547
|
+
post: (path53, body) => request("POST", path53, { body }),
|
|
14548
|
+
patch: (path53, body) => request("PATCH", path53, { body }),
|
|
14549
|
+
del: (path53, body) => request("DELETE", path53, { body })
|
|
12533
14550
|
};
|
|
12534
14551
|
}
|
|
12535
14552
|
var LOCALE_CONFIG_FILENAMES = ["vibgrate.locale.yaml", ".vibgrate/locale.yaml"];
|
|
12536
14553
|
function findLocaleConfig(cwd) {
|
|
12537
14554
|
for (const name of LOCALE_CONFIG_FILENAMES) {
|
|
12538
|
-
const candidate =
|
|
14555
|
+
const candidate = path51.join(cwd, name);
|
|
12539
14556
|
if (fs12.existsSync(candidate)) return candidate;
|
|
12540
14557
|
}
|
|
12541
14558
|
return null;
|
|
@@ -12549,15 +14566,15 @@ function parseLocaleConfig(text) {
|
|
|
12549
14566
|
const items = value.filter((v) => typeof v === "string" && v.trim().length > 0);
|
|
12550
14567
|
return items.length ? items : void 0;
|
|
12551
14568
|
};
|
|
12552
|
-
const
|
|
14569
|
+
const str2 = (value) => typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
12553
14570
|
return {
|
|
12554
|
-
project:
|
|
12555
|
-
sourceLanguage:
|
|
14571
|
+
project: str2(obj.project),
|
|
14572
|
+
sourceLanguage: str2(obj.sourceLanguage),
|
|
12556
14573
|
targetLanguages: stringArray(obj.targetLanguages),
|
|
12557
|
-
format:
|
|
12558
|
-
path:
|
|
14574
|
+
format: str2(obj.format),
|
|
14575
|
+
path: str2(obj.path),
|
|
12559
14576
|
namespaces: stringArray(obj.namespaces),
|
|
12560
|
-
version:
|
|
14577
|
+
version: str2(obj.version)
|
|
12561
14578
|
};
|
|
12562
14579
|
}
|
|
12563
14580
|
function loadLocaleConfig(cwd) {
|
|
@@ -12567,7 +14584,7 @@ function loadLocaleConfig(cwd) {
|
|
|
12567
14584
|
return { config: parseLocaleConfig(fs12.readFileSync(file, "utf8")), file };
|
|
12568
14585
|
} catch (error) {
|
|
12569
14586
|
throw new Error(
|
|
12570
|
-
`Could not parse ${
|
|
14587
|
+
`Could not parse ${path51.relative(cwd, file)}: ${error instanceof Error ? error.message : String(error)}`
|
|
12571
14588
|
);
|
|
12572
14589
|
}
|
|
12573
14590
|
}
|
|
@@ -12582,8 +14599,8 @@ function serializeLocaleConfig(config) {
|
|
|
12582
14599
|
return stringify(out2);
|
|
12583
14600
|
}
|
|
12584
14601
|
function writeLocaleConfig(cwd, config, filename) {
|
|
12585
|
-
const target = filename ??
|
|
12586
|
-
fs12.mkdirSync(
|
|
14602
|
+
const target = filename ?? path51.join(cwd, LOCALE_CONFIG_FILENAMES[0]);
|
|
14603
|
+
fs12.mkdirSync(path51.dirname(target), { recursive: true });
|
|
12587
14604
|
fs12.writeFileSync(target, serializeLocaleConfig(config), "utf8");
|
|
12588
14605
|
return target;
|
|
12589
14606
|
}
|
|
@@ -12725,7 +14742,7 @@ function validatePathTemplate(template) {
|
|
|
12725
14742
|
if (!template.includes("{language}")) {
|
|
12726
14743
|
return 'path template must contain {language} (e.g. "src/locales/{language}/{namespace}.json").';
|
|
12727
14744
|
}
|
|
12728
|
-
if (
|
|
14745
|
+
if (path51.isAbsolute(template)) {
|
|
12729
14746
|
return "path template must be relative to the project directory.";
|
|
12730
14747
|
}
|
|
12731
14748
|
if (template.split(/[\\/]/).includes("..")) {
|
|
@@ -12741,7 +14758,7 @@ function resolveLocaleFiles(cwd, template, languages, namespaces) {
|
|
|
12741
14758
|
for (const language of languages) {
|
|
12742
14759
|
for (const namespace of namespaces) {
|
|
12743
14760
|
const relative13 = renderPath(template, language, namespace);
|
|
12744
|
-
const file =
|
|
14761
|
+
const file = path51.resolve(cwd, relative13);
|
|
12745
14762
|
out2.push({ language, namespace, file, relative: relative13, exists: fs12.existsSync(file) });
|
|
12746
14763
|
}
|
|
12747
14764
|
}
|
|
@@ -12763,7 +14780,7 @@ function writeCatalog(file, format, catalog, opts = {}) {
|
|
|
12763
14780
|
const existing = fs12.existsSync(file) ? fs12.readFileSync(file, "utf8") : null;
|
|
12764
14781
|
const changed = existing !== encoded;
|
|
12765
14782
|
if (changed && !opts.dryRun) {
|
|
12766
|
-
fs12.mkdirSync(
|
|
14783
|
+
fs12.mkdirSync(path51.dirname(file), { recursive: true });
|
|
12767
14784
|
fs12.writeFileSync(file, encoded, "utf8");
|
|
12768
14785
|
}
|
|
12769
14786
|
return { file, changed, bytes: Buffer.byteLength(encoded, "utf8") };
|
|
@@ -12784,7 +14801,7 @@ function diffCatalogs(before, after) {
|
|
|
12784
14801
|
// src/reporting/commands/locale/index.ts
|
|
12785
14802
|
var cwdOption = ["-C, --cwd <dir>", "Run against this directory (default: current)"];
|
|
12786
14803
|
function root(opts) {
|
|
12787
|
-
return
|
|
14804
|
+
return path51.resolve(opts.cwd ?? process.cwd());
|
|
12788
14805
|
}
|
|
12789
14806
|
function withCommonOptions(cmd) {
|
|
12790
14807
|
return cmd.option(...cwdOption).option("--dsn <dsn>", 'DSN token (or use VIBGRATE_DSN, or run "vg login")').option("--project <slug>", "Locale project slug or id").option("--format <format>", `Locale file format (${SUPPORTED_FORMATS.join(", ")})`).option("--path <template>", "Path template, e.g. src/locales/{language}/{namespace}.json").option("--namespace <name>", "Restrict to one namespace").option("--language <code>", "Restrict to one BCP-47 language").option("--ver <version>", "Target version (default: latest)").option("--json", "Machine-readable output").option("-q, --quiet", "Suppress non-essential output");
|
|
@@ -12854,7 +14871,7 @@ var configCmd = withCommonOptions(
|
|
|
12854
14871
|
if (!isSupportedFormat(next2.format)) codecFor(next2.format);
|
|
12855
14872
|
const written = writeLocaleConfig(dir, next2, file ?? void 0);
|
|
12856
14873
|
emit2(opts, { file: written, config: next2 }, () => {
|
|
12857
|
-
console.log(chalk7.green(`Wrote ${
|
|
14874
|
+
console.log(chalk7.green(`Wrote ${path51.relative(dir, written) || path51.basename(written)}`));
|
|
12858
14875
|
console.log(
|
|
12859
14876
|
chalk7.dim('Credentials are not stored here \u2014 run "vg login" or set VIBGRATE_DSN.')
|
|
12860
14877
|
);
|
|
@@ -13330,7 +15347,7 @@ var vexCommand = new Command("vex").description("Generate an OpenVEX document (e
|
|
|
13330
15347
|
try {
|
|
13331
15348
|
const statements = [];
|
|
13332
15349
|
if (opts.from) {
|
|
13333
|
-
const fromPath =
|
|
15350
|
+
const fromPath = path51.resolve(opts.from);
|
|
13334
15351
|
if (!await pathExists(fromPath)) {
|
|
13335
15352
|
process.stderr.write(chalk7.red(`Statements file not found: ${fromPath}
|
|
13336
15353
|
`));
|
|
@@ -13348,9 +15365,9 @@ var vexCommand = new Command("vex").description("Generate an OpenVEX document (e
|
|
|
13348
15365
|
});
|
|
13349
15366
|
const json2 = JSON.stringify(doc, null, 2);
|
|
13350
15367
|
if (opts.out) {
|
|
13351
|
-
await writeTextFile(
|
|
15368
|
+
await writeTextFile(path51.resolve(opts.out), json2 + "\n");
|
|
13352
15369
|
process.stderr.write(
|
|
13353
|
-
chalk7.green("\u2714") + ` Wrote OpenVEX document (${doc.statements.length} statement(s)) to ${
|
|
15370
|
+
chalk7.green("\u2714") + ` Wrote OpenVEX document (${doc.statements.length} statement(s)) to ${path51.resolve(opts.out)}
|
|
13354
15371
|
`
|
|
13355
15372
|
);
|
|
13356
15373
|
} else {
|
|
@@ -13533,7 +15550,7 @@ function formatDeltaText(base, current) {
|
|
|
13533
15550
|
return lines.join("\n");
|
|
13534
15551
|
}
|
|
13535
15552
|
async function readArtifactOrExit(filePath) {
|
|
13536
|
-
const absolutePath =
|
|
15553
|
+
const absolutePath = path51.resolve(filePath);
|
|
13537
15554
|
if (!await pathExists(absolutePath)) {
|
|
13538
15555
|
console.error(chalk7.red(`Artifact not found: ${absolutePath}`));
|
|
13539
15556
|
process.exit(1);
|
|
@@ -13550,7 +15567,7 @@ var exportCommand = new Command("export").description("Export scan artifact as S
|
|
|
13550
15567
|
const sbom = format === "cyclonedx" ? toCycloneDx(artifact) : toSpdx(artifact);
|
|
13551
15568
|
const body = JSON.stringify(sbom, null, 2);
|
|
13552
15569
|
if (opts.out) {
|
|
13553
|
-
await writeTextFile(
|
|
15570
|
+
await writeTextFile(path51.resolve(opts.out), body);
|
|
13554
15571
|
console.log(chalk7.green("\u2714") + ` SBOM written to ${opts.out}`);
|
|
13555
15572
|
} else {
|
|
13556
15573
|
console.log(body);
|
|
@@ -13561,7 +15578,7 @@ var deltaCommand = new Command("delta").description("Show SBOM delta between two
|
|
|
13561
15578
|
const current = await readArtifactOrExit(opts.to);
|
|
13562
15579
|
const report = formatDeltaText(base, current);
|
|
13563
15580
|
if (opts.out) {
|
|
13564
|
-
await writeTextFile(
|
|
15581
|
+
await writeTextFile(path51.resolve(opts.out), report);
|
|
13565
15582
|
console.log(chalk7.green("\u2714") + ` SBOM delta report written to ${opts.out}`);
|
|
13566
15583
|
} else {
|
|
13567
15584
|
console.log(report);
|
|
@@ -13692,20 +15709,20 @@ function resolveRegime(id) {
|
|
|
13692
15709
|
return regime;
|
|
13693
15710
|
}
|
|
13694
15711
|
function evidenceDir(root3) {
|
|
13695
|
-
return
|
|
15712
|
+
return path51.join(root3, ".vibgrate", "evidence");
|
|
13696
15713
|
}
|
|
13697
15714
|
function orgPath(root3) {
|
|
13698
|
-
return
|
|
15715
|
+
return path51.join(evidenceDir(root3), "org.json");
|
|
13699
15716
|
}
|
|
13700
15717
|
function productsPath(root3) {
|
|
13701
|
-
return
|
|
15718
|
+
return path51.join(evidenceDir(root3), "products.json");
|
|
13702
15719
|
}
|
|
13703
15720
|
function releasesDir(root3) {
|
|
13704
|
-
return
|
|
15721
|
+
return path51.join(evidenceDir(root3), "releases");
|
|
13705
15722
|
}
|
|
13706
15723
|
function releasePath(root3, productId, version) {
|
|
13707
15724
|
const safe = `${productId}@${version}`.replace(/[^A-Za-z0-9._@-]/g, "_");
|
|
13708
|
-
return
|
|
15725
|
+
return path51.join(releasesDir(root3), `${safe}.json`);
|
|
13709
15726
|
}
|
|
13710
15727
|
var DEFAULT_ORG = {
|
|
13711
15728
|
responsiblePersons: [],
|
|
@@ -13736,7 +15753,7 @@ async function freezeRelease(root3, release) {
|
|
|
13736
15753
|
const p = releasePath(root3, release.productId, release.version);
|
|
13737
15754
|
if (await pathExists(p)) {
|
|
13738
15755
|
throw new CliError(
|
|
13739
|
-
`release ${release.productId}@${release.version} is already frozen and manifests are immutable \u2014 delete ${
|
|
15756
|
+
`release ${release.productId}@${release.version} is already frozen and manifests are immutable \u2014 delete ${path51.relative(root3, p)} only if it was frozen in error`,
|
|
13740
15757
|
ExitCode.USAGE_ERROR
|
|
13741
15758
|
);
|
|
13742
15759
|
}
|
|
@@ -13750,7 +15767,7 @@ async function loadReleases(root3, productId) {
|
|
|
13750
15767
|
const files = (await readdir(dir)).filter((f) => f.endsWith(".json")).sort();
|
|
13751
15768
|
const releases = [];
|
|
13752
15769
|
for (const f of files) {
|
|
13753
|
-
const rel = await readJsonFile(
|
|
15770
|
+
const rel = await readJsonFile(path51.join(dir, f));
|
|
13754
15771
|
if (!productId || rel.productId === productId) releases.push(rel);
|
|
13755
15772
|
}
|
|
13756
15773
|
return releases;
|
|
@@ -13911,9 +15928,9 @@ function derInt(n) {
|
|
|
13911
15928
|
if (bytes[0] & 128) bytes = Buffer.concat([Buffer.from([0]), bytes]);
|
|
13912
15929
|
return der(TAG.INTEGER, bytes);
|
|
13913
15930
|
}
|
|
13914
|
-
function buildTimeStampReq(
|
|
15931
|
+
function buildTimeStampReq(digest2) {
|
|
13915
15932
|
const algId = der(TAG.SEQUENCE, Buffer.concat([der(TAG.OID, OID_SHA256), der(TAG.NULL, Buffer.alloc(0))]));
|
|
13916
|
-
const messageImprint = der(TAG.SEQUENCE, Buffer.concat([algId, der(TAG.OCTET_STRING,
|
|
15933
|
+
const messageImprint = der(TAG.SEQUENCE, Buffer.concat([algId, der(TAG.OCTET_STRING, digest2)]));
|
|
13917
15934
|
const version = derInt(1);
|
|
13918
15935
|
const nonce = der(TAG.INTEGER, crypto3.randomBytes(16));
|
|
13919
15936
|
const certReq = der(TAG.BOOLEAN, Buffer.from([255]));
|
|
@@ -13992,9 +16009,9 @@ function verifyTimestamp(tsr, digestHex) {
|
|
|
13992
16009
|
reason: imprintMatches ? `RFC 3161 token binds to this result at ${token.genTime} (TSA signature chain not re-verified here \u2014 use \`openssl ts -verify\`)` : "RFC 3161 token imprint does not match this result"
|
|
13993
16010
|
};
|
|
13994
16011
|
}
|
|
13995
|
-
async function requestTimestamp(tsaUrl,
|
|
16012
|
+
async function requestTimestamp(tsaUrl, digest2) {
|
|
13996
16013
|
if (typeof fetch !== "function") throw new CliError("network fetch unavailable \u2014 cannot reach the TSA", ExitCode.ERROR);
|
|
13997
|
-
const req = buildTimeStampReq(
|
|
16014
|
+
const req = buildTimeStampReq(digest2);
|
|
13998
16015
|
let res;
|
|
13999
16016
|
try {
|
|
14000
16017
|
res = await fetch(tsaUrl, {
|
|
@@ -14008,7 +16025,7 @@ async function requestTimestamp(tsaUrl, digest) {
|
|
|
14008
16025
|
}
|
|
14009
16026
|
if (!res.ok) throw new CliError(`TSA ${tsaUrl} returned ${res.status}`, ExitCode.ERROR);
|
|
14010
16027
|
const body = Buffer.from(await res.arrayBuffer());
|
|
14011
|
-
const v = verifyTimestamp(body,
|
|
16028
|
+
const v = verifyTimestamp(body, digest2.toString("hex"));
|
|
14012
16029
|
if (!v.imprintMatches) throw new CliError(`TSA response did not bind to our digest (${v.reason})`, ExitCode.ERROR);
|
|
14013
16030
|
return body;
|
|
14014
16031
|
}
|
|
@@ -14445,12 +16462,12 @@ function verifyEvidenceEnvelope(env, opts = {}) {
|
|
|
14445
16462
|
var DEFAULT_KEY = "attest-key.pem";
|
|
14446
16463
|
function resolveSigningKey(root3, explicit) {
|
|
14447
16464
|
const chosen = process.env.VG_ATTEST_KEY;
|
|
14448
|
-
const keyPath = chosen ?
|
|
16465
|
+
const keyPath = chosen ? path51.resolve(chosen) : path51.join(root3, ".vibgrate", DEFAULT_KEY);
|
|
14449
16466
|
let minted = false;
|
|
14450
16467
|
if (!fs12.existsSync(keyPath)) {
|
|
14451
16468
|
if (chosen) throw new CliError(`signing key not found: ${chosen}`, ExitCode.USAGE_ERROR);
|
|
14452
16469
|
const kp = generateKeypair();
|
|
14453
|
-
fs12.mkdirSync(
|
|
16470
|
+
fs12.mkdirSync(path51.dirname(keyPath), { recursive: true });
|
|
14454
16471
|
fs12.writeFileSync(keyPath, kp.privatePem, { mode: 384 });
|
|
14455
16472
|
fs12.writeFileSync(`${keyPath}.pub`, kp.publicPem);
|
|
14456
16473
|
minted = true;
|
|
@@ -14468,16 +16485,16 @@ function resolveSigningKey(root3, explicit) {
|
|
|
14468
16485
|
}
|
|
14469
16486
|
function writeBundle(input) {
|
|
14470
16487
|
const dir = input.outDir;
|
|
14471
|
-
fs12.mkdirSync(
|
|
14472
|
-
const write = (rel, data) => fs12.writeFileSync(
|
|
16488
|
+
fs12.mkdirSync(path51.join(dir, "inputs", "releases"), { recursive: true });
|
|
16489
|
+
const write = (rel, data) => fs12.writeFileSync(path51.join(dir, rel), `${JSON.stringify(data, null, 2)}
|
|
14473
16490
|
`);
|
|
14474
16491
|
write("result.json", input.result);
|
|
14475
|
-
write(
|
|
16492
|
+
write(path51.join("inputs", "advisory.json"), input.advisory);
|
|
14476
16493
|
for (const rel of input.releases) {
|
|
14477
16494
|
const safe = `${rel.productId}@${rel.version}`.replace(/[^A-Za-z0-9._@-]/g, "_");
|
|
14478
|
-
write(
|
|
16495
|
+
write(path51.join("inputs", "releases", `${safe}.json`), rel);
|
|
14479
16496
|
}
|
|
14480
|
-
write(
|
|
16497
|
+
write(path51.join("inputs", "datapack.lock"), { dataPackVersion: input.result.meta.dataPackVersion, kernelVersion: input.result.meta.kernelVersion });
|
|
14481
16498
|
write("provenance.json", {
|
|
14482
16499
|
method: "deterministic",
|
|
14483
16500
|
modelDrafted: [],
|
|
@@ -14493,13 +16510,13 @@ function writeBundle(input) {
|
|
|
14493
16510
|
disclaimer: input.regime.disclaimer
|
|
14494
16511
|
});
|
|
14495
16512
|
if (input.envelope) {
|
|
14496
|
-
fs12.writeFileSync(
|
|
16513
|
+
fs12.writeFileSync(path51.join(dir, "evidence.intoto.jsonl"), `${JSON.stringify(input.envelope)}
|
|
14497
16514
|
`);
|
|
14498
16515
|
}
|
|
14499
16516
|
if (input.timestampToken) {
|
|
14500
|
-
fs12.writeFileSync(
|
|
16517
|
+
fs12.writeFileSync(path51.join(dir, "timestamp.tsr"), input.timestampToken);
|
|
14501
16518
|
}
|
|
14502
|
-
fs12.writeFileSync(
|
|
16519
|
+
fs12.writeFileSync(path51.join(dir, "VERIFY.md"), verifyDoc(input));
|
|
14503
16520
|
return dir;
|
|
14504
16521
|
}
|
|
14505
16522
|
function verifyDoc(input) {
|
|
@@ -14560,13 +16577,13 @@ function undeterminedFields(result, regime) {
|
|
|
14560
16577
|
return gaps.filter((g) => regime.submission.requires.some((r) => g.startsWith(r)) || g.startsWith("exposure") || g.startsWith("member"));
|
|
14561
16578
|
}
|
|
14562
16579
|
async function recordDrill(root3, record) {
|
|
14563
|
-
const dir =
|
|
14564
|
-
const p =
|
|
16580
|
+
const dir = path51.join(evidenceDir(root3), "drills");
|
|
16581
|
+
const p = path51.join(dir, `${record.drillId}.json`);
|
|
14565
16582
|
await writeJsonFile(p, record);
|
|
14566
16583
|
return p;
|
|
14567
16584
|
}
|
|
14568
16585
|
async function hasRecentDrill(root3, asOf) {
|
|
14569
|
-
const dir =
|
|
16586
|
+
const dir = path51.join(evidenceDir(root3), "drills");
|
|
14570
16587
|
if (!await pathExists(dir)) return void 0;
|
|
14571
16588
|
const { readdir } = await import('fs/promises');
|
|
14572
16589
|
const { readJsonFile: readJsonFile2 } = await import('./fs-4G5A65KS.js');
|
|
@@ -14574,7 +16591,7 @@ async function hasRecentDrill(root3, asOf) {
|
|
|
14574
16591
|
if (files.length === 0) return void 0;
|
|
14575
16592
|
const cutoff = new Date(new Date(asOf).getTime() - 90 * 24 * 3600 * 1e3).toISOString();
|
|
14576
16593
|
for (const f of files) {
|
|
14577
|
-
const rec = await readJsonFile2(
|
|
16594
|
+
const rec = await readJsonFile2(path51.join(dir, f));
|
|
14578
16595
|
if (rec.ranAt >= cutoff) return true;
|
|
14579
16596
|
}
|
|
14580
16597
|
return false;
|
|
@@ -14654,7 +16671,7 @@ function formatRegimeList() {
|
|
|
14654
16671
|
|
|
14655
16672
|
// src/reporting/commands/evidence/index.ts
|
|
14656
16673
|
function root2(opts) {
|
|
14657
|
-
return
|
|
16674
|
+
return path51.resolve(opts.cwd ?? process.cwd());
|
|
14658
16675
|
}
|
|
14659
16676
|
function slug(name) {
|
|
14660
16677
|
return name.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "product";
|
|
@@ -14742,7 +16759,7 @@ var releaseCmd = new Command("release").description("Freeze a shipped release in
|
|
|
14742
16759
|
const release = await buildRelease({
|
|
14743
16760
|
productId: product.id,
|
|
14744
16761
|
version,
|
|
14745
|
-
from:
|
|
16762
|
+
from: path51.resolve(rootDir, opts.from),
|
|
14746
16763
|
shipDate: opts.shipDate,
|
|
14747
16764
|
buildId: opts.buildId,
|
|
14748
16765
|
artefactDigest: opts.digest,
|
|
@@ -14750,7 +16767,7 @@ var releaseCmd = new Command("release").description("Freeze a shipped release in
|
|
|
14750
16767
|
frozenAt: nowIso()
|
|
14751
16768
|
});
|
|
14752
16769
|
const p = await freezeRelease(rootDir, release);
|
|
14753
|
-
console.log(chalk7.green("\u2714") + ` froze ${chalk7.bold(`${product.id}@${version}`)} \u2014 ${release.components.length} components, immutable at ${
|
|
16770
|
+
console.log(chalk7.green("\u2714") + ` froze ${chalk7.bold(`${product.id}@${version}`)} \u2014 ${release.components.length} components, immutable at ${path51.relative(rootDir, p)}`);
|
|
14754
16771
|
});
|
|
14755
16772
|
async function runExposure(rootDir, vuln, opts) {
|
|
14756
16773
|
const org = await loadOrg(rootDir);
|
|
@@ -14787,8 +16804,8 @@ var exposureCmd = new Command("exposure").description("Which shipped products co
|
|
|
14787
16804
|
const { result, regime, releases, advisory } = await runExposure(rootDir, vuln, opts);
|
|
14788
16805
|
let timestampToken;
|
|
14789
16806
|
if (opts.tsa) {
|
|
14790
|
-
const
|
|
14791
|
-
timestampToken = await requestTimestamp(opts.tsa,
|
|
16807
|
+
const digest2 = Buffer.from(exposureSubjectDigest(result), "hex");
|
|
16808
|
+
timestampToken = await requestTimestamp(opts.tsa, digest2);
|
|
14792
16809
|
result.meta.timestamp = { source: "rfc3161", value: parseTimestampToken(timestampToken).genTime };
|
|
14793
16810
|
}
|
|
14794
16811
|
if (opts.format === "json") {
|
|
@@ -14802,7 +16819,7 @@ var exposureCmd = new Command("exposure").description("Which shipped products co
|
|
|
14802
16819
|
console.log("\n" + buildPack(result, regime, org, stage));
|
|
14803
16820
|
}
|
|
14804
16821
|
if (opts.bundle) {
|
|
14805
|
-
const dir =
|
|
16822
|
+
const dir = path51.resolve(rootDir, opts.bundle, `evidence-${result.meta.evidenceId}`);
|
|
14806
16823
|
let envelope;
|
|
14807
16824
|
if (opts.sign !== false) {
|
|
14808
16825
|
const { key: key2, minted } = resolveSigningKey(rootDir);
|
|
@@ -14810,7 +16827,7 @@ var exposureCmd = new Command("exposure").description("Which shipped products co
|
|
|
14810
16827
|
envelope = signEvidenceStatement(buildEvidenceStatement(result, VERSION), key2);
|
|
14811
16828
|
}
|
|
14812
16829
|
writeBundle({ outDir: dir, result, advisory, releases, regime, envelope, timestampToken, cliVersion: VERSION });
|
|
14813
|
-
console.error(chalk7.green("\u2714") + ` evidence bundle written to ${
|
|
16830
|
+
console.error(chalk7.green("\u2714") + ` evidence bundle written to ${path51.relative(rootDir, dir)}${envelope ? " (signed)" : " (unsigned)"}${timestampToken ? " \xB7 RFC 3161" : ""}`);
|
|
14814
16831
|
}
|
|
14815
16832
|
process.exitCode = exposureExit(result.overallStatus);
|
|
14816
16833
|
});
|
|
@@ -14842,7 +16859,7 @@ var packCmd = new Command("pack").description("Build the submission pack a human
|
|
|
14842
16859
|
const stage = opts.stage ?? regime.clocks[0]?.stage ?? "early-warning";
|
|
14843
16860
|
const md = buildPack(result, regime, org, stage);
|
|
14844
16861
|
if (opts.out) {
|
|
14845
|
-
fs12.writeFileSync(
|
|
16862
|
+
fs12.writeFileSync(path51.resolve(rootDir, opts.out), md);
|
|
14846
16863
|
console.error(chalk7.green("\u2714") + ` pack written to ${opts.out}`);
|
|
14847
16864
|
} else {
|
|
14848
16865
|
console.log(md);
|
|
@@ -14884,18 +16901,18 @@ var drillCmd = new Command("drill").description("Timed dry-run of the determinat
|
|
|
14884
16901
|
}
|
|
14885
16902
|
});
|
|
14886
16903
|
var verifyCmd = new Command("verify").description("Verify an evidence bundle offline (no Vibgrate needed)").argument("<bundle>", "Path to an evidence bundle directory or evidence.intoto.jsonl").option("--pub <file>", "Public key PEM to pin the signer (establish trust)").action((bundlePath, opts) => {
|
|
14887
|
-
const abs =
|
|
14888
|
-
const envPath = fs12.statSync(abs).isDirectory() ?
|
|
16904
|
+
const abs = path51.resolve(bundlePath);
|
|
16905
|
+
const envPath = fs12.statSync(abs).isDirectory() ? path51.join(abs, "evidence.intoto.jsonl") : abs;
|
|
14889
16906
|
if (!fs12.existsSync(envPath)) throw new CliError(`no evidence.intoto.jsonl at ${bundlePath}`, ExitCode.NOT_FOUND);
|
|
14890
16907
|
const envelope = JSON.parse(fs12.readFileSync(envPath, "utf8").trim().split("\n")[0]);
|
|
14891
|
-
const resultPath =
|
|
16908
|
+
const resultPath = path51.join(path51.dirname(envPath), "result.json");
|
|
14892
16909
|
const result = fs12.existsSync(resultPath) ? JSON.parse(fs12.readFileSync(resultPath, "utf8")) : void 0;
|
|
14893
|
-
const publicKeyPem = opts.pub ? fs12.readFileSync(
|
|
16910
|
+
const publicKeyPem = opts.pub ? fs12.readFileSync(path51.resolve(opts.pub), "utf8") : void 0;
|
|
14894
16911
|
const v = verifyEvidenceEnvelope(envelope, { publicKeyPem, result });
|
|
14895
16912
|
const color = v.status === "verified" ? chalk7.green : v.status === "failed" ? chalk7.red : chalk7.yellow;
|
|
14896
16913
|
console.log(" " + color(v.status.toUpperCase()) + ` ${v.reason}`);
|
|
14897
16914
|
if (v.evidenceId) console.log(" " + chalk7.dim(`evidence ${v.evidenceId} \xB7 regime ${v.regime} \xB7 advisory ${v.advisoryId} \xB7 ${v.overallStatus}`));
|
|
14898
|
-
const tsrPath =
|
|
16915
|
+
const tsrPath = path51.join(path51.dirname(envPath), "timestamp.tsr");
|
|
14899
16916
|
if (fs12.existsSync(tsrPath) && result) {
|
|
14900
16917
|
const t = verifyTimestamp(fs12.readFileSync(tsrPath), exposureSubjectDigest(result));
|
|
14901
16918
|
const tcolor = t.imprintMatches ? chalk7.green : chalk7.red;
|
|
@@ -14964,12 +16981,12 @@ var exportCmd = new Command("export").description("Air-gap bundle of all evidenc
|
|
|
14964
16981
|
const releases = [];
|
|
14965
16982
|
for (const list of releasesByProduct.values()) releases.push(...list);
|
|
14966
16983
|
const report = computeReadiness({ regime, org, products, releasesByProduct, recentDrill: await hasRecentDrill(rootDir, today()) });
|
|
14967
|
-
const dir =
|
|
14968
|
-
await writeJsonFile(
|
|
14969
|
-
await writeJsonFile(
|
|
14970
|
-
for (const r of releases) await writeJsonFile(
|
|
14971
|
-
await writeJsonFile(
|
|
14972
|
-
console.error(chalk7.green("\u2714") + ` exported evidence state to ${
|
|
16984
|
+
const dir = path51.resolve(rootDir, opts.out);
|
|
16985
|
+
await writeJsonFile(path51.join(dir, "org.json"), org);
|
|
16986
|
+
await writeJsonFile(path51.join(dir, "products.json"), products);
|
|
16987
|
+
for (const r of releases) await writeJsonFile(path51.join(dir, "releases", `${r.productId}@${r.version}.json`.replace(/[^A-Za-z0-9._@-]/g, "_")), r);
|
|
16988
|
+
await writeJsonFile(path51.join(dir, "readiness.json"), report);
|
|
16989
|
+
console.error(chalk7.green("\u2714") + ` exported evidence state to ${path51.relative(rootDir, dir)} (${products.length} products, ${releases.length} frozen releases)`);
|
|
14973
16990
|
});
|
|
14974
16991
|
var pushCmd = new Command("push").description("Push the product registry (and an optional exposure result) to Vibgrate Cloud").option(...cwdOption2).option("--dsn <dsn>", "DSN token (or use VIBGRATE_DSN env / `vg login`)").option("--regime <id>", "Reporting regime").option("--result <file>", "An exposure result.json (or a bundle dir) to include").option("--signed", "Mark the included exposure result as signed").option("--strict", "Fail on push errors").action(async (opts) => {
|
|
14975
16992
|
const rootDir = root2(opts);
|
|
@@ -14995,8 +17012,8 @@ var pushCmd = new Command("push").description("Push the product registry (and an
|
|
|
14995
17012
|
}));
|
|
14996
17013
|
let exposure;
|
|
14997
17014
|
if (opts.result) {
|
|
14998
|
-
const raw =
|
|
14999
|
-
const resultPath = fs12.existsSync(raw) && fs12.statSync(raw).isDirectory() ?
|
|
17015
|
+
const raw = path51.resolve(rootDir, opts.result);
|
|
17016
|
+
const resultPath = fs12.existsSync(raw) && fs12.statSync(raw).isDirectory() ? path51.join(raw, "result.json") : raw;
|
|
15000
17017
|
if (!fs12.existsSync(resultPath)) throw new CliError(`no result.json at ${opts.result}`, ExitCode.NOT_FOUND);
|
|
15001
17018
|
exposure = JSON.parse(fs12.readFileSync(resultPath, "utf8"));
|
|
15002
17019
|
}
|
|
@@ -15059,6 +17076,7 @@ var KNOWN_COMMANDS = /* @__PURE__ */ new Set([
|
|
|
15059
17076
|
"module",
|
|
15060
17077
|
"daemon",
|
|
15061
17078
|
"policy",
|
|
17079
|
+
"review",
|
|
15062
17080
|
"llm-host",
|
|
15063
17081
|
"hcs",
|
|
15064
17082
|
"install",
|
|
@@ -15128,6 +17146,7 @@ ${aliasNote}Docs: https://vibgrate.com/help`;
|
|
|
15128
17146
|
registerPolicy(program);
|
|
15129
17147
|
registerLlmHost(program);
|
|
15130
17148
|
registerHcs(program);
|
|
17149
|
+
registerReview(program);
|
|
15131
17150
|
program.addCommand(initCommand);
|
|
15132
17151
|
program.addCommand(scanCommand);
|
|
15133
17152
|
program.addCommand(fixCommand);
|
|
@@ -15152,6 +17171,7 @@ var VALUE_FLAGS = /* @__PURE__ */ new Set([
|
|
|
15152
17171
|
"--only",
|
|
15153
17172
|
"--exclude",
|
|
15154
17173
|
"--export",
|
|
17174
|
+
"--out",
|
|
15155
17175
|
"-o",
|
|
15156
17176
|
"--budget",
|
|
15157
17177
|
"-b"
|
|
@@ -15197,7 +17217,7 @@ function dispatch(argv, cwd) {
|
|
|
15197
17217
|
}
|
|
15198
17218
|
const looksLikeQuestion = /\s/.test(first) || first.endsWith("?");
|
|
15199
17219
|
if (looksLikeQuestion) return ["ask", ...args];
|
|
15200
|
-
if (fs12.existsSync(
|
|
17220
|
+
if (fs12.existsSync(path51.resolve(cwd, first))) {
|
|
15201
17221
|
return [hasBuildOnlyFlag(args) ? "build" : "scan", ...args];
|
|
15202
17222
|
}
|
|
15203
17223
|
const moved = MOVED_COMMANDS[first];
|
|
@@ -15224,15 +17244,15 @@ function findFirstPositional(args) {
|
|
|
15224
17244
|
async function main(argv = process.argv) {
|
|
15225
17245
|
const raw = argv.slice(2);
|
|
15226
17246
|
if (process.env.NO_COLOR || raw.includes("--no-color")) disableColor();
|
|
15227
|
-
if (!raw.includes("--local")) kickRelevanceReadiness();
|
|
17247
|
+
if (!raw.includes("--local") && !raw.includes("--offline")) kickRelevanceReadiness();
|
|
15228
17248
|
const cwd = readCwd(raw);
|
|
15229
17249
|
const program = buildProgram();
|
|
15230
17250
|
program.exitOverride((err) => {
|
|
15231
17251
|
throw err;
|
|
15232
17252
|
});
|
|
15233
17253
|
program.configureOutput({
|
|
15234
|
-
writeErr: (
|
|
15235
|
-
writeOut: (
|
|
17254
|
+
writeErr: (str2) => process.stderr.write(str2),
|
|
17255
|
+
writeOut: (str2) => process.stdout.write(str2)
|
|
15236
17256
|
});
|
|
15237
17257
|
try {
|
|
15238
17258
|
const dispatched = dispatch(raw, cwd);
|
|
@@ -15243,9 +17263,9 @@ async function main(argv = process.argv) {
|
|
|
15243
17263
|
}
|
|
15244
17264
|
function readCwd(raw) {
|
|
15245
17265
|
const i = raw.findIndex((a) => a === "-C" || a === "--cwd");
|
|
15246
|
-
if (i >= 0 && raw[i + 1]) return
|
|
17266
|
+
if (i >= 0 && raw[i + 1]) return path51.resolve(raw[i + 1]);
|
|
15247
17267
|
const eq = raw.find((a) => a.startsWith("--cwd="));
|
|
15248
|
-
if (eq) return
|
|
17268
|
+
if (eq) return path51.resolve(eq.slice("--cwd=".length));
|
|
15249
17269
|
return process.cwd();
|
|
15250
17270
|
}
|
|
15251
17271
|
function handleError(err) {
|