weavatrix 0.2.6 → 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.
@@ -0,0 +1,114 @@
1
+ import {readFileSync} from 'node:fs'
2
+ import {extname} from 'node:path'
3
+ import ts from 'typescript'
4
+ import {createRepoBoundary} from '../repo-path.js'
5
+
6
+ const lineOfNode = (node) => Number(String(node?.source_location || '').match(/^L(\d+)/i)?.[1] || String(node?.id || '').match(/@(\d+)$/)?.[1] || 0)
7
+ const fileOfNode = (node) => String(node?.source_file || String(node?.id || '').split('#')[0]).replace(/\\/g, '/')
8
+ const cleanName = (value) => String(value || '').replace(/\(\)$/, '').split('.').pop()
9
+ const callName = (node) => node.expression?.name?.text || node.expression?.text || node.expression?.escapedText || ''
10
+
11
+ function scriptKind(file) {
12
+ const ext = extname(file).toLowerCase()
13
+ if (ext === '.tsx') return ts.ScriptKind.TSX
14
+ if (ext === '.jsx') return ts.ScriptKind.JSX
15
+ if (ext === '.ts' || ext === '.mts' || ext === '.cts') return ts.ScriptKind.TS
16
+ return ts.ScriptKind.JS
17
+ }
18
+
19
+ function parse(cache, boundary, file) {
20
+ if (cache.has(file)) return cache.get(file)
21
+ if (!/\.[cm]?[jt]sx?$/i.test(file)) return null
22
+ const resolved = boundary.resolve(file)
23
+ if (!resolved.ok) return null
24
+ try {
25
+ const source = ts.createSourceFile(file, readFileSync(resolved.path, 'utf8'), ts.ScriptTarget.Latest, true, scriptKind(file))
26
+ cache.set(file, source)
27
+ return source
28
+ } catch { return null }
29
+ }
30
+
31
+ function parametersFor(source, target) {
32
+ const expected = cleanName(target?.label)
33
+ const expectedLine = lineOfNode(target)
34
+ const choices = []
35
+ const visit = (node) => {
36
+ if (Array.isArray(node.parameters) && node.name) {
37
+ const name = cleanName(node.name.text || node.name.getText?.(source))
38
+ if (!expected || name === expected) {
39
+ const line = source.getLineAndCharacterOfPosition(node.getStart(source)).line + 1
40
+ choices.push({line, parameters: node.parameters.map((item) => item.name.getText(source).slice(0, 80))})
41
+ }
42
+ }
43
+ ts.forEachChild(node, visit)
44
+ }
45
+ visit(source)
46
+ choices.sort((a, b) => Math.abs(a.line - expectedLine) - Math.abs(b.line - expectedLine))
47
+ return choices[0]?.parameters || []
48
+ }
49
+
50
+ function callsAt(source, line, expectedName) {
51
+ const found = []
52
+ const visit = (node) => {
53
+ if (ts.isCallExpression(node)) {
54
+ const at = source.getLineAndCharacterOfPosition(node.getStart(source)).line + 1
55
+ if ((!line || at === line) && (!expectedName || cleanName(callName(node)) === expectedName)) found.push({node, line: at})
56
+ }
57
+ ts.forEachChild(node, visit)
58
+ }
59
+ visit(source)
60
+ return found
61
+ }
62
+
63
+ // Bounded interprocedural evidence: maps arguments at graph callsites to the callee's declared
64
+ // parameters. This is not a control-flow graph, value propagation, or taint-analysis claim.
65
+ export function extractCallArgumentEvidence({graph, repoRoot, seedIds = [], depth = 2, maxEdges = 40} = {}) {
66
+ const boundary = createRepoBoundary(repoRoot)
67
+ const cache = new Map()
68
+ const queue = (seedIds || []).map((id) => ({id: String(id), hop: 0}))
69
+ const visited = new Set()
70
+ const seenCalls = new Set()
71
+ const edges = []
72
+ let unsupported = 0
73
+ while (queue.length && edges.length < maxEdges) {
74
+ const current = queue.shift()
75
+ if (visited.has(current.id) || current.hop >= depth) continue
76
+ visited.add(current.id)
77
+ const calls = [
78
+ ...(graph.out.get(current.id) || []).filter((edge) => edge.relation === 'calls')
79
+ .map((edge) => ({fromId: current.id, toId: String(edge.id), line: Number(edge.line) || 0, nextId: String(edge.id)})),
80
+ ...(graph.inn.get(current.id) || []).filter((edge) => edge.relation === 'calls')
81
+ .map((edge) => ({fromId: String(edge.id), toId: current.id, line: Number(edge.line) || 0, nextId: String(edge.id)})),
82
+ ]
83
+ for (const callEdge of calls) {
84
+ const callKey = `${callEdge.fromId}\0${callEdge.toId}\0${callEdge.line}`
85
+ if (seenCalls.has(callKey)) continue
86
+ seenCalls.add(callKey)
87
+ const from = graph.byId.get(callEdge.fromId)
88
+ const to = graph.byId.get(callEdge.toId)
89
+ const callerSource = parse(cache, boundary, fileOfNode(from))
90
+ const targetSource = parse(cache, boundary, fileOfNode(to))
91
+ if (!from || !to || !callerSource || !targetSource) { unsupported++; continue }
92
+ const params = parametersFor(targetSource, to)
93
+ const callsites = callsAt(callerSource, callEdge.line, cleanName(to.label))
94
+ if (!callsites.length) { unsupported++; continue }
95
+ for (const call of callsites.slice(0, 3)) {
96
+ edges.push({
97
+ from: callEdge.fromId, to: callEdge.toId, hop: current.hop + 1,
98
+ file: fileOfNode(from), line: call.line,
99
+ arguments: call.node.arguments.slice(0, 12).map((argument, index) => ({
100
+ index, expression: argument.getText(callerSource).slice(0, 160), parameter: params[index] || null,
101
+ })),
102
+ state: 'EXTRACTED',
103
+ })
104
+ if (edges.length >= maxEdges) break
105
+ }
106
+ if (current.hop + 1 < depth) queue.push({id: callEdge.nextId, hop: current.hop + 1})
107
+ }
108
+ }
109
+ return {
110
+ model: 'bounded call-argument-to-parameter evidence (not CFG or taint analysis)',
111
+ status: edges.length ? (unsupported ? 'PARTIAL' : 'COMPLETE') : 'UNAVAILABLE',
112
+ edges, unsupportedEdges: unsupported, capped: edges.length >= maxEdges,
113
+ }
114
+ }
@@ -84,6 +84,18 @@ function symbolCandidate(item, node, context) {
84
84
  caveats.push("Reflection is present and may invoke names without a resolvable static edge.");
85
85
  }
86
86
 
87
+ const evidenceTier = confidence === "high" && exactNoReference
88
+ ? "STRONG_STATIC_EVIDENCE"
89
+ : confidence === "low" ? "HIGH_UNCERTAINTY" : "BOUNDED_STATIC_EVIDENCE";
90
+ const remainingChecks = [
91
+ ...(!exactNoReference ? ["Run an exact language-server reference query for this declaration."] : []),
92
+ ...(publicSurface ? ["Check downstream/external consumers of the public API."] : []),
93
+ ...(externalEntry || framework ? ["Inspect framework registration and externally entered call paths."] : []),
94
+ ...(dynamicFile ? ["Resolve dynamic import/require targets and name-based dispatch."] : []),
95
+ ...(reflectionFile || (publicSurface && context.repoSignals.reflection) ? ["Inspect reflection/annotation/configuration consumers."] : []),
96
+ "Run targeted tests after any removal.",
97
+ ];
98
+
87
99
  return {
88
100
  id: String(item.id),
89
101
  kind,
@@ -95,6 +107,8 @@ function symbolCandidate(item, node, context) {
95
107
  symbolKind: node?.symbol_kind || null,
96
108
  visibility: node?.visibility || (node?.exported ? "exported" : "internal"),
97
109
  confidence,
110
+ evidenceTier,
111
+ actionability: evidenceTier === "STRONG_STATIC_EVIDENCE" ? "PRIORITY_MANUAL_REVIEW" : "MANUAL_REVIEW",
98
112
  reason: item.reason,
99
113
  evidence: [
100
114
  ...(testOnly ? [{kind: item.evidence || "graph", fact: `Only test/e2e consumers were found${item.testConsumerFiles?.length ? `: ${item.testConsumerFiles.join(", ")}` : "."}`}]
@@ -106,6 +120,18 @@ function symbolCandidate(item, node, context) {
106
120
  externallyEnteredFile: externalEntry,
107
121
  pathClasses: pathInfo.classes || [],
108
122
  matchedPathRule: pathInfo.matchedRule || null,
123
+ verification: {
124
+ graphInboundRuntimeEdge: "NOT_FOUND",
125
+ indexedSecondOccurrence: testOnly ? "TEST_ONLY" : "NOT_FOUND",
126
+ exactLanguageServerReferences: exactNoReference ? "ZERO_CONFIRMED" : "NOT_CHECKED_OR_INCOMPLETE",
127
+ recognizedEntryPoint: externalEntry ? "FOUND" : "NOT_FOUND",
128
+ dynamicLoadingRisk: dynamicFile ? "PRESENT" : "NOT_OBSERVED_IN_DECLARING_FILE",
129
+ reflectionRisk: reflectionFile || (publicSurface && context.repoSignals.reflection) ? "PRESENT" : "NOT_OBSERVED",
130
+ publicApi: publicSurface ? "YES" : "NO",
131
+ decision: "MANUAL_REVIEW_REQUIRED",
132
+ },
133
+ remainingChecks,
134
+ autoDelete: false,
109
135
  reviewAction: testOnly
110
136
  ? "Confirm that no production/config/framework consumer exists; decide whether the declaration is intentional test support or removable with its tests. Never auto-delete."
111
137
  : "Confirm with read_source, get_dependents, exact search, framework/config inspection and tests; never auto-delete.",
@@ -135,6 +161,13 @@ function fileCandidate(item, symbols, context) {
135
161
  confidence = "low";
136
162
  caveats.push("Reflection is present in this file.");
137
163
  }
164
+ const remainingChecks = [
165
+ "Inspect package scripts, manifests, framework/plugin discovery and deployment configuration.",
166
+ "Check external launchers and consumers outside the indexed repository.",
167
+ ...(dynamicFile ? ["Resolve dynamic import/require targets."] : []),
168
+ ...(reflectionFile ? ["Inspect reflection/name-based consumers."] : []),
169
+ "Run targeted tests after any removal.",
170
+ ];
138
171
  return {
139
172
  id: `file:${file}`,
140
173
  kind: "file",
@@ -146,6 +179,8 @@ function fileCandidate(item, symbols, context) {
146
179
  symbolKind: null,
147
180
  visibility: null,
148
181
  confidence,
182
+ evidenceTier: confidence === "low" ? "HIGH_UNCERTAINTY" : "BOUNDED_STATIC_EVIDENCE",
183
+ actionability: "MANUAL_REVIEW",
149
184
  reason: item.reason,
150
185
  evidence: [
151
186
  { kind: "graph", fact: "No indexed module imports this file." },
@@ -156,6 +191,17 @@ function fileCandidate(item, symbols, context) {
156
191
  externallyEnteredFile: false,
157
192
  pathClasses: pathInfo.classes || [],
158
193
  matchedPathRule: pathInfo.matchedRule || null,
194
+ verification: {
195
+ graphInboundModuleEdge: "NOT_FOUND",
196
+ indexedSymbolsReferenced: "NONE",
197
+ recognizedEntryPoint: "NOT_FOUND",
198
+ dynamicLoadingRisk: dynamicFile ? "PRESENT" : "NOT_OBSERVED",
199
+ reflectionRisk: reflectionFile ? "PRESENT" : "NOT_OBSERVED",
200
+ externalConsumerCheck: "NOT_POSSIBLE_FROM_REPOSITORY_GRAPH",
201
+ decision: "MANUAL_REVIEW_REQUIRED",
202
+ },
203
+ remainingChecks,
204
+ autoDelete: false,
159
205
  reviewAction: "Verify package scripts, manifests, framework discovery, dynamic loading and external consumers; never auto-delete.",
160
206
  };
161
207
  }
@@ -247,6 +293,11 @@ export function computeDeadCodeReview(graph, sources, options = {}) {
247
293
  medium: candidates.filter((candidate) => candidate.confidence === "medium").length,
248
294
  low: candidates.filter((candidate) => candidate.confidence === "low").length,
249
295
  },
296
+ byEvidenceTier: {
297
+ strongStatic: candidates.filter((candidate) => candidate.evidenceTier === "STRONG_STATIC_EVIDENCE").length,
298
+ boundedStatic: candidates.filter((candidate) => candidate.evidenceTier === "BOUNDED_STATIC_EVIDENCE").length,
299
+ highUncertainty: candidates.filter((candidate) => candidate.evidenceTier === "HIGH_UNCERTAINTY").length,
300
+ },
250
301
  },
251
302
  policy: {
252
303
  verdict: "REVIEW_REQUIRED",
@@ -167,11 +167,24 @@ export function computeDepFindings({
167
167
  severity: dev ? "info" : "low",
168
168
  confidence: dev || ecosystem ? "low" : "medium",
169
169
  title: `Unused ${section === "dependencies" ? "dependency" : section.replace(/ies$/, "y")}: ${name}`,
170
- reason: "No recorded package import, package-script command, recognized config mention, framework peer contract, or implicit style-compiler input uses this declaration.",
171
- detail: `"${name}" is declared in ${section} but never imported in source, never referenced by a script, and not mentioned in any known config file. Dynamic/config-convention usage can't be fully ruled out — review before removing.`,
170
+ reason: "No indexed package import, package-script command, recognized config mention, framework peer contract, or implicit style-compiler input uses this declaration.",
171
+ detail: `"${name}" is declared in ${section}, but no usage was found in the indexed source scope, package scripts, or ${configTexts.size} known config file(s). Dynamic/plugin/config-convention usage is not proven absent — review before removing.`,
172
172
  package: name,
173
173
  source: "internal",
174
- fixHint: `npm uninstall ${name} (after confirming no config/CLI usage)`,
174
+ actionability: "MANIFEST_REVIEW_REQUIRED",
175
+ autoRemove: false,
176
+ verification: {
177
+ evidenceModel: "MANIFEST_PLUS_INDEXED_SOURCE",
178
+ manifestDeclaration: { status: "FOUND", file: manifest, section, version: String(deps[name] || "") },
179
+ indexedSourceImports: { status: "ZERO_FOUND", completeness: "COMPLETE_FOR_GRAPH_SCOPE", count: 0 },
180
+ packageScripts: { status: "CHECKED", matched: false },
181
+ knownConfigs: { status: "CHECKED", filesScanned: configTexts.size, matched: false },
182
+ frameworkPeerContract: "NOT_FOUND",
183
+ implicitCompilerInput: "NOT_FOUND",
184
+ dynamicOrPluginUsage: "NOT_PROVEN_ABSENT",
185
+ decision: "REVIEW_REQUIRED",
186
+ },
187
+ fixHint: `Review manifest/config/plugin usage; if confirmed unused, remove ${name} with the package manager and run targeted tests`,
175
188
  ...meta,
176
189
  }));
177
190
  }
@@ -202,6 +215,25 @@ export function computeDepFindings({
202
215
  line: use.lines.get(files[0]) || 0,
203
216
  evidence: files.slice(0, 5).map((f) => ({ file: f, line: use.lines.get(f) || 0, snippet: "" })),
204
217
  source: "internal",
218
+ actionability: "STRONG_MANIFEST_MISMATCH",
219
+ autoInstall: false,
220
+ verification: {
221
+ evidenceModel: "MANIFEST_PLUS_INDEXED_SOURCE",
222
+ manifestDeclaration: { status: "NOT_FOUND", file: manifest, checkedSections: Object.keys(sections) },
223
+ indexedSourceImports: {
224
+ status: "FOUND",
225
+ completeness: "COMPLETE_FOR_GRAPH_SCOPE",
226
+ count: files.length,
227
+ files: files.slice(0, 20),
228
+ truncated: files.length > 20,
229
+ typeOnly: use.typeOnly,
230
+ stylesheetOnly,
231
+ testOnly,
232
+ },
233
+ packageScope: meta.scope,
234
+ transitiveResolutionRisk: "PRESENT",
235
+ decision: "DECLARE_AFTER_SCOPE_REVIEW",
236
+ },
205
237
  fixHint: `npm install ${testOnly ? "--save-dev " : ""}${name}`,
206
238
  ...meta,
207
239
  }));
@@ -223,6 +255,13 @@ export function computeDepFindings({
223
255
  detail: `"${name}" is declared in ${ss.join(" + ")} — npm resolves one of them; keep a single section.`,
224
256
  package: name,
225
257
  source: "internal",
258
+ actionability: "MANIFEST_REVIEW_REQUIRED",
259
+ autoRemove: false,
260
+ verification: {
261
+ evidenceModel: "MANIFEST_ONLY",
262
+ manifestDeclaration: { status: "DUPLICATE", file: manifest, sections: ss },
263
+ decision: "REVIEW_REQUIRED",
264
+ },
226
265
  ...meta,
227
266
  }));
228
267
  }
@@ -0,0 +1,84 @@
1
+ import {createHash} from 'node:crypto'
2
+ import {writeFileSync} from 'node:fs'
3
+ import {join} from 'node:path'
4
+ import {computeDuplicates} from './duplicates.js'
5
+ import {withGitRefCheckout} from './git-ref-graph.js'
6
+ import {buildInternalGraph} from '../graph/internal-builder.js'
7
+ import {filterGraphForMode} from '../graph/graph-filter.js'
8
+
9
+ const NON_PRODUCT = new Set(['generated', 'mock', 'story', 'docs', 'benchmark', 'temp'])
10
+ const eligible = (fragment, settings) => {
11
+ const classes = new Set(fragment.classes || [])
12
+ if (fragment.n < settings.tokMin) return false
13
+ if (settings.skipTests && (fragment.test || classes.has('test') || classes.has('e2e'))) return false
14
+ return settings.includeClassified || !(fragment.excluded || [...classes].some((name) => NON_PRODUCT.has(name)))
15
+ }
16
+
17
+ function groupPairs(data, settings) {
18
+ const fragments = data.frags || []
19
+ const pairs = (data.modes?.[settings.mode] || []).filter(([a, b, similarity]) => similarity >= settings.simMin && eligible(fragments[a], settings) && eligible(fragments[b], settings))
20
+ const parent = new Map()
21
+ const find = (value) => { let root = value; while (parent.get(root) !== root) root = parent.get(root); return root }
22
+ for (const [a, b] of pairs) {
23
+ if (!parent.has(a)) parent.set(a, a)
24
+ if (!parent.has(b)) parent.set(b, b)
25
+ parent.set(find(a), find(b))
26
+ }
27
+ const groups = new Map()
28
+ for (const [a, b, similarity] of pairs) {
29
+ const root = find(a)
30
+ if (!groups.has(root)) groups.set(root, {members: new Set(), maxSim: 0})
31
+ const group = groups.get(root)
32
+ group.members.add(a); group.members.add(b); group.maxSim = Math.max(group.maxSim, similarity)
33
+ }
34
+ return [...groups.values()].map((group) => {
35
+ const members = [...group.members].map((index) => fragments[index]).sort((a, b) => b.n - a.n)
36
+ return {members, maxSim: group.maxSim, tokens: members.reduce((sum, item) => sum + item.n, 0)}
37
+ }).sort((a, b) => b.tokens - a.tokens)
38
+ }
39
+
40
+ export function analyzeDuplicateGroups(repoRoot, graphPath, args = {}) {
41
+ const settings = {
42
+ simMin: Math.min(100, Math.max(50, Number(args.min_similarity) || 80)),
43
+ tokMin: Math.min(400, Math.max(12, Number(args.min_tokens) || 50)),
44
+ mode: args.mode === 'strict' ? 'strict' : 'renamed',
45
+ skipTests: args.include_tests !== true,
46
+ includeClassified: args.include_classified === true || args.include_non_product === true,
47
+ }
48
+ const data = computeDuplicates(repoRoot, graphPath, {includeStrings: args.include_strings === true, minTokens: settings.tokMin})
49
+ const groups = groupPairs(data, settings)
50
+ const suppressed = (data.frags || []).filter((fragment) => !eligible(fragment, settings)).length
51
+ return {settings, groups, suppressed}
52
+ }
53
+
54
+ const digest = (value) => createHash('sha256').update(value).digest('hex').slice(0, 20)
55
+ function groupKey(group, mode) {
56
+ const members = group.members.map((fragment) => {
57
+ const fingerprints = Array.isArray(fragment.fp?.[mode]) ? [...fragment.fp[mode]].sort() : []
58
+ return `${String(fragment.file).replace(/\\/g, '/')}\0${fragment.label || ''}\0${digest(JSON.stringify(fingerprints))}`
59
+ }).sort()
60
+ return digest(members.join('\n'))
61
+ }
62
+
63
+ export async function compareDuplicateGroups({repoRoot, graphPath, currentGraph, baseRef, changedFiles = [], args = {}}) {
64
+ const current = analyzeDuplicateGroups(repoRoot, graphPath, args)
65
+ const mode = ['full', 'no-tests', 'tests-only'].includes(currentGraph?.graphBuildMode) ? currentGraph.graphBuildMode : 'full'
66
+ const baseline = await withGitRefCheckout(repoRoot, baseRef, async (checkout) => {
67
+ let graph = await buildInternalGraph(checkout)
68
+ if (mode !== 'full') graph = filterGraphForMode(graph, mode, {repoRoot: checkout})
69
+ graph.graphBuildMode = mode
70
+ const baselineGraphPath = join(checkout, '.weavatrix-verified-graph.json')
71
+ writeFileSync(baselineGraphPath, JSON.stringify(graph))
72
+ return analyzeDuplicateGroups(checkout, baselineGraphPath, args)
73
+ })
74
+ if (!baseline.ok) return {state: 'UNKNOWN', reason: baseline.error, currentGroups: current.groups.length}
75
+ const baselineKeys = new Set(baseline.value.groups.map((group) => groupKey(group, current.settings.mode)))
76
+ const changed = new Set(changedFiles.map((file) => String(file).replace(/\\/g, '/')))
77
+ const added = current.groups.filter((group) => !baselineKeys.has(groupKey(group, current.settings.mode)))
78
+ const scoped = added.filter((group) => group.members.some((member) => changed.has(String(member.file).replace(/\\/g, '/'))))
79
+ return {
80
+ state: scoped.length ? 'BLOCKED' : 'PASS', baseline: {ref: baseline.ref, commit: baseline.commit},
81
+ currentGroups: current.groups.length, baselineGroups: baseline.value.groups.length, newGroups: added.length,
82
+ scopedNewGroups: scoped.slice(0, 20).map((group) => ({similarity: group.maxSim, tokens: group.tokens, members: group.members.slice(0, 8).map((item) => ({file: item.file, start: item.start, end: item.end, label: item.label}))})),
83
+ }
84
+ }
@@ -109,8 +109,9 @@ export function computeHotPathReview(graph, options = {}) {
109
109
  calls: boundedInt(options.callThreshold, 12, 1, 10000),
110
110
  loopDepth: boundedInt(options.loopDepthThreshold, 2, 1, 10),
111
111
  timeRank: boundedInt(options.timeRankThreshold, 2, 0, 5),
112
- minScore: boundedInt(options.minScore, 0, 0, 100),
112
+ minScore: boundedInt(options.minScore, 85, 0, 100),
113
113
  }
114
+ const defaultFocus = options.minScore == null
114
115
  const topN = boundedInt(options.topN, 20, 1, 100)
115
116
  const classifier = createPathClassifier(options.repoRoot || null)
116
117
  const knownFiles = [...new Set((graph?.nodes || []).map((node) => normalize(node?.source_file)).filter(Boolean))]
@@ -152,8 +153,19 @@ export function computeHotPathReview(graph, options = {}) {
152
153
  const nearest = staticByFile.get(file)?.nearestTests?.[0] || null
153
154
  const coverageRisk = actualCoverage == null ? 0 : (1 - actualCoverage) * 100
154
155
  const score = round(clamp(syntaxScore * 0.72 + graphScore * 0.2 + coverageRisk * 0.08))
155
- if (score < thresholds.minScore) continue
156
156
  const directHotEvidence = Array.isArray(complexity.hotEvidence) ? complexity.hotEvidence.slice(0, 12) : []
157
+ // The default queue is intentionally narrow. A small, locally expensive function can still be
158
+ // important even with little graph fan-in, so retain only a bounded strong-local fallback. An
159
+ // explicit minScore disables this fallback and gives the caller a strict numeric gate.
160
+ const bodyLines = startLine > 0 && endLine >= startLine ? endLine - startLine + 1 : Number.POSITIVE_INFINITY
161
+ const strongLocalEvidence = defaultFocus && directHotEvidence.length > 0 && bodyLines <= 80 && (
162
+ Number(complexity.recursionInLoops || 0) > 0
163
+ || (bodyLines <= 40 && (
164
+ Number(complexity.sortsInLoops || 0) > 0
165
+ || (Number(complexity.timeRank || 0) >= 4 && Number(complexity.maxLoopDepth || 0) >= 2)
166
+ ))
167
+ )
168
+ if (score < thresholds.minScore && !strongLocalEvidence) continue
157
169
  const confidence = directHotEvidence.length ? 'HIGH' : complexity.recursion ? 'LOW' : 'MEDIUM'
158
170
  candidates.push({
159
171
  id: String(node.id),
@@ -164,6 +176,7 @@ export function computeHotPathReview(graph, options = {}) {
164
176
  endLine,
165
177
  classification: entry.classification,
166
178
  score,
179
+ selection: score >= thresholds.minScore ? 'SCORE_THRESHOLD' : 'STRONG_LOCAL_EVIDENCE',
167
180
  confidence,
168
181
  localSyntax: {
169
182
  score: syntaxScore,
@@ -207,6 +220,11 @@ export function computeHotPathReview(graph, options = {}) {
207
220
  includeClassified: options.includeClassified === true,
208
221
  },
209
222
  thresholds,
223
+ selectionPolicy: {
224
+ mode: defaultFocus ? 'FOCUSED_DEFAULT' : 'EXPLICIT_SCORE_THRESHOLD',
225
+ strongLocalFallback: defaultFocus,
226
+ broadenWith: 'Set min_score lower (0 restores the full diagnostic candidate set).',
227
+ },
210
228
  analyzedSymbols: eligible.length,
211
229
  candidateSymbols: candidates.length,
212
230
  coverage: measuredCoverage.size
@@ -300,15 +300,20 @@ export async function runInternalAudit(repoPath, { graph, advisoryStorePath, ski
300
300
  findings: sorted,
301
301
  dependencyReport: {
302
302
  status: dependencyStatus,
303
+ evidenceModel: "MANIFEST_PLUS_INDEXED_SOURCE",
304
+ perFindingVerification: true,
305
+ verificationCoverage: { npm: "COMPLETE_FOR_GRAPH_SCOPE", go: "SUMMARY_ONLY", python: "SUMMARY_ONLY" },
303
306
  declared: dep.declared.size + goDep.declared.size + pyDep.declared.size,
304
307
  importedPackages: importedPackages.size,
305
308
  importRecords: externalImports.length,
306
309
  unused: dependencyFindings.filter((finding) => finding.rule === "unused-dep").length,
307
310
  missing: dependencyFindings.filter((finding) => finding.rule === "missing-dep").length,
308
311
  duplicateDeclarations: dependencyFindings.filter((finding) => finding.rule === "duplicate-dep").length,
312
+ unusedRequiringReview: dependencyFindings.filter((finding) => finding.rule === "unused-dep" && finding.verification?.decision === "REVIEW_REQUIRED").length,
313
+ missingWithSourceEvidence: dependencyFindings.filter((finding) => finding.rule === "missing-dep" && finding.verification?.indexedSourceImports?.status === "FOUND").length,
309
314
  packageScopes: packageScopes.length,
310
315
  reason: dependencyStatus === "COMPLETE"
311
- ? "All discovered dependency manifests were compared with the complete indexed import set."
316
+ ? "All discovered dependency manifests were compared with the complete indexed import set; every npm unused/missing/duplicate finding carries manifest and source/config verification state."
312
317
  : "The dependency result is scoped to a partial graph and is not a repository-wide clean bill.",
313
318
  },
314
319
  deadReport: {
@@ -0,0 +1,116 @@
1
+ import {createPathClassifier, hasPathClass} from '../path-classification.js'
2
+
3
+ const words = (value) => new Set(String(value || '').toLowerCase().match(/[\p{L}_$][\p{L}\p{N}_$-]{2,}/gu) || [])
4
+ const fileOf = (node) => String(node?.source_file || (String(node?.id || '').includes('#') ? String(node.id).split('#')[0] : node?.id || '')).replace(/\\/g, '/')
5
+ const isSymbol = (id) => String(id || '').includes('#')
6
+ const NON_PRODUCT_CLASSES = Object.freeze(['test', 'e2e', 'generated', 'mock', 'story', 'docs', 'benchmark', 'temp'])
7
+ const CLASS_TERMS = Object.freeze({
8
+ test: ['test', 'tests', 'testing', 'spec', 'coverage', 'verify'],
9
+ e2e: ['e2e', 'playwright', 'cypress'],
10
+ generated: ['generated', 'autogenerated', 'dist'],
11
+ mock: ['mock', 'mocks', 'fixture', 'fixtures', 'fake'],
12
+ story: ['story', 'stories', 'storybook'],
13
+ docs: ['doc', 'docs', 'documentation', 'readme', 'guide'],
14
+ benchmark: ['benchmark', 'benchmarks', 'bench'],
15
+ temp: ['temp', 'temporary', 'tmp'],
16
+ })
17
+
18
+ const INTENT_TRANSLATIONS = [
19
+ [/авториз|аутентиф|логин|сесси|токен/iu, 'auth authentication login session token'],
20
+ [/маршрут|роут|эндпоинт|апи|http/iu, 'route router endpoint api http'],
21
+ [/тест|покрыти/iu, 'test spec coverage verify'],
22
+ [/к[эе]ш|хранилищ/iu, 'cache store storage'],
23
+ [/баз[аы]|запрос|sql/iu, 'database query sql'],
24
+ [/конфиг|настройк/iu, 'config settings configuration'],
25
+ [/безопас|вредонос|секрет/iu, 'security malware secret'],
26
+ [/зависим|импорт/iu, 'dependency import module'],
27
+ [/дублик|клон/iu, 'duplicate clone'],
28
+ ]
29
+
30
+ export function expandTaskQuery(task) {
31
+ const text = String(task || '')
32
+ const expansions = INTENT_TRANSLATIONS.filter(([pattern]) => pattern.test(text)).map(([, value]) => value)
33
+ return [...new Set([text, ...expansions])].join(' ')
34
+ }
35
+
36
+ function overlapScore(taskWords, node) {
37
+ const haystack = words(`${node?.label || ''} ${node?.id || ''} ${node?.symbol_kind || ''}`)
38
+ let score = 0
39
+ for (const word of taskWords) if (haystack.has(word)) score += 8
40
+ return score
41
+ }
42
+
43
+ function addCandidate(map, node, score, reason) {
44
+ if (!node?.id) return
45
+ const id = String(node.id)
46
+ const current = map.get(id) || {node, score: 0, reasons: new Set()}
47
+ current.score += score
48
+ current.reasons.add(reason)
49
+ map.set(id, current)
50
+ }
51
+
52
+ function containedSymbols(g, node) {
53
+ if (isSymbol(node?.id)) return [node]
54
+ return (g.out.get(String(node?.id)) || [])
55
+ .filter((edge) => edge.relation === 'contains')
56
+ .map((edge) => g.byId.get(String(edge.id)))
57
+ .filter(Boolean)
58
+ }
59
+
60
+ // Combines intent-expanded search seeds with exact changed symbols. The result deliberately stays
61
+ // deterministic and source-free; exact LSP/source evidence is collected by context_bundle later.
62
+ export function retrieveTaskContext(g, {
63
+ task, semanticSeeds = [], changedSeedIds = [], maxSymbols = 3, repoRoot = null, includeClassified = false,
64
+ } = {}) {
65
+ const expandedTask = expandTaskQuery(task)
66
+ const taskWords = words(expandedTask)
67
+ const requestedClasses = new Set(Object.entries(CLASS_TERMS)
68
+ .filter(([, terms]) => terms.some((term) => taskWords.has(term)))
69
+ .map(([name]) => name))
70
+ if (requestedClasses.has('test')) requestedClasses.add('e2e')
71
+ if (requestedClasses.has('e2e')) requestedClasses.add('test')
72
+ const classifier = createPathClassifier(repoRoot)
73
+ const classificationCache = new Map()
74
+ const changedFiles = new Set((changedSeedIds || []).map((id) => fileOf(g.byId.get(String(id)) || {id})))
75
+ const pathAllowed = (node) => {
76
+ const file = fileOf(node)
77
+ if (!file || changedFiles.has(file) || includeClassified === true) return true
78
+ if (!classificationCache.has(file)) classificationCache.set(file, classifier.explain(file, {content: ''}))
79
+ const info = classificationCache.get(file)
80
+ const classes = NON_PRODUCT_CLASSES.filter((name) => hasPathClass(info, name))
81
+ if (!info?.excluded && !classes.length) return true
82
+ return classes.some((name) => requestedClasses.has(name))
83
+ }
84
+ const candidates = new Map()
85
+ for (const id of changedSeedIds || []) {
86
+ const node = g.byId.get(String(id))
87
+ if (node) addCandidate(candidates, node, 100 + overlapScore(taskWords, node), 'changed-symbol')
88
+ }
89
+ for (const seed of semanticSeeds || []) addCandidate(candidates, seed, 45 + overlapScore(taskWords, seed), 'task-intent')
90
+
91
+ for (const candidate of [...candidates.values()]) {
92
+ for (const symbol of containedSymbols(g, candidate.node)) {
93
+ const degree = (g.out.get(String(symbol.id)) || []).length + (g.inn.get(String(symbol.id)) || []).length
94
+ addCandidate(candidates, symbol, 22 + overlapScore(taskWords, symbol) + Math.min(12, degree), `symbol-in:${fileOf(candidate.node)}`)
95
+ }
96
+ }
97
+
98
+ const symbolCandidates = [...candidates.values()].filter((item) => isSymbol(item.node.id))
99
+ const ranked = symbolCandidates
100
+ .filter((item) => pathAllowed(item.node))
101
+ .sort((left, right) => right.score - left.score || String(left.node.id).localeCompare(String(right.node.id)))
102
+ const limit = Math.max(1, Math.min(5, Number(maxSymbols) || 3))
103
+ return {
104
+ method: 'intent-expanded graph retrieval + exact changed-symbol seeds',
105
+ status: ranked.length ? 'COMPLETE' : 'NO_SYMBOLS',
106
+ selected: ranked.slice(0, limit).map((item) => ({
107
+ id: String(item.node.id), label: item.node.label || String(item.node.id), file: fileOf(item.node),
108
+ kind: item.node.symbol_kind || null, score: item.score, reasons: [...item.reasons].sort(),
109
+ })),
110
+ candidateCount: ranked.length,
111
+ suppressedClassified: symbolCandidates.length - ranked.length,
112
+ pathPolicy: includeClassified === true
113
+ ? {mode: 'ALL_CLASSIFIED'}
114
+ : {mode: 'PRODUCTION_FIRST', requestedClasses: [...requestedClasses].sort(), changedFilesPinned: changedFiles.size},
115
+ }
116
+ }