weavatrix 0.2.6 → 0.2.8

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.
@@ -3,10 +3,11 @@
3
3
  // staleness probe in graph-context. Hot-reloadable (re-imported by catalog.mjs on change).
4
4
  import {
5
5
  isSymbol, degreeOf, labelOf, connList,
6
- resolveNodeInfo, resolveNode, ambiguityNote, findSeeds, resolveSeedFiles, undirectedNeighbors,
6
+ resolveNodeInfo, resolveNode, ambiguityNote, findSeeds, resolveSeedFiles, undirectedNeighbors, requestedPathClasses,
7
7
  graphStaleness, fileStalenessNote,
8
8
  } from './graph-context.mjs'
9
9
  import {summarizeEdgeProvenance} from '../graph/edge-provenance.js'
10
+ import {createPathClassifier, hasPathClass} from '../path-classification.js'
10
11
 
11
12
  const compileKind = (edge) => edge?.typeOnly === true ? 'type-only' : edge?.compileOnly === true ? 'compile-only' : null
12
13
 
@@ -165,7 +166,15 @@ export function tGetCommunity(g, {community_id} = {}) {
165
166
  // A plain BFS/DFS flood dumps every reached node (thousands on a real graph) at near-zero signal.
166
167
  // Instead: traverse to record reach + distance-from-seed, then show only the closest, most-connected
167
168
  // slice as a coherent subgraph (edges kept only among shown nodes). Honest about what was trimmed.
168
- export function tQueryGraph(g, {question, mode = 'bfs', depth = 3, context_filter, seed_files, augment_seeds = false, token_budget = 2000} = {}, toolCtx = {}) {
169
+ const QUERY_NON_PRODUCT = Object.freeze(['test', 'e2e', 'generated', 'mock', 'story', 'docs', 'benchmark', 'temp'])
170
+ const LOW_SIGNAL_SYMBOL_RE = /^(?:const(?:ant)?|variable|property|field|enum_member)$/i
171
+ const querySourceFile = (node) => String(node?.source_file || String(node?.id || '').split('#', 1)[0]).replace(/\\/g, '/').replace(/^\.\//, '').toLowerCase()
172
+ const queryWords = (value) => new Set(String(value || '').replace(/([a-z0-9])([A-Z])/g, '$1 $2').toLowerCase().split(/[^a-z0-9_]+/).filter(Boolean))
173
+
174
+ export function tQueryGraph(g, {
175
+ question, mode = 'bfs', depth = 3, context_filter, seed_files, augment_seeds = false,
176
+ include_classified = false, include_low_signal = false, token_budget = 2000,
177
+ } = {}, toolCtx = {}) {
169
178
  const pinned = resolveSeedFiles(g, seed_files)
170
179
  // Exact seed files are a control surface, not a hint: by default they disable fuzzy keyword seeds.
171
180
  // Callers can opt back into augmentation when they explicitly want both behaviors.
@@ -177,6 +186,32 @@ export function tQueryGraph(g, {question, mode = 'bfs', depth = 3, context_filte
177
186
  const maxDepth = Math.max(1, Math.min(6, Number(depth) || 3))
178
187
  const ctx = Array.isArray(context_filter) && context_filter.length ? new Set(context_filter.map((c) => String(c).toLowerCase())) : null
179
188
  const relOk = (rel) => !ctx || ctx.has(String(rel ?? '').toLowerCase())
189
+ const requestedClasses = requestedPathClasses(question)
190
+ const classifier = createPathClassifier(toolCtx.repoRoot || null)
191
+ const classificationCache = new Map()
192
+ const pinnedFiles = new Set(pinned.seeds.map(querySourceFile))
193
+ const classifiedSuppressed = new Set()
194
+ const pathPolicy = (id) => {
195
+ const node = g.byId.get(String(id))
196
+ const file = querySourceFile(node)
197
+ if (!file || pinnedFiles.has(file) || include_classified === true) return {ok: true}
198
+ if (!classificationCache.has(file)) classificationCache.set(file, classifier.explain(file, {content: ''}))
199
+ const info = classificationCache.get(file)
200
+ const classes = QUERY_NON_PRODUCT.filter((name) => hasPathClass(info, name))
201
+ if (!classes.length && !info?.excluded) return {ok: true}
202
+ if (classes.some((name) => requestedClasses.has(name))) return {ok: true}
203
+ classifiedSuppressed.add(String(id))
204
+ return {ok: false, bucket: 'classified'}
205
+ }
206
+ const questionTerms = queryWords(question)
207
+ const isLowSignal = (id) => {
208
+ if (include_low_signal === true || start.includes(String(id))) return false
209
+ const node = g.byId.get(String(id))
210
+ if (!node || !isSymbol(node.id) || !LOW_SIGNAL_SYMBOL_RE.test(String(node.symbol_kind || ''))) return false
211
+ const labelTerms = queryWords(node.label || String(node.id || '').split('#').pop() || '')
212
+ if ([...questionTerms].some((term) => labelTerms.has(term))) return false
213
+ return degreeOf(g, id) === 0
214
+ }
180
215
  const charBudget = Math.max(400, (Number(token_budget) || 2000) * 4)
181
216
  // node budget scales gently with the token budget; edges follow the surviving nodes.
182
217
  const nodeBudget = Math.max(20, Math.min(120, Math.round((Number(token_budget) || 2000) / 40)))
@@ -193,6 +228,7 @@ export function tQueryGraph(g, {question, mode = 'bfs', depth = 3, context_filte
193
228
  if (d >= maxDepth) continue
194
229
  for (const [nid, rel] of undirectedNeighbors(g, id)) {
195
230
  if (!relOk(rel)) continue
231
+ if (!pathPolicy(nid).ok) continue
196
232
  if (!seen.has(nid)) stack.push({id: nid, d: d + 1})
197
233
  }
198
234
  }
@@ -204,6 +240,7 @@ export function tQueryGraph(g, {question, mode = 'bfs', depth = 3, context_filte
204
240
  for (const id of frontier)
205
241
  for (const [nid, rel] of undirectedNeighbors(g, id)) {
206
242
  if (!relOk(rel)) continue
243
+ if (!pathPolicy(nid).ok) continue
207
244
  if (!depthOf.has(nid)) {
208
245
  depthOf.set(nid, d + 1)
209
246
  next.push(nid)
@@ -213,7 +250,10 @@ export function tQueryGraph(g, {question, mode = 'bfs', depth = 3, context_filte
213
250
  }
214
251
  }
215
252
  // rank reached nodes: seeds first, then by proximity (depth asc), then connectivity (degree desc)
253
+ const reachedBeforeSignalFilter = depthOf.size
254
+ const lowSignalSuppressed = [...depthOf.keys()].filter(isLowSignal).length
216
255
  const ranked = [...depthOf.entries()]
256
+ .filter(([id]) => !isLowSignal(id))
217
257
  .map(([id, d]) => ({id, d, deg: degreeOf(g, id)}))
218
258
  .sort((a, b) => a.d - b.d || b.deg - a.deg)
219
259
  const shown = ranked.slice(0, nodeBudget)
@@ -236,7 +276,10 @@ export function tQueryGraph(g, {question, mode = 'bfs', depth = 3, context_filte
236
276
  `Query: "${question}" (${mode}, depth ${maxDepth}${ctx ? `, context ${[...ctx].join('/')}` : ''})`,
237
277
  `Seeds: ${seeds.map((s) => s.label ?? s.id).join(', ')}`,
238
278
  pinned.missing.length ? `Unresolved pinned seed files: ${pinned.missing.join(', ')}` : null,
239
- `Reached ${depthOf.size} nodes; showing ${shown.length} closest by proximity + connectivity, ${shownEdges.length} edges among them.`,
279
+ `Reached ${reachedBeforeSignalFilter} policy-eligible nodes; showing ${shown.length} closest by proximity + connectivity, ${shownEdges.length} edges among them.`,
280
+ classifiedSuppressed.size ? `Suppressed ${classifiedSuppressed.size} classified/non-product traversal node(s); ask for that class or pass include_classified:true.` : null,
281
+ lowSignalSuppressed ? `Suppressed ${lowSignalSuppressed} unreferenced constant/field node(s) with no query-term match; pass include_low_signal:true to inspect them.` : null,
282
+ include_classified === true ? 'Path policy: classified/non-product traversal explicitly enabled.' : `Path policy: production-first${requestedClasses.size ? `; explicit question classes enabled: ${[...requestedClasses].join(', ')}` : ''}.`,
240
283
  ``,
241
284
  `Nodes:`,
242
285
  ]
@@ -3,6 +3,7 @@
3
3
  import {spawnSync} from 'node:child_process'
4
4
  import {degreeOf, rawGraph, effectiveRawGraph} from './graph-context.mjs'
5
5
  import {computeDuplicates} from '../analysis/duplicates.js'
6
+ import {analyzeDuplicateGroups} from '../analysis/duplicate-groups.js'
6
7
  import {runInternalAudit} from '../analysis/internal-audit.js'
7
8
  import {classifyChangeImpact} from '../analysis/change-classification.js'
8
9
  import {compareAuditDebt, normalizeAuditScopeFiles, scopeAuditFindings} from '../analysis/audit-debt.js'
@@ -21,6 +22,7 @@ import {toolResult} from './tool-result.mjs'
21
22
  import {createPathClassifier, hasPathClass} from '../path-classification.js'
22
23
  import {createRepoBoundary} from '../repo-path.js'
23
24
  import {filterGraphForMode} from '../graph/graph-filter.js'
25
+ import {readCachedSymbolPrecisionEvidence} from '../precision/symbol-query.js'
24
26
 
25
27
  const NON_PRODUCT_DUPLICATE_CLASSES = new Set(['generated', 'mock', 'story', 'docs', 'benchmark', 'temp'])
26
28
  const fragmentEligible = (fragment, {tokMin, skipTests, includeClassified}) => {
@@ -31,26 +33,6 @@ const fragmentEligible = (fragment, {tokMin, skipTests, includeClassified}) => {
31
33
  return true
32
34
  }
33
35
 
34
- // Group clone pairs into union-find families.
35
- function groupClones(data, {simMin, tokMin, mode, skipTests, includeClassified}) {
36
- const frags = data.frags || []
37
- const elig = (i) => fragmentEligible(frags[i], {tokMin, skipTests, includeClassified})
38
- const pairs = (data.modes?.[mode] || []).filter(([i, j, s]) => s >= simMin && elig(i) && elig(j))
39
- const parent = new Map()
40
- const find = (x) => { let r = x; while (parent.has(r) && parent.get(r) !== r) r = parent.get(r); return r }
41
- for (const [i, j] of pairs) { if (!parent.has(i)) parent.set(i, i); if (!parent.has(j)) parent.set(j, j); parent.set(find(i), find(j)) }
42
- const groups = new Map()
43
- for (const [i, j, s] of pairs) {
44
- const r = find(i)
45
- if (!groups.has(r)) groups.set(r, {members: new Set(), maxSim: 0})
46
- const g = groups.get(r); g.members.add(i); g.members.add(j); g.maxSim = Math.max(g.maxSim, s)
47
- }
48
- return [...groups.values()].map((g) => {
49
- const members = [...g.members].sort((a, b) => frags[b].n - frags[a].n)
50
- return {members: members.map((i) => frags[i]), maxSim: g.maxSim, tokens: members.reduce((n, i) => n + frags[i].n, 0)}
51
- }).sort((a, b) => b.tokens - a.tokens)
52
- }
53
-
54
36
  export function tFindDuplicates(g, args, ctx) {
55
37
  if (!ctx.repoRoot) return 'Duplicate scan needs the repo root (not provided to this server).'
56
38
  const simMin = Math.min(100, Math.max(50, Number(args.min_similarity) || 80))
@@ -97,10 +79,10 @@ export function tFindDuplicates(g, args, ctx) {
97
79
  })
98
80
  return `Found ${candidates.length} actionable same-name pair(s) across files (semantic mode; one closest clone and/or farthest collision per name). Top ${top.length}:\n\n${lines.join('\n\n')}\n\nThese are review candidates, not automatic refactors. Use read_source on both sites before changing code.`
99
81
  }
100
- const data = computeDuplicates(ctx.repoRoot, ctx.graphPath, {includeStrings, minTokens: tokMin})
101
- const groups = groupClones(data, {simMin, tokMin, mode, skipTests, includeClassified})
82
+ const analysis = analyzeDuplicateGroups(ctx.repoRoot, ctx.graphPath, args)
83
+ const groups = analysis.groups
102
84
  const smallPolicy = tokMin < 30 ? ' Small fragments below 30 tokens require at least 95% similarity and two shared bounded fingerprints.' : ''
103
- const suppressed = data.frags.filter((fragment) => !fragmentEligible(fragment, {tokMin, skipTests, includeClassified})).length
85
+ const suppressed = analysis.suppressed
104
86
  const suppressionNote = suppressed && !includeClassified
105
87
  ? ` ${suppressed} fragment(s) classified as tests/e2e/generated/mock/story/docs/benchmark/temp or matched by .weavatrix.json exclude were suppressed; pass include_classified:true (and include_tests:true for tests) to inspect them explicitly.`
106
88
  : ''
@@ -120,7 +102,27 @@ export function tFindDuplicates(g, args, ctx) {
120
102
  // candidates. It never returns an automatic-delete verdict.
121
103
  export function tFindDeadCode(g, args, ctx) {
122
104
  if (!ctx.repoRoot) return 'Dead-code review needs the repo root (not provided to this server).'
123
- const graph = effectiveRawGraph(ctx)
105
+ const effectiveGraph = effectiveRawGraph(ctx)
106
+ const pointEvidence = readCachedSymbolPrecisionEvidence({repoRoot: ctx.repoRoot, graphPath: ctx.graphPath, graph: effectiveGraph})
107
+ const graph = {
108
+ ...effectiveGraph,
109
+ precisionReferenceSymbols: [...new Set([
110
+ ...(effectiveGraph.precisionReferenceSymbols || []),
111
+ ...pointEvidence.referenceSymbols,
112
+ ])],
113
+ precisionProductionReferenceSymbols: [...new Set([
114
+ ...(effectiveGraph.precisionProductionReferenceSymbols || []),
115
+ ...pointEvidence.productionReferenceSymbols,
116
+ ])],
117
+ precisionTestReferenceSymbols: [...new Set([
118
+ ...(effectiveGraph.precisionTestReferenceSymbols || []),
119
+ ...pointEvidence.testReferenceSymbols,
120
+ ])],
121
+ precisionNoReferenceSymbols: [...new Set([
122
+ ...(effectiveGraph.precisionNoReferenceSymbols || []),
123
+ ...pointEvidence.noReferenceSymbols,
124
+ ])],
125
+ }
124
126
  const boundary = createRepoBoundary(ctx.repoRoot)
125
127
  const pkg = readRepoJson(boundary, 'package.json') || {}
126
128
  const rules = readRepoJson(boundary, '.weavatrix-deps.json') || {}
@@ -162,13 +164,15 @@ export function tFindDeadCode(g, args, ctx) {
162
164
  : `${candidate.owner ? `${candidate.owner}.` : ''}${candidate.symbol || candidate.id}`
163
165
  const where = `${candidate.file}${candidate.line ? `:${candidate.line}` : ''}`
164
166
  return [
165
- `${index + 1}. [${candidate.confidence}/${candidate.classification}] ${subject} (${where})`,
167
+ `${index + 1}. [${candidate.confidence}/${candidate.evidenceTier}/${candidate.classification}] ${subject} (${where})`,
166
168
  ` evidence: ${candidate.evidence.map((item) => item.fact).join(' ')}`,
167
169
  candidate.caveats.length ? ` caution: ${candidate.caveats.join(' ')}` : null,
170
+ ` remaining: ${candidate.remainingChecks.join(' ')}`,
168
171
  ].filter(Boolean).join('\n')
169
172
  })
170
173
  const text = [
171
174
  `Dead-code review: ${shown.length} of ${review.candidates.length} candidate(s) shown (high ${counts.high}, medium ${counts.medium}, low ${counts.low}).`,
175
+ `Evidence tiers: strong static ${review.totals.byEvidenceTier.strongStatic}, bounded static ${review.totals.byEvidenceTier.boundedStatic}, high uncertainty ${review.totals.byEvidenceTier.highUncertainty}.`,
172
176
  `Verdict: REVIEW_REQUIRED. This is static evidence, never permission to auto-delete or bulk-delete.`,
173
177
  suppression ? `Suppressed by current filters: ${suppression}.` : null,
174
178
  review.suppressed.confidence ? 'Use min_confidence=low only when public/framework/dynamic candidates need explicit review.' : null,
@@ -196,7 +200,10 @@ const SEVERITY_RANK = {critical: 0, high: 1, medium: 2, low: 3, info: 4}
196
200
 
197
201
  export function formatAuditFinding(f) {
198
202
  const where = f.file ? ` (${f.file}${f.symbol ? ` ${f.symbol}` : ''})` : f.package ? ` (pkg ${f.package}${f.version ? `@${f.version}` : ''}${f.manifest ? `; ${f.manifest}` : ''})` : ''
199
- return ` [${f.severity}/${f.confidence || '?'}] ${f.rule}: ${f.title}${where}${f.reason ? `\n reason: ${f.reason}` : ''}${f.cycleRoute ? `\n route: ${f.cycleRoute}` : ''}${f.fixHint ? `\n fix: ${f.fixHint}` : ''}`
203
+ const verification = f.verification?.evidenceModel
204
+ ? `\n verification: ${f.verification.evidenceModel}; manifest ${f.verification.manifestDeclaration?.status || 'N/A'}; indexed imports ${f.verification.indexedSourceImports?.status || 'N/A'}; decision ${f.verification.decision || 'REVIEW_REQUIRED'}`
205
+ : ''
206
+ return ` [${f.severity}/${f.confidence || '?'}] ${f.rule}: ${f.title}${where}${f.reason ? `\n reason: ${f.reason}` : ''}${verification}${f.cycleRoute ? `\n route: ${f.cycleRoute}` : ''}${f.fixHint ? `\n fix: ${f.fixHint}` : ''}`
200
207
  }
201
208
 
202
209
  const auditFilter = (audit, args, findings = audit.findings) => {
@@ -234,7 +241,7 @@ const formatOrdinaryAudit = (audit, args, findings = audit.findings, heading = n
234
241
  heading,
235
242
  `Internal audit of ${audit.repo} (${audit.scanned.files} files, ${audit.scanned.symbols} symbols, ${audit.scanned.externalImports} external imports; malware scan: ${audit.scanned.malwareScanMode}).`,
236
243
  ...auditConventionLines(audit),
237
- `Dependency manifests: ${deps.status || 'UNKNOWN'} — checked ${deps.declared ?? audit.scanned.manifestDeps ?? 0} declared package(s) against ${deps.importRecords ?? audit.scanned.externalImports ?? 0} external import record(s); unused ${deps.unused ?? 'unknown'}, missing ${deps.missing ?? 'unknown'}, duplicate declarations ${deps.duplicateDeclarations ?? 'unknown'}.`,
244
+ `Dependency manifests: ${deps.status || 'UNKNOWN'} — checked ${deps.declared ?? audit.scanned.manifestDeps ?? 0} declared package(s) against ${deps.importRecords ?? audit.scanned.externalImports ?? 0} external import record(s); unused ${deps.unused ?? 'unknown'}, missing ${deps.missing ?? 'unknown'}, duplicate declarations ${deps.duplicateDeclarations ?? 'unknown'}. npm per-finding manifest/source/config verification: ${deps.perFindingVerification ? 'AVAILABLE' : 'UNAVAILABLE'}.`,
238
245
  `Scoped severity: critical ${sev.critical}, high ${sev.high}, medium ${sev.medium}, low ${sev.low}, info ${sev.info}. Scoped categories: unused ${bycat.unused}, structure ${bycat.structure}, vulnerability ${bycat.vulnerability}, malware ${bycat.malware}.`,
239
246
  `Repository-level ${auditChecksLine(audit)}`,
240
247
  '',
@@ -428,7 +435,7 @@ export async function tRunAudit(g, args, ctx) {
428
435
  `Internal audit of ${audit.repo} (${audit.scanned.files} files, ${audit.scanned.symbols} symbols, ${audit.scanned.externalImports} external imports; malware scan: ${audit.scanned.malwareScanMode}).`,
429
436
  `Severity: critical ${sev.critical}, high ${sev.high}, medium ${sev.medium}, low ${sev.low}, info ${sev.info}. Categories: unused ${bycat.unused}, structure ${bycat.structure}, vulnerability ${bycat.vulnerability}, malware ${bycat.malware}.`,
430
437
  `Structure: ${audit.structureReport?.runtimeCycles ?? audit.structureReport?.cycles ?? 0} runtime cycle(s), ${audit.structureReport?.compileTimeCouplings ?? audit.structureReport?.typeCouplings ?? 0} compile-time coupling group(s), ${audit.structureReport?.orphans ?? 0} orphan(s); import edges: ${audit.structureReport?.runtimeImportEdges ?? audit.structureReport?.importEdges ?? 0} runtime + ${audit.structureReport?.typeOnlyImportEdges ?? 0} type-only + ${audit.structureReport?.compileOnlyImportEdges ?? 0} compile-only. Dead: ${audit.deadReport.deadFiles} file(s), ${audit.deadReport.unusedExports} unused export(s).`,
431
- `Dependency manifests: ${deps.status || 'UNKNOWN'} — checked ${deps.declared ?? audit.scanned.manifestDeps ?? 0} declared package(s) against ${deps.importRecords ?? audit.scanned.externalImports ?? 0} external import record(s); unused ${deps.unused ?? 'unknown'}, missing ${deps.missing ?? 'unknown'}, duplicate declarations ${deps.duplicateDeclarations ?? 'unknown'}.`,
438
+ `Dependency manifests: ${deps.status || 'UNKNOWN'} — checked ${deps.declared ?? audit.scanned.manifestDeps ?? 0} declared package(s) against ${deps.importRecords ?? audit.scanned.externalImports ?? 0} external import record(s); unused ${deps.unused ?? 'unknown'}, missing ${deps.missing ?? 'unknown'}, duplicate declarations ${deps.duplicateDeclarations ?? 'unknown'}. npm per-finding manifest/source/config verification: ${deps.perFindingVerification ? 'AVAILABLE' : 'UNAVAILABLE'}.`,
432
439
  ...auditConventionLines(audit),
433
440
  `Checks: ${check('OSV', audit.checks?.osv)}; ${check('malware', audit.checks?.malware)}. A NOT_CHECKED/PARTIAL/ERROR check is incomplete or unknown, never a clean zero.`,
434
441
  ``,
@@ -540,6 +547,7 @@ export function tHotPathReview(g, args, ctx) {
540
547
  const text = [
541
548
  `Local hot-path review: ${review.candidateSymbols} candidate(s) from ${review.analyzedSymbols} analyzed symbol(s); showing ${review.hotspots.length}.`,
542
549
  `Thresholds: time rank >=${review.thresholds.timeRank}, cyclomatic >=${review.thresholds.cyclomatic}, calls >=${review.thresholds.calls}, loop depth >=${review.thresholds.loopDepth}, score >=${review.thresholds.minScore}.`,
550
+ `Selection: ${review.selectionPolicy.mode}${review.selectionPolicy.strongLocalFallback ? '; strong local sort/recursion/deep-loop evidence can pass below the blended score gate' : '; strict explicit score gate'}.`,
543
551
  coverageLine,
544
552
  'Local syntax cost and graph coupling are separate. Scores are review priority, not measured runtime.',
545
553
  '',
@@ -557,7 +565,7 @@ export function tHotPathReview(g, args, ctx) {
557
565
  ` ${item.reasons.slice(0, 5).join('; ')}${evidence}`,
558
566
  ]
559
567
  }) : [' (none at the selected thresholds)']),
560
- review.bounds.truncated ? `... +${review.bounds.totalCandidates - review.bounds.returned} more (raise top_n, min_score, or narrow path).` : null,
568
+ review.bounds.truncated ? `... +${review.bounds.totalCandidates - review.bounds.returned} more (raise top_n to display more, raise min_score or narrow path to tighten; lower min_score to broaden).` : null,
561
569
  '',
562
570
  'Caveat: no interprocedural Big-O, recursion-bound, CFG, dead-store or taint-flow claim is made.',
563
571
  ].filter((line) => line != null).join('\n')
@@ -244,6 +244,7 @@ export function tChangeImpactV2(g, args = {}, ctx = {}) {
244
244
  },
245
245
  testEvidence: {
246
246
  actualCoverage: hasCoverage ? 'AVAILABLE' : 'NOT_AVAILABLE',
247
+ changedFiles: changed.slice(0, 500).map((file) => ({file, ...testEvidenceFor(file)})),
247
248
  staticTestReachability: {
248
249
  kind: staticTests.kind,
249
250
  testFiles: staticTests.testFiles,
@@ -0,0 +1,185 @@
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 exactUsageLines(contexts, g) {
66
+ if (!contexts.length) return []
67
+ return contexts.map((context) => {
68
+ const node = g.byId.get(String(context.symbol || context.definition?.id || ''))
69
+ const label = String(context.definition?.label || node?.label || context.symbol || 'symbol')
70
+ if (context.status !== 'OK') return `Exact usage: ${label} — unavailable (${context.status || 'UNKNOWN'}).`
71
+ const exact = context.evidence?.state === 'EXACT'
72
+ const occurrences = Number(context.references?.occurrences) || 0
73
+ const files = Number(context.references?.files) || 0
74
+ const inbound = Array.isArray(context.inbound?.shown) ? context.inbound.shown.slice(0, 3) : []
75
+ const callers = inbound.map((item) => `${item.label || item.id}${item.file ? ` [${item.file}]` : ''}`).join(', ')
76
+ return `${exact ? 'Exact usage' : 'Bounded usage'}: ${label} — ${occurrences} reference occurrence(s)${files ? ` in ${files} file(s)` : ''}; ${Number(context.inbound?.total) || 0} inbound container(s)${callers ? `: ${callers}` : ''}.`
77
+ })
78
+ }
79
+
80
+ function decide({phase, impact, graph, architecture, duplicates, api, tests, suggestedTests, testCoverageState}) {
81
+ if (phase === 'plan') return tests.state === 'BLOCKED'
82
+ ? {verdict: 'BLOCKED', blockers: [`targeted test plan was rejected: ${tests.reason || 'invalid request'}`], unknowns: []}
83
+ : {verdict: 'UNKNOWN', blockers: [], unknowns: ['verification has not run; apply the edit and call verified_change with phase=verify']}
84
+ if (impact.status === 'COMPLETE' && !impact.changes?.length) return {verdict: 'PASS', blockers: [], unknowns: []}
85
+ const blockers = [], unknowns = []
86
+ if (impact.status !== 'COMPLETE') unknowns.push('change impact is partial or has unmapped evidence')
87
+ if (graph.state === 'BLOCKED') blockers.push('the change introduces a runtime dependency cycle')
88
+ if (graph.state === 'UNKNOWN') unknowns.push(`Git graph baseline is unavailable: ${graph.reason || 'unknown reason'}`)
89
+ if (architecture.state === 'BLOCKED') blockers.push('new architecture-contract violations were found')
90
+ if (architecture.state === 'UNKNOWN') unknowns.push('architecture contract is not configured or verification is incomplete')
91
+ if (duplicates.state === 'BLOCKED') blockers.push('new duplicate groups intersect the changed files')
92
+ if (duplicates.state === 'UNKNOWN') unknowns.push(`duplicate ratchet is incomplete: ${duplicates.reason || 'health capability unavailable'}`)
93
+ if (api.state === 'BLOCKED') blockers.push('cross-repository API evidence contains HTTP method mismatches')
94
+ if (api.state === 'UNKNOWN') unknowns.push(`API contract evidence is incomplete: ${api.reason || 'no bounded proof'}`)
95
+ if (tests.state === 'FAIL' || tests.state === 'BLOCKED') {
96
+ const failed = (tests.results || []).filter((result) => result.status !== 'PASS').map((result) => `${result.script} (${result.status}${result.exitCode == null ? '' : `, exit ${result.exitCode}`})`)
97
+ blockers.push(`targeted tests failed or were rejected: ${tests.reason || failed.join(', ') || 'unknown failure'}`)
98
+ }
99
+ if (tests.state === 'DISABLED') unknowns.push('targeted test execution was requested but runtime permission is disabled')
100
+ if (tests.state === 'NOT_REQUESTED' && suggestedTests.length) unknowns.push('affected tests were identified but no allowlisted package test was requested')
101
+ if (tests.state === 'PASS' && testCoverageState.state === 'PARTIAL') unknowns.push(`targeted test run did not cover ${testCoverageState.missing.length} suggested test path(s)`)
102
+ return {verdict: blockers.length ? 'BLOCKED' : unknowns.length ? 'UNKNOWN' : 'PASS', blockers, unknowns}
103
+ }
104
+
105
+ export async function tVerifiedChange(g, args = {}, ctx = {}, tools = {}, permissions = {}) {
106
+ const phase = args.phase === 'verify' ? 'verify' : 'plan'
107
+ const currentGraph = rawGraph(ctx)
108
+ 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)
109
+ const impact = richResult(impactValue) || {status: 'PARTIAL', changes: [], seeds: {ids: []}, blastRadius: {nodes: []}}
110
+ const changedFiles = changedFilesOf(impact)
111
+ const retrieval = retrieveTaskContext(g, {
112
+ task: args.task, semanticSeeds: findSeeds(g, expandTaskQuery(args.task), 12, {repoRoot: ctx.repoRoot}),
113
+ changedSeedIds: impact.seeds?.ids, maxSymbols: args.max_symbols, repoRoot: ctx.repoRoot,
114
+ })
115
+
116
+ let contexts = []
117
+ if (permissions.source) contexts = await Promise.all(retrieval.selected.map(async (symbol) => {
118
+ 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)
119
+ const result = richResult(value)
120
+ 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'}
121
+ }))
122
+ const dataFlow = permissions.source ? extractCallArgumentEvidence({
123
+ graph: g, repoRoot: ctx.repoRoot, seedIds: retrieval.selected.map((item) => item.id),
124
+ depth: Math.max(1, Math.min(3, Number(args.data_flow_depth) || 2)),
125
+ maxEdges: Math.max(1, Math.min(60, Number(args.max_data_flow_edges) || 30)),
126
+ })
127
+ : {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}
128
+ const suggestedTests = targetTests(impact)
129
+ const checkedTests = validateTestRequests(ctx.repoRoot, args.tests || [])
130
+ const testProof = phase === 'verify'
131
+ ? await runAllowedTests(ctx.repoRoot, args.tests || [], {enabled: args.run_tests === true, timeoutMs: args.test_timeout_ms})
132
+ : {state: checkedTests.ok ? 'PLANNED' : 'BLOCKED', reason: checkedTests.reason, plan: checkedTests.tests || [], results: []}
133
+ const testCoverageState = testCoverage(testProof, args.tests || [], suggestedTests)
134
+
135
+ const architectureValue = phase === 'verify'
136
+ ? (permissions.health ? tools.verifyArchitecture(g, {}, ctx) : null)
137
+ : tools.prepareChange(g, {intent: args.task, files: changedFiles}, ctx)
138
+ const architectureResult = richResult(architectureValue)
139
+ const architecture = {state: architectureState(architectureResult), evidence: architectureResult}
140
+ const baseRef = String(args.base_ref || 'HEAD').trim()
141
+ const graph = phase === 'verify' ? await baselineProof(ctx, baseRef, currentGraph) : {state: 'PLANNED', baseline: baseRef}
142
+
143
+ let duplicates = {state: 'SKIPPED', reason: 'duplicate ratchet disabled'}
144
+ if (phase === 'verify' && args.duplicate_ratchet !== false) duplicates = permissions.health
145
+ ? await compareDuplicateGroups({repoRoot: ctx.repoRoot, graphPath: ctx.graphPath, currentGraph, baseRef, changedFiles, args: {mode: 'renamed', min_similarity: 80, min_tokens: 50}})
146
+ : {state: 'UNKNOWN', reason: 'health capability is not enabled'}
147
+
148
+ let api = {state: 'SKIPPED', reason: 'no api_contract scope was requested'}
149
+ if (args.api_contract) {
150
+ if (!permissions.crossrepo) api = {state: 'UNKNOWN', reason: 'crossrepo capability is not enabled'}
151
+ else {
152
+ const result = richResult(await tools.traceApi(g, {
153
+ ...args.api_contract, changed_files: args.api_contract.changed_files || changedFiles,
154
+ max_endpoints: Math.min(100, Number(args.api_contract.max_endpoints) || 100),
155
+ max_matches: Math.min(500, Number(args.api_contract.max_matches) || 500),
156
+ max_affected_files: Math.min(100, Number(args.api_contract.max_affected_files) || 100),
157
+ top_n: Math.min(10, Number(args.api_contract.top_n) || 10),
158
+ }, ctx))
159
+ api = {state: apiState(result), evidence: result}
160
+ }
161
+ }
162
+
163
+ const decision = decide({phase, impact, graph, architecture, duplicates, api, tests: testProof, suggestedTests, testCoverageState})
164
+ if (!permissions.source && retrieval.selected.length) decision.unknowns.push('source capability is disabled; exact LSP/source edit contexts were not collected')
165
+ if (phase === 'verify' && contexts.some((item) => item.status !== 'OK' || item.evidence?.state !== 'EXACT' || item.references?.capped)) {
166
+ decision.unknowns.push('one or more exact edit contexts are incomplete')
167
+ }
168
+ 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')
169
+ if (decision.verdict === 'PASS' && decision.unknowns.length) decision.verdict = 'UNKNOWN'
170
+ const result = {
171
+ schemaVersion: 'weavatrix.verified-change.v1', verdict: decision.verdict, phase, task: String(args.task),
172
+ blockers: decision.blockers, unknowns: decision.unknowns,
173
+ retrieval, editContexts: contexts, dataFlow, changeImpact: impact, graphBaseline: graph,
174
+ architecture, duplicates, apiContract: api, tests: {...testProof, suggestedFiles: suggestedTests, coverage: testCoverageState},
175
+ }
176
+ const text = [
177
+ `${decision.verdict} — verified_change ${phase}`, `Task: ${String(args.task).slice(0, 500)}`,
178
+ `Change: ${changedFiles.length} file(s), ${impact.seeds?.ids?.length || 0} exact seed(s), blast radius ${impact.blastRadius?.impacted || 0}.`,
179
+ `Edit context: ${retrieval.selected.length} symbol(s); ${contexts.length} exact bundle(s); data-flow ${dataFlow.status} (${dataFlow.edges.length} call edge(s)).`,
180
+ ...exactUsageLines(contexts, g),
181
+ `Ratchets: graph ${graph.state}; architecture ${architecture.state}; duplicates ${duplicates.state}; API ${api.state}; tests ${testProof.state}.`,
182
+ ...decision.blockers.map((item) => `BLOCKER: ${item}`), ...decision.unknowns.map((item) => `UNKNOWN: ${item}`),
183
+ ].join('\n')
184
+ return toolResult(text, result, {completeness: {status: decision.verdict === 'UNKNOWN' ? 'PARTIAL' : 'COMPLETE'}})
185
+ }
@@ -46,7 +46,7 @@ const DEFAULT_RULES = [
46
46
  {
47
47
  category: "test",
48
48
  pattern: "test/spec roots and conventional test filenames",
49
- regex: /(^|\/)(?:__tests?__|tests?|spec)(\/|$)|\.(?:test|itest|spec)\.[^.\/]+$|_test\.go$|(^|\/)test_[^/]*\.py$/i,
49
+ regex: /(^|\/)(?:__tests?__|tests?|spec)(\/|$)|\.(?:test|itest|spec)\.[^.\/]+$|_test\.go$|(^|\/)test(?:_[^/]*)?\.py$/i,
50
50
  },
51
51
  {
52
52
  category: "generated",
@@ -7,6 +7,7 @@ import {
7
7
  precisionOverlayMatches,
8
8
  precisionSemanticInputsMatch,
9
9
  } from './lsp-overlay.js'
10
+ import {createPathClassifier, hasPathClass} from '../path-classification.js'
10
11
 
11
12
  export const SYMBOL_PRECISION_CACHE_V = 1
12
13
  export const SYMBOL_PRECISION_CACHE_FILE = 'precision-symbols.json'
@@ -79,6 +80,56 @@ function cacheable(overlay) {
79
80
  && overlay?.reason === 'semantic precision stopped at a configured safety limit'
80
81
  }
81
82
 
83
+ // Point-query results intentionally live outside the broad precision overlay. Health tools still
84
+ // need to consume revision-bound positive evidence: once an exact query found a reference, the
85
+ // same symbol must not remain in a dead-code queue. This reader is synchronous and fail-closed;
86
+ // stale, malformed, incomplete no-reference, or semantically changed entries contribute nothing.
87
+ export function readCachedSymbolPrecisionEvidence({repoRoot, graphPath, graph} = {}) {
88
+ const empty = {referenceSymbols: [], productionReferenceSymbols: [], testReferenceSymbols: [], noReferenceSymbols: []}
89
+ if (!repoRoot || !graphPath || !graph) return empty
90
+ const precisionGraph = graph.graphPrecisionMode === 'off' ? {...graph, graphPrecisionMode: 'lsp'} : graph
91
+ const nodesById = new Map((precisionGraph.nodes || []).map((node) => [String(node.id), node]))
92
+ const classifier = createPathClassifier(repoRoot)
93
+ const referenced = new Set()
94
+ const production = new Set()
95
+ const tests = new Set()
96
+ const noReference = new Set()
97
+ const semanticMatches = new Map()
98
+ for (const entry of readCache(symbolPrecisionCachePath(graphPath)).entries) {
99
+ const overlay = entry?.overlay
100
+ if (!overlay || !precisionOverlayMatches(overlay, precisionGraph, {request: overlay.request})) continue
101
+ const fingerprint = String(overlay.semanticInputFingerprint || '')
102
+ if (!semanticMatches.has(fingerprint)) semanticMatches.set(
103
+ fingerprint,
104
+ precisionSemanticInputsMatch(overlay, repoRoot, precisionGraph),
105
+ )
106
+ if (!semanticMatches.get(fingerprint)) continue
107
+ const targetId = String(entry.targetId || '')
108
+ if (!nodesById.has(targetId)) continue
109
+ const locations = Array.isArray(overlay.locations)
110
+ ? overlay.locations.filter((location) => String(location?.target || '') === targetId)
111
+ : []
112
+ if (locations.length) {
113
+ referenced.add(targetId)
114
+ for (const location of locations) {
115
+ const file = String(location?.file || nodesById.get(String(location?.source || ''))?.source_file || '')
116
+ if (!file) continue
117
+ const info = classifier.explain(file, {content: ''})
118
+ if (hasPathClass(info, 'test', 'e2e')) tests.add(targetId)
119
+ else production.add(targetId)
120
+ }
121
+ }
122
+ if (overlay.state === 'COMPLETE' && Array.isArray(overlay.noReferenceSymbols)
123
+ && overlay.noReferenceSymbols.some((id) => String(id) === targetId)) noReference.add(targetId)
124
+ }
125
+ return {
126
+ referenceSymbols: [...referenced],
127
+ productionReferenceSymbols: [...production],
128
+ testReferenceSymbols: [...tests],
129
+ noReferenceSymbols: [...noReference],
130
+ }
131
+ }
132
+
82
133
  export async function querySymbolPrecision({
83
134
  repoRoot,
84
135
  graphPath,
@@ -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",