weavatrix 0.2.7 → 0.2.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +101 -27
- package/SECURITY.md +30 -7
- package/docs/releases/v0.2.9.md +123 -0
- package/package.json +3 -3
- package/skill/SKILL.md +12 -7
- package/src/analysis/dead-check.js +24 -2
- package/src/analysis/dep-check-ecosystems.js +41 -2
- package/src/analysis/duplicate-groups.js +11 -2
- package/src/analysis/endpoints.js +256 -22
- package/src/analysis/internal-audit.collect.js +53 -20
- package/src/graph/builder/lang-rust.js +49 -12
- package/src/mcp/catalog.mjs +13 -10
- package/src/mcp/evidence-snapshot.package-graph.mjs +257 -3
- package/src/mcp/graph-context.mjs +72 -2
- package/src/mcp/tools-actions.mjs +170 -36
- package/src/mcp/tools-context.mjs +25 -8
- package/src/mcp/tools-endpoints.mjs +162 -0
- package/src/mcp/tools-graph.mjs +1 -1
- package/src/mcp/tools-health.mjs +92 -42
- package/src/mcp/tools-source.mjs +3 -3
- package/src/mcp/tools-verified-change.mjs +16 -0
- package/src/mcp-server.mjs +30 -2
- package/src/mcp-source-tools.mjs +10 -7
- package/src/path-classification.js +1 -1
- package/src/precision/symbol-query.js +51 -0
- package/docs/releases/v0.2.7.md +0 -93
package/src/mcp/tools-health.mjs
CHANGED
|
@@ -9,7 +9,7 @@ import {classifyChangeImpact} from '../analysis/change-classification.js'
|
|
|
9
9
|
import {compareAuditDebt, normalizeAuditScopeFiles, scopeAuditFindings} from '../analysis/audit-debt.js'
|
|
10
10
|
import {summarizeFindings} from '../analysis/findings.js'
|
|
11
11
|
import {summarizeCommunities, aggregateGraph} from '../analysis/graph-analysis.js'
|
|
12
|
-
import {
|
|
12
|
+
import {analyzeEndpointInventory} from '../analysis/endpoints.js'
|
|
13
13
|
import {computeStaticTestReachability} from '../analysis/static-test-reachability.js'
|
|
14
14
|
import {computeDeadCodeReview} from '../analysis/dead-code-review.js'
|
|
15
15
|
import {computeHotPathReview} from '../analysis/hot-path-review.js'
|
|
@@ -22,6 +22,7 @@ import {toolResult} from './tool-result.mjs'
|
|
|
22
22
|
import {createPathClassifier, hasPathClass} from '../path-classification.js'
|
|
23
23
|
import {createRepoBoundary} from '../repo-path.js'
|
|
24
24
|
import {filterGraphForMode} from '../graph/graph-filter.js'
|
|
25
|
+
import {readCachedSymbolPrecisionEvidence} from '../precision/symbol-query.js'
|
|
25
26
|
|
|
26
27
|
const NON_PRODUCT_DUPLICATE_CLASSES = new Set(['generated', 'mock', 'story', 'docs', 'benchmark', 'temp'])
|
|
27
28
|
const fragmentEligible = (fragment, {tokMin, skipTests, includeClassified}) => {
|
|
@@ -85,7 +86,10 @@ export function tFindDuplicates(g, args, ctx) {
|
|
|
85
86
|
const suppressionNote = suppressed && !includeClassified
|
|
86
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.`
|
|
87
88
|
: ''
|
|
88
|
-
|
|
89
|
+
const boilerplateNote = analysis.boilerplateSuppressed
|
|
90
|
+
? ` ${analysis.boilerplateSuppressed} all-router framework boilerplate group(s) were suppressed; pass include_boilerplate:true to inspect them.`
|
|
91
|
+
: ''
|
|
92
|
+
if (!groups.length) return `No clones at ≥${simMin}% similarity / ≥${tokMin} tokens (${mode} mode). Try lowering the thresholds.${smallPolicy}${suppressionNote}${boilerplateNote}`
|
|
89
93
|
const top = groups.slice(0, Math.min(30, Math.max(1, Number(args.top_n) || 15)))
|
|
90
94
|
const lines = top.map((grp, k) => {
|
|
91
95
|
const isStr = grp.members.some((f) => f.kind === 'string')
|
|
@@ -93,7 +97,7 @@ export function tFindDuplicates(g, args, ctx) {
|
|
|
93
97
|
const sites = grp.members.slice(0, 8).map((f) => ` ${f.file}:${f.start}-${f.end}`)
|
|
94
98
|
return [head, ...sites].join('\n')
|
|
95
99
|
})
|
|
96
|
-
return `Found ${groups.length} clone group(s) (${mode} mode, ≥${simMin}%, ≥${tokMin} tok${includeStrings ? ', incl. large string literals' : ''}). Top ${top.length}:\n\n${lines.join('\n\n')}\n\nUse read_source on any two sites to compare, then extract shared logic.${smallPolicy}${suppressionNote}`
|
|
100
|
+
return `Found ${groups.length} clone group(s) (${mode} mode, ≥${simMin}%, ≥${tokMin} tok${includeStrings ? ', incl. large string literals' : ''}). Top ${top.length}:\n\n${lines.join('\n\n')}\n\nUse read_source on any two sites to compare, then extract shared logic.${smallPolicy}${suppressionNote}${boilerplateNote}`
|
|
97
101
|
}
|
|
98
102
|
|
|
99
103
|
// Focused dead-code review queue. Unlike the broad run_audit surface, this includes functions and
|
|
@@ -101,7 +105,27 @@ export function tFindDuplicates(g, args, ctx) {
|
|
|
101
105
|
// candidates. It never returns an automatic-delete verdict.
|
|
102
106
|
export function tFindDeadCode(g, args, ctx) {
|
|
103
107
|
if (!ctx.repoRoot) return 'Dead-code review needs the repo root (not provided to this server).'
|
|
104
|
-
const
|
|
108
|
+
const effectiveGraph = effectiveRawGraph(ctx)
|
|
109
|
+
const pointEvidence = readCachedSymbolPrecisionEvidence({repoRoot: ctx.repoRoot, graphPath: ctx.graphPath, graph: effectiveGraph})
|
|
110
|
+
const graph = {
|
|
111
|
+
...effectiveGraph,
|
|
112
|
+
precisionReferenceSymbols: [...new Set([
|
|
113
|
+
...(effectiveGraph.precisionReferenceSymbols || []),
|
|
114
|
+
...pointEvidence.referenceSymbols,
|
|
115
|
+
])],
|
|
116
|
+
precisionProductionReferenceSymbols: [...new Set([
|
|
117
|
+
...(effectiveGraph.precisionProductionReferenceSymbols || []),
|
|
118
|
+
...pointEvidence.productionReferenceSymbols,
|
|
119
|
+
])],
|
|
120
|
+
precisionTestReferenceSymbols: [...new Set([
|
|
121
|
+
...(effectiveGraph.precisionTestReferenceSymbols || []),
|
|
122
|
+
...pointEvidence.testReferenceSymbols,
|
|
123
|
+
])],
|
|
124
|
+
precisionNoReferenceSymbols: [...new Set([
|
|
125
|
+
...(effectiveGraph.precisionNoReferenceSymbols || []),
|
|
126
|
+
...pointEvidence.noReferenceSymbols,
|
|
127
|
+
])],
|
|
128
|
+
}
|
|
105
129
|
const boundary = createRepoBoundary(ctx.repoRoot)
|
|
106
130
|
const pkg = readRepoJson(boundary, 'package.json') || {}
|
|
107
131
|
const rules = readRepoJson(boundary, '.weavatrix-deps.json') || {}
|
|
@@ -176,6 +200,7 @@ export function tFindDeadCode(g, args, ctx) {
|
|
|
176
200
|
}
|
|
177
201
|
|
|
178
202
|
const SEVERITY_RANK = {critical: 0, high: 1, medium: 2, low: 3, info: 4}
|
|
203
|
+
const AUDIT_NON_PRODUCT_CLASSES = ['test', 'e2e', 'generated', 'mock', 'story', 'docs', 'benchmark', 'temp']
|
|
179
204
|
|
|
180
205
|
export function formatAuditFinding(f) {
|
|
181
206
|
const where = f.file ? ` (${f.file}${f.symbol ? ` ${f.symbol}` : ''})` : f.package ? ` (pkg ${f.package}${f.version ? `@${f.version}` : ''}${f.manifest ? `; ${f.manifest}` : ''})` : ''
|
|
@@ -185,12 +210,40 @@ export function formatAuditFinding(f) {
|
|
|
185
210
|
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}` : ''}`
|
|
186
211
|
}
|
|
187
212
|
|
|
188
|
-
const
|
|
213
|
+
const auditFindingPaths = (finding) => {
|
|
214
|
+
const paths = []
|
|
215
|
+
if (finding?.file) paths.push(String(finding.file))
|
|
216
|
+
if (Array.isArray(finding?.files)) paths.push(...finding.files.map(String))
|
|
217
|
+
if (finding?.cycleRoute) paths.push(...String(finding.cycleRoute).split(/\s*(?:→|→|->)\s*/))
|
|
218
|
+
return [...new Set(paths.map((file) => file.replace(/\\/g, '/').trim()).filter(Boolean))]
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export function auditFindingPathScope(findings, {includeClassified = false, repoRoot = null} = {}) {
|
|
222
|
+
const all = Array.isArray(findings) ? findings : []
|
|
223
|
+
if (includeClassified) return {findings: all, suppressed: 0}
|
|
224
|
+
const classifier = createPathClassifier(repoRoot)
|
|
225
|
+
const cache = new Map()
|
|
226
|
+
const classified = (file) => {
|
|
227
|
+
if (!cache.has(file)) cache.set(file, classifier.explain(file, {content: ''}))
|
|
228
|
+
const info = cache.get(file)
|
|
229
|
+
return info?.excluded || AUDIT_NON_PRODUCT_CLASSES.some((name) => hasPathClass(info, name))
|
|
230
|
+
}
|
|
231
|
+
const kept = all.filter((finding) => {
|
|
232
|
+
const paths = auditFindingPaths(finding)
|
|
233
|
+
return !paths.length || paths.some((file) => !classified(file))
|
|
234
|
+
})
|
|
235
|
+
return {findings: kept, suppressed: all.length - kept.length}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const DEPENDENCY_AUDIT_RULES = new Set(['unused-dep', 'missing-dep', 'duplicate-dep', 'unresolved-import', 'lockfile-drift'])
|
|
239
|
+
export const isDependencyAuditFinding = (finding) => DEPENDENCY_AUDIT_RULES.has(String(finding?.rule || ''))
|
|
240
|
+
|
|
241
|
+
const auditFilter = (audit, args, findings = audit.findings, repoRoot = null) => {
|
|
189
242
|
const minSev = SEVERITY_RANK[args.min_severity] ?? 4
|
|
190
243
|
const category = args.category ? String(args.category) : null
|
|
191
|
-
return findings
|
|
244
|
+
return auditFindingPathScope(findings, {includeClassified: args.include_classified === true, repoRoot}).findings
|
|
192
245
|
.filter((finding) => (SEVERITY_RANK[finding.severity] ?? 4) <= minSev)
|
|
193
|
-
.filter((finding) => !category || finding.category === category)
|
|
246
|
+
.filter((finding) => !category || (category === 'dependencies' ? isDependencyAuditFinding(finding) : finding.category === category))
|
|
194
247
|
}
|
|
195
248
|
|
|
196
249
|
const auditChecksLine = (audit) => {
|
|
@@ -208,11 +261,12 @@ const auditConventionLines = (audit) => {
|
|
|
208
261
|
].filter((line) => line != null)
|
|
209
262
|
}
|
|
210
263
|
|
|
211
|
-
const formatOrdinaryAudit = (audit, args, findings = audit.findings, heading = null) => {
|
|
264
|
+
const formatOrdinaryAudit = (audit, args, findings = audit.findings, heading = null, repoRoot = null) => {
|
|
212
265
|
const max = Math.max(1, Math.min(100, Number(args.max_findings) || 30))
|
|
213
|
-
const
|
|
266
|
+
const pathScope = auditFindingPathScope(findings, {includeClassified: args.include_classified === true, repoRoot})
|
|
267
|
+
const filtered = auditFilter(audit, args, pathScope.findings, repoRoot)
|
|
214
268
|
const shown = filtered.slice(0, max)
|
|
215
|
-
const summary = summarizeFindings(findings)
|
|
269
|
+
const summary = summarizeFindings(pathScope.findings)
|
|
216
270
|
const sev = summary.bySeverity
|
|
217
271
|
const bycat = summary.byCategory
|
|
218
272
|
const deps = audit.dependencyReport || {}
|
|
@@ -220,8 +274,9 @@ const formatOrdinaryAudit = (audit, args, findings = audit.findings, heading = n
|
|
|
220
274
|
heading,
|
|
221
275
|
`Internal audit of ${audit.repo} (${audit.scanned.files} files, ${audit.scanned.symbols} symbols, ${audit.scanned.externalImports} external imports; malware scan: ${audit.scanned.malwareScanMode}).`,
|
|
222
276
|
...auditConventionLines(audit),
|
|
223
|
-
`Dependency manifests: ${deps.status || 'UNKNOWN'}
|
|
277
|
+
`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'}.`,
|
|
224
278
|
`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}.`,
|
|
279
|
+
pathScope.suppressed ? `Path policy: production-first; suppressed ${pathScope.suppressed} finding(s) whose evidence is entirely test/e2e/generated/mock/story/docs/benchmark/temp or explicitly excluded. Pass include_classified:true to include them.` : 'Path policy: production-first; no classified-only findings were suppressed.',
|
|
225
280
|
`Repository-level ${auditChecksLine(audit)}`,
|
|
226
281
|
'',
|
|
227
282
|
`Showing ${shown.length} of ${filtered.length} finding(s)${args.category ? ` in category "${args.category}"` : ''}${args.min_severity ? ` at ≥${args.min_severity}` : ''}:`,
|
|
@@ -332,12 +387,12 @@ async function runAuditWithBaseline(args, ctx, currentGraph) {
|
|
|
332
387
|
const comparison = compareAuditDebt(currentAudit, baseline.value, changedFiles, {completeChangeSet})
|
|
333
388
|
const mode = ['new', 'existing', 'all'].includes(args.debt) ? args.debt : 'new'
|
|
334
389
|
const selectedRaw = mode === 'new' ? comparison.new : mode === 'existing' ? comparison.existing : comparison.all
|
|
335
|
-
const selected = auditFilter(currentAudit, args, selectedRaw)
|
|
390
|
+
const selected = auditFilter(currentAudit, args, selectedRaw, ctx.repoRoot)
|
|
336
391
|
const max = Math.max(1, Math.min(100, Number(args.max_findings) || 30))
|
|
337
392
|
const shown = selected.slice(0, max)
|
|
338
393
|
const optional = comparison.optional.checks.map((check) => `${check.name.toUpperCase()} UNCOMPARABLE (current ${check.current}; baseline ${check.baseline})`).join('; ')
|
|
339
394
|
const stateOf = (finding) => mode === 'all' ? finding.debtState : mode
|
|
340
|
-
const fixedShown = auditFilter(baseline.value, args, comparison.fixed).slice(0, Math.min(10, max))
|
|
395
|
+
const fixedShown = auditFilter(baseline.value, args, comparison.fixed, ctx.repoRoot).slice(0, Math.min(10, max))
|
|
341
396
|
const text = [
|
|
342
397
|
`${mode.toUpperCase()} DEBT — deterministic internal audit vs ${resolved.ref} (${resolved.commit.slice(0, 12)})`,
|
|
343
398
|
`Changed scope: ${changedFiles.length} file(s) from ${scopeSource}${changedFiles.length ? ` — ${changedFiles.slice(0, 12).join(', ')}${changedFiles.length > 12 ? ', …' : ''}` : ' — working tree matches the baseline'}.`,
|
|
@@ -390,38 +445,16 @@ export async function tRunAudit(g, args, ctx) {
|
|
|
390
445
|
if (!normalized.ok) return `Changed-scope audit invalid: ${normalized.error}.`
|
|
391
446
|
const scoped = scopeAuditFindings(audit.findings, normalized.files)
|
|
392
447
|
const text = formatOrdinaryAudit(audit, args, scoped,
|
|
393
|
-
`CHANGED-SCOPE ONLY — ${normalized.files.length} explicitly supplied file(s); no baseline was provided, so these findings are not classified as new, existing, or fixed
|
|
448
|
+
`CHANGED-SCOPE ONLY — ${normalized.files.length} explicitly supplied file(s); no baseline was provided, so these findings are not classified as new, existing, or fixed.`, ctx.repoRoot)
|
|
394
449
|
return toolResult(text, {
|
|
395
450
|
status: 'COMPLETE',
|
|
396
451
|
mode: 'changed-scope',
|
|
397
452
|
scope: {files: normalized.files, source: 'explicit changed_files'},
|
|
398
453
|
comparison: {status: 'UNAVAILABLE', reason: 'base_ref was not provided; changed-scope is not a new-debt claim'},
|
|
399
|
-
findings: auditFilter(audit, args, scoped),
|
|
454
|
+
findings: auditFilter(audit, args, scoped, ctx.repoRoot),
|
|
400
455
|
}, {completeness: {status: 'COMPLETE'}})
|
|
401
456
|
}
|
|
402
|
-
|
|
403
|
-
const cat = args.category ? String(args.category) : null
|
|
404
|
-
const max = Math.max(1, Math.min(100, Number(args.max_findings) || 30))
|
|
405
|
-
const filtered = audit.findings
|
|
406
|
-
.filter((f) => (SEVERITY_RANK[f.severity] ?? 4) <= minSev)
|
|
407
|
-
.filter((f) => !cat || f.category === cat)
|
|
408
|
-
const shown = filtered.slice(0, max)
|
|
409
|
-
const sev = audit.summary.bySeverity
|
|
410
|
-
const bycat = audit.summary.byCategory
|
|
411
|
-
const deps = audit.dependencyReport || {}
|
|
412
|
-
const check = (name, state) => `${name} ${state?.status || 'ERROR'}${state?.detail ? ` — ${state.detail}` : ''}`
|
|
413
|
-
return [
|
|
414
|
-
`Internal audit of ${audit.repo} (${audit.scanned.files} files, ${audit.scanned.symbols} symbols, ${audit.scanned.externalImports} external imports; malware scan: ${audit.scanned.malwareScanMode}).`,
|
|
415
|
-
`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}.`,
|
|
416
|
-
`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).`,
|
|
417
|
-
`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'}.`,
|
|
418
|
-
...auditConventionLines(audit),
|
|
419
|
-
`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.`,
|
|
420
|
-
``,
|
|
421
|
-
`Showing ${shown.length} of ${filtered.length} finding(s)${cat ? ` in category "${cat}"` : ''}${args.min_severity ? ` at ≥${args.min_severity}` : ''}:`,
|
|
422
|
-
...shown.map(formatAuditFinding),
|
|
423
|
-
filtered.length > shown.length ? ` … +${filtered.length - shown.length} more (raise max_findings or filter by category/min_severity)` : null,
|
|
424
|
-
].filter((x) => x != null).join('\n')
|
|
457
|
+
return formatOrdinaryAudit(audit, args, audit.findings, null, ctx.repoRoot)
|
|
425
458
|
}
|
|
426
459
|
|
|
427
460
|
// Named module clusters: graph communities labeled by their dominant folder instead of bare numbers.
|
|
@@ -644,12 +677,29 @@ export function tListEndpoints(g, args, ctx) {
|
|
|
644
677
|
.filter((n) => !String(n.id).includes('#') && n.source_file && n.file_type === 'code')
|
|
645
678
|
.map((n) => n.source_file)
|
|
646
679
|
)]
|
|
647
|
-
const
|
|
680
|
+
const inventory = analyzeEndpointInventory(ctx.repoRoot, codeFiles)
|
|
681
|
+
let eps = inventory.endpoints
|
|
682
|
+
const method = args.method ? String(args.method).toUpperCase() : null
|
|
683
|
+
const path = args.path ? String(args.path) : null
|
|
684
|
+
if (method) eps = eps.filter((endpoint) => endpoint.method === method)
|
|
685
|
+
if (path) eps = eps.filter((endpoint) => endpoint.path === path || endpoint.path.endsWith(path))
|
|
648
686
|
if (!eps.length) return 'No HTTP endpoints detected in the indexed code files.'
|
|
649
687
|
const max = Math.max(1, Math.min(300, Number(args.max_results) || 100))
|
|
650
688
|
const shown = eps.slice(0, max)
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
689
|
+
const stats = inventory.stats
|
|
690
|
+
const text = [
|
|
691
|
+
`${eps.length} endpoint(s) matched${eps.length > shown.length ? `, showing ${shown.length}` : ''}. Inventory: ${stats.declaredRoutes} declaration(s); ${stats.reachableStaticRoutes} statically reachable composed route(s); ${stats.localDeclarations} local/root declaration candidate(s); ${stats.staticMounts} static router mount(s)${stats.truncated ? `; TRUNCATED at ${stats.maxEndpoints}` : ''}.`,
|
|
692
|
+
...shown.map((e) => {
|
|
693
|
+
const via = e.mountChain?.length
|
|
694
|
+
? `\n declared ${e.declaredPath} in ${e.file}${e.line ? `:${e.line}` : ''}; mount chain ${e.mountChain.map((mount) => `${mount.file}:${mount.line} ${mount.path}`).join(' → ')}`
|
|
695
|
+
: ''
|
|
696
|
+
return ` ${e.method.toUpperCase().padEnd(6)} ${e.path}${e.handler ? ` → ${e.handler}` : ''} (${e.file}${e.line ? `:${e.line}` : ''}; ${e.mountState}/${e.confidence})${via}`
|
|
697
|
+
}),
|
|
654
698
|
].join('\n')
|
|
699
|
+
return toolResult(text, {
|
|
700
|
+
filters: {method, path},
|
|
701
|
+
stats,
|
|
702
|
+
endpoints: shown,
|
|
703
|
+
page: {shown: shown.length, total: eps.length, truncated: eps.length > shown.length || stats.truncated},
|
|
704
|
+
}, {completeness: {status: stats.truncated ? 'PARTIAL' : 'COMPLETE', reason: stats.truncated ? `endpoint cap ${stats.maxEndpoints} reached` : 'all indexed code files scanned'}})
|
|
655
705
|
}
|
package/src/mcp/tools-source.mjs
CHANGED
|
@@ -27,7 +27,7 @@ function candidateNodes(g, query, limit = 20) {
|
|
|
27
27
|
}))
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
function
|
|
30
|
+
export function sourceExcerpt(repoRoot, file, focusLine, contextLines) {
|
|
31
31
|
const resolved = resolveRepoPath(repoRoot, file)
|
|
32
32
|
if (!resolved.ok) return null
|
|
33
33
|
let size
|
|
@@ -198,8 +198,8 @@ export async function tInspectSymbol(g, args = {}, ctx = {}) {
|
|
|
198
198
|
...(node.source_range ? {range: node.source_range} : {}),
|
|
199
199
|
...(node.complexity ? {complexity: node.complexity} : {}),
|
|
200
200
|
}
|
|
201
|
-
const definitionSource =
|
|
202
|
-
const callerSources = grouped.shown.slice(0, 5).map((group) =>
|
|
201
|
+
const definitionSource = sourceExcerpt(ctx.repoRoot, definition.file, definition.line, contextLines)
|
|
202
|
+
const callerSources = grouped.shown.slice(0, 5).map((group) => sourceExcerpt(ctx.repoRoot, group.file, group.locations[0]?.line || 1, contextLines)).filter(Boolean)
|
|
203
203
|
const result = {
|
|
204
204
|
status: 'OK',
|
|
205
205
|
definition,
|
|
@@ -62,6 +62,21 @@ function architectureState(result) {
|
|
|
62
62
|
return result.verification.new?.length || String(result.verification.status).toUpperCase() === 'FAIL' ? 'BLOCKED' : 'PASS'
|
|
63
63
|
}
|
|
64
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
|
+
|
|
65
80
|
function decide({phase, impact, graph, architecture, duplicates, api, tests, suggestedTests, testCoverageState}) {
|
|
66
81
|
if (phase === 'plan') return tests.state === 'BLOCKED'
|
|
67
82
|
? {verdict: 'BLOCKED', blockers: [`targeted test plan was rejected: ${tests.reason || 'invalid request'}`], unknowns: []}
|
|
@@ -162,6 +177,7 @@ export async function tVerifiedChange(g, args = {}, ctx = {}, tools = {}, permis
|
|
|
162
177
|
`${decision.verdict} — verified_change ${phase}`, `Task: ${String(args.task).slice(0, 500)}`,
|
|
163
178
|
`Change: ${changedFiles.length} file(s), ${impact.seeds?.ids?.length || 0} exact seed(s), blast radius ${impact.blastRadius?.impacted || 0}.`,
|
|
164
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),
|
|
165
181
|
`Ratchets: graph ${graph.state}; architecture ${architecture.state}; duplicates ${duplicates.state}; API ${api.state}; tests ${testProof.state}.`,
|
|
166
182
|
...decision.blockers.map((item) => `BLOCKER: ${item}`), ...decision.unknowns.map((item) => `UNKNOWN: ${item}`),
|
|
167
183
|
].join('\n')
|
package/src/mcp-server.mjs
CHANGED
|
@@ -270,6 +270,7 @@ async function main() {
|
|
|
270
270
|
const graphSnapshot = graph
|
|
271
271
|
const callCtx = mutatesTarget ? ctx : {...ctx}
|
|
272
272
|
const execute = async () => {
|
|
273
|
+
const startedAt = performance.now()
|
|
273
274
|
try {
|
|
274
275
|
// A queued rebuild must see a preceding retarget's graph, while non-mutating calls stay
|
|
275
276
|
// pinned to the graph that was active when their request arrived.
|
|
@@ -302,12 +303,39 @@ async function main() {
|
|
|
302
303
|
if (schemaWarning) text += `\n\nWarning: ${schemaWarning.message}`
|
|
303
304
|
if (staleLine && staleNotices.shouldShow({line: staleLine, graphPath: callCtx.graphPath, force: tool.name === 'graph_stats'})) text += `\n\n${staleLine}`
|
|
304
305
|
}
|
|
305
|
-
const
|
|
306
|
+
const structuredBytes = normalized.structured ? Buffer.byteLength(JSON.stringify(normalized.structured)) : 0
|
|
307
|
+
const textBytes = Buffer.byteLength(text)
|
|
308
|
+
const durationMs = Math.max(0, performance.now() - startedAt)
|
|
309
|
+
const response = {
|
|
310
|
+
content: [{type: 'text', text}],
|
|
311
|
+
_meta: {
|
|
312
|
+
'weavatrix/metrics': {
|
|
313
|
+
schemaVersion: 'weavatrix.metrics.v1',
|
|
314
|
+
durationMs: Math.round(durationMs * 10) / 10,
|
|
315
|
+
textBytes,
|
|
316
|
+
structuredBytes,
|
|
317
|
+
estimatedOutputTokens: Math.ceil((textBytes + structuredBytes) / 4),
|
|
318
|
+
graphFreshness: staleLine ? 'stale' : (refresh?.error ? 'stale' : 'fresh'),
|
|
319
|
+
graphUpdate: refresh?.kind || 'none',
|
|
320
|
+
graphRevision: refresh?.revision || callGraph?.graphRevision || null,
|
|
321
|
+
cache: refreshesGraph ? (refresh?.kind === 'none' ? 'graph-hit' : 'graph-refreshed') : 'not-applicable',
|
|
322
|
+
},
|
|
323
|
+
},
|
|
324
|
+
}
|
|
306
325
|
if (normalized.structured) response.structuredContent = normalized.structured
|
|
307
326
|
return reply(id, response)
|
|
308
327
|
} catch (e) {
|
|
309
328
|
log(`tool ${params?.name} threw: ${e.stack || e.message}`)
|
|
310
|
-
|
|
329
|
+
const text = `Tool error: ${e.message}`
|
|
330
|
+
return reply(id, {
|
|
331
|
+
content: [{type: 'text', text}], isError: true,
|
|
332
|
+
_meta: {'weavatrix/metrics': {
|
|
333
|
+
schemaVersion: 'weavatrix.metrics.v1', durationMs: Math.round(Math.max(0, performance.now() - startedAt) * 10) / 10,
|
|
334
|
+
textBytes: Buffer.byteLength(text), structuredBytes: 0,
|
|
335
|
+
estimatedOutputTokens: Math.ceil(Buffer.byteLength(text) / 4), graphFreshness: 'unknown',
|
|
336
|
+
graphUpdate: 'none', graphRevision: graphSnapshot?.graphRevision || null, cache: 'not-applicable',
|
|
337
|
+
}},
|
|
338
|
+
})
|
|
311
339
|
}
|
|
312
340
|
}
|
|
313
341
|
if (!mutatesTarget && !refreshesGraph) return execute()
|
package/src/mcp-source-tools.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs'
|
|
2
2
|
import { spawnSync } from 'node:child_process'
|
|
3
|
-
import { extname, join, relative } from 'node:path'
|
|
3
|
+
import { extname, isAbsolute, join, relative, resolve } from 'node:path'
|
|
4
4
|
import { resolveRepoPath } from './repo-path.js'
|
|
5
5
|
import { childProcessEnv } from './child-env.js'
|
|
6
6
|
import { toolResult } from './mcp/tool-result.mjs'
|
|
@@ -11,21 +11,24 @@ const MAX_SEARCH_FILE_BYTES = 1024 * 1024
|
|
|
11
11
|
const MAX_SOURCE_FILE_BYTES = 2 * 1024 * 1024
|
|
12
12
|
const MAX_SOURCE_CONTEXT_LINES = 1000
|
|
13
13
|
|
|
14
|
-
function rgSearch(repoRoot, resolveRg, query, { isRegex, glob, maxResults }) {
|
|
14
|
+
function rgSearch(repoRoot, resolveRg, query, { isRegex, glob, maxResults }, spawnRg = spawnSync) {
|
|
15
15
|
const rg = resolveRg()
|
|
16
16
|
if (!rg) return null
|
|
17
17
|
const args = ['--line-number', '--no-heading', '--color', 'never', '--hidden', '-g', '!.git/**', '-m', '100', '-i']
|
|
18
18
|
if (!isRegex) args.push('--fixed-strings')
|
|
19
19
|
if (glob) args.push('-g', glob)
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
// Run from the repository root and search `.` so path globs such as `src/**` are evaluated
|
|
21
|
+
// against repository-relative paths on Windows too. Passing an absolute search root makes rg
|
|
22
|
+
// compare the glob with a drive-prefixed path and silently return no matches.
|
|
23
|
+
args.push('--', query, '.')
|
|
24
|
+
const res = spawnRg(rg, args, { cwd: repoRoot, encoding: 'utf8', maxBuffer: 16 * 1024 * 1024, timeout: 15000, env: childProcessEnv() })
|
|
22
25
|
if (res.status !== 0 && res.status !== 1) return null
|
|
23
26
|
const out = []
|
|
24
27
|
for (const line of (res.stdout || '').split(/\r?\n/)) {
|
|
25
28
|
const match = line.match(/^(.*?):(\d+):(.*)$/)
|
|
26
29
|
if (!match) continue
|
|
27
30
|
out.push({
|
|
28
|
-
file: relative(repoRoot, match[1]).replace(/\\/g, '/'),
|
|
31
|
+
file: relative(repoRoot, isAbsolute(match[1]) ? match[1] : resolve(repoRoot, match[1])).replace(/\\/g, '/'),
|
|
29
32
|
line: Number(match[2]),
|
|
30
33
|
text: match[3].trim().slice(0, 300),
|
|
31
34
|
})
|
|
@@ -87,12 +90,12 @@ function nodeGrep(repoRoot, query, { isRegex, glob, maxResults }) {
|
|
|
87
90
|
return out
|
|
88
91
|
}
|
|
89
92
|
|
|
90
|
-
export function searchCode({ repoRoot, resolveRg }, { query, is_regex = false, max_results = 40, glob } = {}) {
|
|
93
|
+
export function searchCode({ repoRoot, resolveRg, spawnRg = spawnSync }, { query, is_regex = false, max_results = 40, glob } = {}) {
|
|
91
94
|
if (!query) return 'Provide a "query" string.'
|
|
92
95
|
if (!repoRoot || !existsSync(repoRoot)) return 'Source search unavailable: repo root not provided to this MCP server.'
|
|
93
96
|
const max = Math.max(1, Math.min(200, Number(max_results) || 40))
|
|
94
97
|
const opts = { isRegex: !!is_regex, glob: glob || null, maxResults: max }
|
|
95
|
-
let matches = rgSearch(repoRoot, resolveRg, query, opts)
|
|
98
|
+
let matches = rgSearch(repoRoot, resolveRg, query, opts, spawnRg)
|
|
96
99
|
const engine = matches ? 'ripgrep' : 'node'
|
|
97
100
|
if (!matches) matches = nodeGrep(repoRoot, query, opts)
|
|
98
101
|
const what = is_regex ? `/${query}/i` : `"${query}"`
|
|
@@ -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$|(^|\/)
|
|
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,
|
package/docs/releases/v0.2.7.md
DELETED
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
# Weavatrix 0.2.7
|
|
2
|
-
|
|
3
|
-
## Proof-carrying changes
|
|
4
|
-
|
|
5
|
-
`verified_change` is a new high-level MCP workflow for planning and verifying a code change. It
|
|
6
|
-
accepts a natural-language task, an optional unified diff or changed-file hints, and an immutable Git
|
|
7
|
-
baseline. Its stable result is one of:
|
|
8
|
-
|
|
9
|
-
- `PASS`: every applicable enabled proof completed and no ratchet failed;
|
|
10
|
-
- `BLOCKED`: a test failed, a new duplicate intersects changed code, a runtime cycle appeared, an
|
|
11
|
-
architecture ratchet failed, or bounded API evidence found an HTTP method mismatch;
|
|
12
|
-
- `UNKNOWN`: required evidence is missing, partial, disabled, or not configured.
|
|
13
|
-
|
|
14
|
-
The structured result contains intent-expanded task retrieval combined with exact changed-symbol
|
|
15
|
-
seeds, up to five compact LSP/graph edit contexts, symbol-aware blast radius, affected-test paths,
|
|
16
|
-
bounded JS/TS call-argument-to-parameter evidence, Git graph drift, architecture verification, a
|
|
17
|
-
new-duplicate baseline comparison, and optional cross-repository HTTP contract evidence.
|
|
18
|
-
|
|
19
|
-
The workflow respects the existing capability boundary. Exact source/LSP context requires `source`,
|
|
20
|
-
duplicate comparison and architecture verification require `health`, and API tracing requires
|
|
21
|
-
`crossrepo`. A narrow custom profile is never silently bypassed.
|
|
22
|
-
|
|
23
|
-
## Safe targeted tests
|
|
24
|
-
|
|
25
|
-
The workflow does not accept shell commands. It can execute at most five existing `package.json`
|
|
26
|
-
scripts whose names match the test/check/verify allowlist. Execution requires both
|
|
27
|
-
`run_tests:true` on the tool call and `WEAVATRIX_ALLOW_TEST_RUNS=1` in the server environment. The
|
|
28
|
-
default remains read-only and returns the test plan or `UNKNOWN` when runtime evidence was requested
|
|
29
|
-
but not authorized.
|
|
30
|
-
|
|
31
|
-
## Malware scanner correction
|
|
32
|
-
|
|
33
|
-
The npm content sweep now starts at actual installed package roots instead of the entire
|
|
34
|
-
`node_modules` container. Hidden package-manager caches and hosted release snapshots are therefore
|
|
35
|
-
not attributed as dependencies and cannot make Weavatrix flag its own malware-signature source.
|
|
36
|
-
|
|
37
|
-
Heuristic remediation no longer says to treat a package as compromised or rotate all reachable
|
|
38
|
-
secrets. It asks the operator to quarantine and inspect the package, verify origin and lockfile
|
|
39
|
-
integrity, and rotate secrets only after execution or credential exposure is confirmed.
|
|
40
|
-
|
|
41
|
-
Static heuristic findings now have a hard `high` severity ceiling. They expose
|
|
42
|
-
`confirmedExecution:false` plus explicit runtime/origin/lockfile/exposure verification states;
|
|
43
|
-
`critical` is reserved for independently confirmed malicious advisories or runtime evidence.
|
|
44
|
-
|
|
45
|
-
Package-lock install-script drift now compares only `preinstall`, `install`, and `postinstall`.
|
|
46
|
-
Development/publication hooks such as `prepare` no longer create false high-severity drift findings.
|
|
47
|
-
|
|
48
|
-
## Signal-quality corrections
|
|
49
|
-
|
|
50
|
-
- `query_graph` applies production-first path classification to the traversal, not only seed
|
|
51
|
-
selection. Tests, generated code, fixtures, docs, benchmarks and excluded paths require an
|
|
52
|
-
explicit question class or `include_classified:true`. Exact non-product seed files remain usable.
|
|
53
|
-
- `verified_change` task retrieval uses the same production-first policy while always retaining
|
|
54
|
-
exact changed symbols. The bundled routing microbenchmark moved from one test-symbol false
|
|
55
|
-
positive (`0.25`) to zero (`0`) without losing any of its three expected production targets.
|
|
56
|
-
- Unreferenced constant/field leaves that do not match the query are hidden by default; use
|
|
57
|
-
`include_low_signal:true` for diagnostic expansion.
|
|
58
|
-
- `hot_path_review` now defaults to a focused score gate of 85 plus a narrow strong-local fallback.
|
|
59
|
-
On the 0.2.7 source tree this reduced the default queue from 717 diagnostic candidates to 116 of
|
|
60
|
-
1,043 analyzed symbols; `min_score:0` restores the full diagnostic set.
|
|
61
|
-
- `find_dead_code` adds `STRONG_STATIC_EVIDENCE`, `BOUNDED_STATIC_EVIDENCE`, and
|
|
62
|
-
`HIGH_UNCERTAINTY` tiers, structured completed/remaining checks, and `autoDelete:false` on every
|
|
63
|
-
candidate. Strong static evidence means exact zero LSP references, not permission to delete.
|
|
64
|
-
- npm unused/missing/duplicate dependency findings now carry manifest section, indexed-source,
|
|
65
|
-
script/config, package-scope and unresolved dynamic/plugin verification state. Unused dependencies
|
|
66
|
-
remain `REVIEW_REQUIRED`; missing dependencies retain the exact importing-file evidence.
|
|
67
|
-
|
|
68
|
-
## Benchmark contract
|
|
69
|
-
|
|
70
|
-
`npm run benchmark:agent` runs a deterministic task-to-symbol routing microbenchmark and reports task
|
|
71
|
-
success, false-positive rate, estimated output tokens, and duration. This is intentionally labelled
|
|
72
|
-
as a routing microbenchmark, not an autonomous end-to-end change benchmark.
|
|
73
|
-
|
|
74
|
-
Independent Codebase Memory and Serena results may be supplied with
|
|
75
|
-
`--independent-results <json>` using the blind evaluator contract in
|
|
76
|
-
[`docs/agent-task-benchmark.md`](../agent-task-benchmark.md). Without same-task results for all three
|
|
77
|
-
systems, comparison status is `INCOMPLETE`; `npm run benchmark:agent:release` fails closed. The
|
|
78
|
-
repository does not manufacture competitor scores.
|
|
79
|
-
|
|
80
|
-
## Scope and limitations
|
|
81
|
-
|
|
82
|
-
- The interprocedural evidence maps call arguments to declared callee parameters for bounded JS/TS
|
|
83
|
-
graph call edges. It is not a CFG, value-propagation engine, or taint analysis.
|
|
84
|
-
- An absent architecture contract produces `UNKNOWN`; provisional budgets are guidance, not policy.
|
|
85
|
-
- A static path to a test is not proof that the test executed. A `PASS` that depends on runtime
|
|
86
|
-
behavior requires explicitly authorized test execution.
|
|
87
|
-
- API proof is optional and registry-scoped. No client match is `UNKNOWN`, never proof of no external
|
|
88
|
-
consumers.
|
|
89
|
-
|
|
90
|
-
## Tool catalog
|
|
91
|
-
|
|
92
|
-
0.2.7 exposes 36 MCP tools. The default offline profile exposes 33; pinned exposes 30. Network tools
|
|
93
|
-
remain absent from the offline and pinned profiles.
|