weavatrix 0.2.5 → 0.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/README.md +77 -14
  2. package/SECURITY.md +14 -0
  3. package/docs/agent-task-benchmark.md +58 -0
  4. package/docs/releases/v0.2.7.md +93 -0
  5. package/package.json +6 -1
  6. package/scripts/run-agent-task-benchmark.mjs +119 -0
  7. package/skill/SKILL.md +31 -10
  8. package/src/analysis/allowed-test-runner.js +59 -0
  9. package/src/analysis/data-flow-evidence.js +114 -0
  10. package/src/analysis/dead-code-review.js +51 -0
  11. package/src/analysis/dep-check.js +42 -3
  12. package/src/analysis/duplicate-groups.js +84 -0
  13. package/src/analysis/graph-analysis.aggregate.js +3 -1
  14. package/src/analysis/graph-analysis.refs.js +19 -11
  15. package/src/analysis/hot-path-review.js +20 -2
  16. package/src/analysis/internal-audit.run.js +6 -1
  17. package/src/analysis/task-retrieval.js +116 -0
  18. package/src/bounds.js +4 -0
  19. package/src/graph/builder/lang-js.js +52 -17
  20. package/src/graph/freshness-probe.js +4 -2
  21. package/src/graph/incremental-refresh.js +4 -2
  22. package/src/graph/internal-builder.barrels.js +39 -2
  23. package/src/graph/internal-builder.build.js +58 -12
  24. package/src/mcp/catalog.mjs +28 -6
  25. package/src/mcp/graph-context.mjs +5 -1
  26. package/src/mcp/graph-diff.mjs +1 -1
  27. package/src/mcp/sync-payload.mjs +1 -1
  28. package/src/mcp/tools-actions.mjs +3 -1
  29. package/src/mcp/tools-context.mjs +164 -0
  30. package/src/mcp/tools-graph.mjs +48 -3
  31. package/src/mcp/tools-health.mjs +15 -28
  32. package/src/mcp/tools-impact-change.mjs +1 -0
  33. package/src/mcp/tools-source.mjs +7 -6
  34. package/src/mcp/tools-verified-change.mjs +169 -0
  35. package/src/precision/lsp-client.js +1 -1
  36. package/src/precision/lsp-overlay.js +1 -5
  37. package/src/precision/symbol-query.js +1 -5
  38. package/src/security/malware-heuristics.exclusions.js +3 -3
  39. package/src/security/malware-heuristics.roots.js +4 -1
  40. package/src/security/malware-heuristics.scan.js +4 -2
  41. package/src/security/malware-scoring.js +22 -5
@@ -0,0 +1,169 @@
1
+ import {findSeeds, rawGraph} from './graph-context.mjs'
2
+ import {toolResult} from './tool-result.mjs'
3
+ import {expandTaskQuery, retrieveTaskContext} from '../analysis/task-retrieval.js'
4
+ import {extractCallArgumentEvidence} from '../analysis/data-flow-evidence.js'
5
+ import {runAllowedTests, validateTestRequests} from '../analysis/allowed-test-runner.js'
6
+ import {compareDuplicateGroups} from '../analysis/duplicate-groups.js'
7
+ import {buildGraphAtGitRef} from '../analysis/git-ref-graph.js'
8
+ import {diffGraphs, formatGraphDiff} from './graph-diff.mjs'
9
+
10
+ const richResult = (value) => value?.__weavatrixToolResult === true ? value.result : null
11
+ const normalizeFile = (value) => String(value || '').replace(/\\/g, '/')
12
+ const changedFilesOf = (impact) => [...new Set((impact?.changes || []).flatMap((change) => [change.path, change.oldPath, change.newPath]).map(normalizeFile).filter((file) => file && file !== '(diff unavailable)'))]
13
+ const targetTests = (impact) => [...new Set([
14
+ ...(impact?.testEvidence?.changedFiles || []).map((item) => item.staticTestReachability?.test),
15
+ ...(impact?.blastRadius?.nodes || []).map((node) => node.testEvidence?.staticTestReachability?.test),
16
+ ].filter(Boolean))].slice(0, 30)
17
+
18
+ function testCoverage(proof, requests, suggested) {
19
+ if (!suggested.length) return {state: 'NOT_APPLICABLE', covered: [], missing: []}
20
+ if (proof.state !== 'PASS') return {state: 'PENDING', covered: [], missing: suggested}
21
+ if ((requests || []).some((request) => request?.script === 'test' && (!request.args || !request.args.length))) {
22
+ return {state: 'COMPLETE', kind: 'full-test-script', covered: suggested, missing: []}
23
+ }
24
+ const args = (requests || []).flatMap((request) => request?.args || []).map(normalizeFile)
25
+ const covered = suggested.filter((file) => args.some((arg) => arg === file || arg.endsWith(`/${file}`)))
26
+ return {state: covered.length === suggested.length ? 'COMPLETE' : 'PARTIAL', kind: 'explicit-test-paths', covered, missing: suggested.filter((file) => !covered.includes(file))}
27
+ }
28
+
29
+ function graphProof(diff) {
30
+ const runtimeCycles = diff?.cycles?.runtime?.introduced || []
31
+ return {
32
+ state: runtimeCycles.length ? 'BLOCKED' : diff.schemaMigration ? 'UNKNOWN' : 'PASS',
33
+ ...(diff.schemaMigration ? {reason: 'graph extractor/schema versions differ, so structural ratchets are not comparable'} : {}),
34
+ summary: formatGraphDiff(diff),
35
+ counts: {
36
+ nodesAdded: diff.nodes.added.length, nodesRemoved: diff.nodes.removed.length,
37
+ edgesAdded: diff.edges.added, edgesRemoved: diff.edges.removed,
38
+ moduleDependenciesAdded: diff.moduleEdges.added.length, orphaned: diff.orphaned.length,
39
+ runtimeCyclesIntroduced: runtimeCycles.length,
40
+ },
41
+ runtimeCycles: runtimeCycles.slice(0, 20), moduleDependenciesAdded: diff.moduleEdges.added.slice(0, 20),
42
+ orphaned: diff.orphaned.slice(0, 20), schemaMigration: diff.schemaMigration,
43
+ }
44
+ }
45
+
46
+ async function baselineProof(ctx, baseRef, currentGraph) {
47
+ const mode = ['full', 'no-tests', 'tests-only'].includes(currentGraph?.graphBuildMode) ? currentGraph.graphBuildMode : 'full'
48
+ const built = await buildGraphAtGitRef(ctx.repoRoot, baseRef, {mode})
49
+ if (!built.ok) return {state: 'UNKNOWN', reason: built.error}
50
+ return {...graphProof(diffGraphs(built.graph, currentGraph)), baseline: {ref: built.ref, commit: built.commit}}
51
+ }
52
+
53
+ function apiState(result) {
54
+ if (!result || result.status !== 'COMPLETE' || result.completeness?.complete !== true) return 'UNKNOWN'
55
+ if (['HTTP_METHOD_MISMATCH', 'CLIENTS_AT_RISK_WITH_METHOD_MISMATCHES'].includes(result.verdict?.code)) return 'BLOCKED'
56
+ return 'UNKNOWN'
57
+ }
58
+
59
+ function architectureState(result) {
60
+ if (!result || result.state === 'NOT_CONFIGURED' || result.state === 'ERROR') return 'UNKNOWN'
61
+ if (!result.verification) return result.state === 'READY' ? 'PASS' : 'UNKNOWN'
62
+ return result.verification.new?.length || String(result.verification.status).toUpperCase() === 'FAIL' ? 'BLOCKED' : 'PASS'
63
+ }
64
+
65
+ function decide({phase, impact, graph, architecture, duplicates, api, tests, suggestedTests, testCoverageState}) {
66
+ if (phase === 'plan') return tests.state === 'BLOCKED'
67
+ ? {verdict: 'BLOCKED', blockers: [`targeted test plan was rejected: ${tests.reason || 'invalid request'}`], unknowns: []}
68
+ : {verdict: 'UNKNOWN', blockers: [], unknowns: ['verification has not run; apply the edit and call verified_change with phase=verify']}
69
+ if (impact.status === 'COMPLETE' && !impact.changes?.length) return {verdict: 'PASS', blockers: [], unknowns: []}
70
+ const blockers = [], unknowns = []
71
+ if (impact.status !== 'COMPLETE') unknowns.push('change impact is partial or has unmapped evidence')
72
+ if (graph.state === 'BLOCKED') blockers.push('the change introduces a runtime dependency cycle')
73
+ if (graph.state === 'UNKNOWN') unknowns.push(`Git graph baseline is unavailable: ${graph.reason || 'unknown reason'}`)
74
+ if (architecture.state === 'BLOCKED') blockers.push('new architecture-contract violations were found')
75
+ if (architecture.state === 'UNKNOWN') unknowns.push('architecture contract is not configured or verification is incomplete')
76
+ if (duplicates.state === 'BLOCKED') blockers.push('new duplicate groups intersect the changed files')
77
+ if (duplicates.state === 'UNKNOWN') unknowns.push(`duplicate ratchet is incomplete: ${duplicates.reason || 'health capability unavailable'}`)
78
+ if (api.state === 'BLOCKED') blockers.push('cross-repository API evidence contains HTTP method mismatches')
79
+ if (api.state === 'UNKNOWN') unknowns.push(`API contract evidence is incomplete: ${api.reason || 'no bounded proof'}`)
80
+ if (tests.state === 'FAIL' || tests.state === 'BLOCKED') {
81
+ const failed = (tests.results || []).filter((result) => result.status !== 'PASS').map((result) => `${result.script} (${result.status}${result.exitCode == null ? '' : `, exit ${result.exitCode}`})`)
82
+ blockers.push(`targeted tests failed or were rejected: ${tests.reason || failed.join(', ') || 'unknown failure'}`)
83
+ }
84
+ if (tests.state === 'DISABLED') unknowns.push('targeted test execution was requested but runtime permission is disabled')
85
+ if (tests.state === 'NOT_REQUESTED' && suggestedTests.length) unknowns.push('affected tests were identified but no allowlisted package test was requested')
86
+ if (tests.state === 'PASS' && testCoverageState.state === 'PARTIAL') unknowns.push(`targeted test run did not cover ${testCoverageState.missing.length} suggested test path(s)`)
87
+ return {verdict: blockers.length ? 'BLOCKED' : unknowns.length ? 'UNKNOWN' : 'PASS', blockers, unknowns}
88
+ }
89
+
90
+ export async function tVerifiedChange(g, args = {}, ctx = {}, tools = {}, permissions = {}) {
91
+ const phase = args.phase === 'verify' ? 'verify' : 'plan'
92
+ const currentGraph = rawGraph(ctx)
93
+ const impactValue = await tools.impact(g, {base: args.base_ref, diff: args.diff, files: args.files, depth: args.impact_depth, max_nodes: args.max_impact_nodes}, ctx)
94
+ const impact = richResult(impactValue) || {status: 'PARTIAL', changes: [], seeds: {ids: []}, blastRadius: {nodes: []}}
95
+ const changedFiles = changedFilesOf(impact)
96
+ const retrieval = retrieveTaskContext(g, {
97
+ task: args.task, semanticSeeds: findSeeds(g, expandTaskQuery(args.task), 12, {repoRoot: ctx.repoRoot}),
98
+ changedSeedIds: impact.seeds?.ids, maxSymbols: args.max_symbols, repoRoot: ctx.repoRoot,
99
+ })
100
+
101
+ let contexts = []
102
+ if (permissions.source) contexts = await Promise.all(retrieval.selected.map(async (symbol) => {
103
+ const value = await tools.context(g, {label: symbol.id, precision: args.precision, max_related: 8, max_source_files: 3, context_lines: 4}, ctx, tools.inspect)
104
+ const result = richResult(value)
105
+ return result ? {symbol: symbol.id, status: result.status, definition: result.definition, evidence: result.evidence, references: result.references, inbound: result.inbound, outbound: result.outbound, reExports: result.reExports, source: result.source} : {symbol: symbol.id, status: 'UNKNOWN'}
106
+ }))
107
+ const dataFlow = permissions.source ? extractCallArgumentEvidence({
108
+ graph: g, repoRoot: ctx.repoRoot, seedIds: retrieval.selected.map((item) => item.id),
109
+ depth: Math.max(1, Math.min(3, Number(args.data_flow_depth) || 2)),
110
+ maxEdges: Math.max(1, Math.min(60, Number(args.max_data_flow_edges) || 30)),
111
+ })
112
+ : {model: 'bounded call-argument-to-parameter evidence (not CFG or taint analysis)', status: 'UNAVAILABLE', reason: 'source capability is not enabled', edges: [], unsupportedEdges: 0, capped: false}
113
+ const suggestedTests = targetTests(impact)
114
+ const checkedTests = validateTestRequests(ctx.repoRoot, args.tests || [])
115
+ const testProof = phase === 'verify'
116
+ ? await runAllowedTests(ctx.repoRoot, args.tests || [], {enabled: args.run_tests === true, timeoutMs: args.test_timeout_ms})
117
+ : {state: checkedTests.ok ? 'PLANNED' : 'BLOCKED', reason: checkedTests.reason, plan: checkedTests.tests || [], results: []}
118
+ const testCoverageState = testCoverage(testProof, args.tests || [], suggestedTests)
119
+
120
+ const architectureValue = phase === 'verify'
121
+ ? (permissions.health ? tools.verifyArchitecture(g, {}, ctx) : null)
122
+ : tools.prepareChange(g, {intent: args.task, files: changedFiles}, ctx)
123
+ const architectureResult = richResult(architectureValue)
124
+ const architecture = {state: architectureState(architectureResult), evidence: architectureResult}
125
+ const baseRef = String(args.base_ref || 'HEAD').trim()
126
+ const graph = phase === 'verify' ? await baselineProof(ctx, baseRef, currentGraph) : {state: 'PLANNED', baseline: baseRef}
127
+
128
+ let duplicates = {state: 'SKIPPED', reason: 'duplicate ratchet disabled'}
129
+ if (phase === 'verify' && args.duplicate_ratchet !== false) duplicates = permissions.health
130
+ ? await compareDuplicateGroups({repoRoot: ctx.repoRoot, graphPath: ctx.graphPath, currentGraph, baseRef, changedFiles, args: {mode: 'renamed', min_similarity: 80, min_tokens: 50}})
131
+ : {state: 'UNKNOWN', reason: 'health capability is not enabled'}
132
+
133
+ let api = {state: 'SKIPPED', reason: 'no api_contract scope was requested'}
134
+ if (args.api_contract) {
135
+ if (!permissions.crossrepo) api = {state: 'UNKNOWN', reason: 'crossrepo capability is not enabled'}
136
+ else {
137
+ const result = richResult(await tools.traceApi(g, {
138
+ ...args.api_contract, changed_files: args.api_contract.changed_files || changedFiles,
139
+ max_endpoints: Math.min(100, Number(args.api_contract.max_endpoints) || 100),
140
+ max_matches: Math.min(500, Number(args.api_contract.max_matches) || 500),
141
+ max_affected_files: Math.min(100, Number(args.api_contract.max_affected_files) || 100),
142
+ top_n: Math.min(10, Number(args.api_contract.top_n) || 10),
143
+ }, ctx))
144
+ api = {state: apiState(result), evidence: result}
145
+ }
146
+ }
147
+
148
+ const decision = decide({phase, impact, graph, architecture, duplicates, api, tests: testProof, suggestedTests, testCoverageState})
149
+ if (!permissions.source && retrieval.selected.length) decision.unknowns.push('source capability is disabled; exact LSP/source edit contexts were not collected')
150
+ if (phase === 'verify' && contexts.some((item) => item.status !== 'OK' || item.evidence?.state !== 'EXACT' || item.references?.capped)) {
151
+ decision.unknowns.push('one or more exact edit contexts are incomplete')
152
+ }
153
+ if (phase === 'verify' && args.diff) decision.unknowns.push('a supplied diff was classified, but equivalence between that patch and the active graph is not proven; verify on a checked-out change without diff')
154
+ if (decision.verdict === 'PASS' && decision.unknowns.length) decision.verdict = 'UNKNOWN'
155
+ const result = {
156
+ schemaVersion: 'weavatrix.verified-change.v1', verdict: decision.verdict, phase, task: String(args.task),
157
+ blockers: decision.blockers, unknowns: decision.unknowns,
158
+ retrieval, editContexts: contexts, dataFlow, changeImpact: impact, graphBaseline: graph,
159
+ architecture, duplicates, apiContract: api, tests: {...testProof, suggestedFiles: suggestedTests, coverage: testCoverageState},
160
+ }
161
+ const text = [
162
+ `${decision.verdict} — verified_change ${phase}`, `Task: ${String(args.task).slice(0, 500)}`,
163
+ `Change: ${changedFiles.length} file(s), ${impact.seeds?.ids?.length || 0} exact seed(s), blast radius ${impact.blastRadius?.impacted || 0}.`,
164
+ `Edit context: ${retrieval.selected.length} symbol(s); ${contexts.length} exact bundle(s); data-flow ${dataFlow.status} (${dataFlow.edges.length} call edge(s)).`,
165
+ `Ratchets: graph ${graph.state}; architecture ${architecture.state}; duplicates ${duplicates.state}; API ${api.state}; tests ${testProof.state}.`,
166
+ ...decision.blockers.map((item) => `BLOCKER: ${item}`), ...decision.unknowns.map((item) => `UNKNOWN: ${item}`),
167
+ ].join('\n')
168
+ return toolResult(text, result, {completeness: {status: decision.verdict === 'UNKNOWN' ? 'PARTIAL' : 'COMPLETE'}})
169
+ }
@@ -425,7 +425,7 @@ export class StdioLspClient {
425
425
  await this.writeMessage({jsonrpc: JSON_RPC_VERSION, method, params})
426
426
  }
427
427
 
428
- async initialize({capabilities = {}, initializationOptions, clientInfo = {name: 'weavatrix', version: '0.2.5'}} = {}) {
428
+ async initialize({capabilities = {}, initializationOptions, clientInfo = {name: 'weavatrix', version: '0.2.6'}} = {}) {
429
429
  if (this.state !== 'running') throw new Error(`LSP initialize is invalid in state=${this.state}`)
430
430
  const result = await this.request('initialize', {
431
431
  processId: process.pid,
@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
2
2
  import { existsSync, readFileSync, realpathSync, statSync } from "node:fs";
3
3
  import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
+ import { boundedInteger } from "../bounds.js";
5
6
  import { atomicWriteFileSync } from "../graph/file-lock.js";
6
7
  import { edgeProvenance } from "../graph/edge-provenance.js";
7
8
  import { isStructuralRelation } from "../graph/relations.js";
@@ -450,11 +451,6 @@ function publicSemanticSafetyReason(reason) {
450
451
  : "TypeScript project configuration could not be verified safely";
451
452
  }
452
453
 
453
- function boundedInteger(value, fallback, minimum, maximum) {
454
- const number = Number(value);
455
- return Math.max(minimum, Math.min(maximum, Number.isFinite(number) ? Math.floor(number) : fallback));
456
- }
457
-
458
454
  export async function buildLspPrecisionOverlay({
459
455
  repoRoot,
460
456
  graph,
@@ -1,5 +1,6 @@
1
1
  import {existsSync, readFileSync, statSync} from 'node:fs'
2
2
  import {dirname, resolve} from 'node:path'
3
+ import {boundedInteger} from '../bounds.js'
3
4
  import {atomicWriteFileSync, withFileLock} from '../graph/file-lock.js'
4
5
  import {
5
6
  buildLspPrecisionOverlay,
@@ -15,11 +16,6 @@ const MAX_CACHE_BYTES = 8 * 1024 * 1024
15
16
  const MAX_CACHE_READ_BYTES = 16 * 1024 * 1024
16
17
  const inFlight = new Map()
17
18
 
18
- const boundedInteger = (value, fallback, minimum, maximum) => {
19
- const number = Number(value)
20
- return Math.max(minimum, Math.min(maximum, Number.isFinite(number) ? Math.floor(number) : fallback))
21
- }
22
-
23
19
  export function symbolPrecisionCachePath(graphPath) {
24
20
  return resolve(dirname(graphPath), SYMBOL_PRECISION_CACHE_FILE)
25
21
  }
@@ -8,7 +8,7 @@ const URL_RE = /https?:\/\/[^\s'"`)\\<>]+/gi;
8
8
  const PACKAGE_JSON_SCRIPT_RE = /["']?(preinstall|install|postinstall|prepare|prepack|postpack|prepublish|prepublishonly)["']?\s*:/i;
9
9
  const PACKAGE_JSON_METADATA_URL_RE = /["']?(bugs|homepage|repository|funding|author|contributors|maintainers|publishConfig|license)["']?\s*:|["']?url["']?\s*:/i;
10
10
  const NETWORK_CALL_RE = /\b(fetch|axios\.|XMLHttpRequest|sendBeacon|https?\.request|request\(|curl\b|wget\b|iwr\b|invoke-webrequest)\b/i;
11
- const LIFECYCLE_SCRIPT_HOOKS = ["preinstall", "install", "postinstall", "prepare"];
11
+ const INSTALL_SCRIPT_HOOKS = ["preinstall", "install", "postinstall"];
12
12
 
13
13
  function cleanUrlToken(value) {
14
14
  return String(value || "").trim().replace(/[.,;:!?]+$/g, "").replace(/[)\]}]+$/g, "");
@@ -102,8 +102,8 @@ export function isManifestUrlNoise(file, signal) {
102
102
  return /(^|\/)(pkg-info|metadata)$/.test(f);
103
103
  }
104
104
 
105
- export function lifecycleScriptSnippet(scripts = {}) {
106
- for (const hook of LIFECYCLE_SCRIPT_HOOKS) {
105
+ export function installScriptSnippet(scripts = {}) {
106
+ for (const hook of INSTALL_SCRIPT_HOOKS) {
107
107
  const s = String(scripts?.[hook] || "").trim();
108
108
  if (s) return `${hook}: ${s}`.slice(0, 200);
109
109
  }
@@ -109,7 +109,10 @@ export function buildContentRoots(repoPath, installed) {
109
109
  const roots = [];
110
110
  const nmDir = join(repoPath, "node_modules");
111
111
  const jsPkgDirs = existingDir(nmDir) ? listPkgDirs(nmDir) : [];
112
- if (jsPkgDirs.length) roots.push({ kind: "npm", root: nmDir, packages: jsPkgDirs.length, pathToPkg: pkgOfNodeModulesPath });
112
+ // Scan installed package roots, not the whole node_modules container. Package-manager caches,
113
+ // hosted release snapshots and other hidden metadata are not installed dependencies and may
114
+ // legitimately contain malware-signature fixtures (including Weavatrix's own rule source).
115
+ for (const item of jsPkgDirs) roots.push({ kind: "npm", root: item.dir, packages: 1, pathToPkg: pkgOfNodeModulesPath });
113
116
 
114
117
  for (const sp of sitePackageRoots(repoPath)) {
115
118
  const topMap = pyTopLevelMap(sp);
@@ -6,7 +6,7 @@ import { resolveRg } from "../scan/search.js";
6
6
  import { classifyInstallScript, classifyResolvedUrl, isCloudSdkMetadataUse } from "./registry-sig.js";
7
7
  import { fileHeuristicSweep } from "./malware-file-heuristics.js";
8
8
  import { buildMalwareFindings } from "./malware-scoring.js";
9
- import { parseMalwareExclusions, isManifestUrlNoise, isDocUrlNoise, isMalwareSignalExcluded, lifecycleScriptSnippet } from "./malware-heuristics.exclusions.js";
9
+ import { parseMalwareExclusions, isManifestUrlNoise, isDocUrlNoise, isMalwareSignalExcluded, installScriptSnippet } from "./malware-heuristics.exclusions.js";
10
10
  import { buildContentRoots, listPkgDirs } from "./malware-heuristics.roots.js";
11
11
  import { rgSweep, nodeSweep } from "./malware-heuristics.sweep.js";
12
12
 
@@ -37,7 +37,9 @@ export async function scanMalware(repoPath, { installed = [], importedPkgs = new
37
37
  for (const { pkg, dir } of pkgDirs) {
38
38
  try {
39
39
  const pj = JSON.parse(readFileSync(join(dir, "package.json"), "utf8"));
40
- const scriptSnippet = lifecycleScriptSnippet(pj.scripts);
40
+ // package-lock hasInstallScript describes install-time hooks. Development and
41
+ // publication hooks such as prepare do not constitute drift from that flag.
42
+ const scriptSnippet = installScriptSnippet(pj.scripts);
41
43
  if (scriptSnippet && lockScriptMeta.get(`${pkg}@${pj.version || ""}`) === false) {
42
44
  add(pkg, {
43
45
  key: "install-script-drift",
@@ -26,8 +26,11 @@ function visibleSignals(signals, allow) {
26
26
  return hasStrongerUrl ? kept.filter((s) => s.key !== "hardcoded-url") : kept;
27
27
  }
28
28
 
29
- function escalatedSeverity(baseSeverity, smoking, multi, nearZeroFp) {
30
- if (smoking && nearZeroFp) return "critical";
29
+ // Heuristic source matches can justify urgent review, but they cannot prove that code executed or
30
+ // credentials left the machine. Reserve `critical` for independently confirmed advisories/runtime
31
+ // evidence; this scanner therefore has an explicit HIGH ceiling.
32
+ function heuristicSeverity(baseSeverity, smoking, multi) {
33
+ if (baseSeverity === "critical") return "high";
31
34
  if ((smoking || multi) && (baseSeverity === "medium" || baseSeverity === "low")) return "high";
32
35
  return baseSeverity;
33
36
  }
@@ -63,20 +66,34 @@ export function buildMalwareFindings({ byPkg, importedPkgs, contentRoots, scanMo
63
66
  for (const key of keys) {
64
67
  const ofKey = visible.filter((s) => s.key === key);
65
68
  const base = ofKey[0];
66
- const severity = escalatedSeverity(base.severity, smoking, multi, base.nearZeroFp);
69
+ const severity = heuristicSeverity(base.severity, smoking, multi);
67
70
  findings.push(makeFinding({
68
71
  category: "malware",
69
72
  rule: key,
70
73
  severity,
71
74
  confidence: base.nearZeroFp ? "high" : multi ? "medium" : "low",
75
+ assessment: "SUSPICIOUS_STATIC_EVIDENCE",
76
+ confirmedExecution: false,
77
+ requiresRuntimeConfirmation: true,
78
+ severityCeiling: "high",
72
79
  title: `${base.what}: ${pkg}`,
73
- detail: `${base.what} in installed package "${pkg}"${multi ? ` - co-occurs with ${[...keys].filter((k) => k !== key).join(", ")} (escalated)` : ""}${importedPkgs.has(pkg) ? ". This package IS imported by the repo's own code." : ". Transitive install (still runs its install scripts)."} Heuristic pattern match - inspect the cited file before acting.`,
80
+ detail: `${base.what} in installed package "${pkg}"${multi ? ` - co-occurs with ${[...keys].filter((k) => k !== key).join(", ")} (escalated)` : ""}${importedPkgs.has(pkg) ? ". This package IS imported by the repo's own code." : ". Transitive install (its install scripts may have run)."} Static heuristic evidence only: execution, package compromise, and credential exposure are NOT confirmed. Inspect the cited file and verify origin/lockfile/runtime evidence before acting.`,
74
81
  package: pkg,
75
82
  file: base.file ? String(base.file).replace(/\\/g, "/") : "",
76
83
  line: base.line || 0,
77
84
  evidence: evidenceRows(ofKey),
78
85
  source: "heuristic",
79
- fixHint: smoking ? `treat as compromised: remove ${pkg}, reinstall from a clean lockfile, rotate reachable secrets` : `review the cited code in ${pkg}`,
86
+ verification: {
87
+ staticPattern: "MATCHED",
88
+ runtimeExecution: "NOT_VERIFIED",
89
+ packageOrigin: "NOT_VERIFIED",
90
+ lockfileIntegrity: "NOT_VERIFIED",
91
+ credentialExposure: "NOT_VERIFIED",
92
+ decision: "MANUAL_CONFIRMATION_REQUIRED",
93
+ },
94
+ fixHint: smoking
95
+ ? `quarantine and inspect ${pkg}; verify package origin and lockfile integrity before removal; rotate secrets only if execution or credential exposure is confirmed`
96
+ : `review the cited code in ${pkg}`,
80
97
  }));
81
98
  }
82
99
  }