weavatrix 0.3.12 → 0.3.14
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 +9 -4
- package/docs/releases/v0.3.13.md +34 -0
- package/docs/releases/v0.3.14.md +37 -0
- package/package.json +3 -1
- package/src/analysis/coverage-reports-tarpaulin.js +112 -0
- package/src/analysis/coverage-reports.js +13 -2
- package/src/analysis/dead-code-review/policy.js +1 -1
- package/src/analysis/duplicate-groups.js +1 -1
- package/src/analysis/endpoints/common.js +18 -1
- package/src/analysis/endpoints/extract.js +13 -0
- package/src/analysis/graph-analysis.aggregate.js +4 -3
- package/src/analysis/graph-analysis.refs.js +48 -0
- package/src/analysis/hot-path-review.js +1 -1
- package/src/analysis/internal-audit.reach.js +28 -0
- package/src/analysis/internal-audit.run.js +1 -1
- package/src/analysis/static-test-reachability.js +1 -1
- package/src/mcp/architecture-starter.mjs +1 -1
- package/src/mcp/catalog.mjs +9 -5
- package/src/mcp/company-contract-page.mjs +142 -0
- package/src/mcp/company-contract-reconcile.mjs +87 -0
- package/src/mcp/evidence-snapshot.duplicates.mjs +1 -1
- package/src/mcp/graph/context-seeds.mjs +0 -7
- package/src/mcp/graph/tools-query.mjs +34 -7
- package/src/mcp/health/audit-format.mjs +2 -2
- package/src/mcp/health/audit.mjs +29 -4
- package/src/mcp/health/duplicates.mjs +43 -7
- package/src/mcp/health/endpoints.mjs +1 -1
- package/src/mcp/health/structure.mjs +4 -3
- package/src/mcp/tools-company.mjs +52 -76
- package/src/mcp/tools-context.mjs +1 -1
- package/src/mcp/tools-endpoints.mjs +1 -1
- package/src/mcp/tools-graph-hubs.mjs +1 -1
- package/src/mcp/tools-verified-change.mjs +11 -4
- package/src/path-classification.js +7 -1
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
// Cross-repository HTTP contract intelligence. Repository paths are resolved only through the
|
|
2
2
|
// local global registry; callers can select an opaque repository UUID or an unambiguous label but
|
|
3
3
|
// can never pass an arbitrary filesystem path through this tool.
|
|
4
|
-
import {join} from 'node:path'
|
|
5
4
|
import {analyzeHttpContracts} from '../analysis/http-contracts.js'
|
|
6
5
|
import {analyzeTransportContracts} from '../analysis/transport-contracts.js'
|
|
7
|
-
import {buildGraphForRepo} from '../build-graph.js'
|
|
8
6
|
import {graphHomeDir} from '../graph/layout.js'
|
|
9
7
|
import {liveRepositoryRecords} from '../graph/repo-registry.js'
|
|
10
|
-
import {
|
|
8
|
+
import {ContractCursorError, paginateContractEvidence} from './company-contract-page.mjs'
|
|
9
|
+
import {publicRecord, reconcileGraph} from './company-contract-reconcile.mjs'
|
|
11
10
|
import {toolResult} from './tool-result.mjs'
|
|
12
11
|
import {contractVerdict, contractVerdictLine} from './company-contract-verdict.mjs'
|
|
13
12
|
|
|
14
|
-
const CROSS_REPO_HTTP_CONTRACT_V =
|
|
13
|
+
const CROSS_REPO_HTTP_CONTRACT_V = 4
|
|
15
14
|
const selectorText = (value) => String(value ?? '').trim()
|
|
16
15
|
|
|
17
16
|
function selectRecord(records, selector) {
|
|
@@ -35,76 +34,6 @@ function safeAlias(record, records) {
|
|
|
35
34
|
return duplicates.length > 1 ? `${base}-${String(record.repositoryId).slice(0, 8)}` : base
|
|
36
35
|
}
|
|
37
36
|
|
|
38
|
-
function publicRecord(record, alias) {
|
|
39
|
-
return {repositoryId: record.repositoryId, label: record.label, alias}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function safeRefreshReason(record, error) {
|
|
43
|
-
let message = String(error instanceof Error ? error.message : error || 'graph refresh failed')
|
|
44
|
-
for (const path of [record.repoPath, record.graphDir]) {
|
|
45
|
-
if (!path) continue
|
|
46
|
-
message = message.split(String(path)).join(record.label || 'repository')
|
|
47
|
-
message = message.split(String(path).replace(/\\/g, '/')).join(record.label || 'repository')
|
|
48
|
-
}
|
|
49
|
-
return message.replace(/[\r\n]+/g, ' ').trim().slice(0, 400) || 'graph refresh failed'
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
async function reconcileGraph(record, alias, role, graphHome) {
|
|
53
|
-
let registeredGraph = null
|
|
54
|
-
try { registeredGraph = loadGraph(join(record.graphDir, 'graph.json'), {repoRoot: record.repoPath}) } catch { /* refresh may repair it */ }
|
|
55
|
-
const buildMode = ['full', 'no-tests', 'tests-only'].includes(registeredGraph?.graphBuildMode)
|
|
56
|
-
? registeredGraph.graphBuildMode
|
|
57
|
-
: 'full'
|
|
58
|
-
const precision = registeredGraph?.graphPrecisionMode === 'off' ? 'off' : 'lsp'
|
|
59
|
-
let build
|
|
60
|
-
try {
|
|
61
|
-
build = await buildGraphForRepo(record.repoPath, {
|
|
62
|
-
mode: buildMode,
|
|
63
|
-
precision,
|
|
64
|
-
scope: '',
|
|
65
|
-
outDir: record.graphDir,
|
|
66
|
-
graphHome,
|
|
67
|
-
})
|
|
68
|
-
} catch (error) {
|
|
69
|
-
build = {ok: false, error: safeRefreshReason(record, error)}
|
|
70
|
-
}
|
|
71
|
-
const refreshKind = build?.refresh?.kind || null
|
|
72
|
-
const changedFiles = Array.isArray(build?.refresh?.changedFiles) ? build.refresh.changedFiles : []
|
|
73
|
-
const publicStatus = {
|
|
74
|
-
role,
|
|
75
|
-
repository: publicRecord(record, alias),
|
|
76
|
-
buildMode,
|
|
77
|
-
status: build?.ok ? (refreshKind === 'none' ? 'CURRENT' : 'REFRESHED') : 'STALE_FALLBACK',
|
|
78
|
-
refresh: build?.ok ? {
|
|
79
|
-
kind: refreshKind || 'full',
|
|
80
|
-
reason: String(build?.refresh?.reason || 'graph-rebuilt'),
|
|
81
|
-
changedFileCount: changedFiles.length,
|
|
82
|
-
} : null,
|
|
83
|
-
}
|
|
84
|
-
if (build?.ok) {
|
|
85
|
-
try {
|
|
86
|
-
return {record, alias, graph: loadGraph(join(record.graphDir, 'graph.json'), {repoRoot: record.repoPath}), publicStatus}
|
|
87
|
-
} catch (error) {
|
|
88
|
-
const reason = `refreshed graph could not be loaded: ${safeRefreshReason(record, error)}`
|
|
89
|
-
return {record, alias, graph: null, reason, publicStatus: {...publicStatus, status: 'FAILED', reason}}
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
const reason = `graph refresh failed: ${safeRefreshReason(record, build?.error)}`
|
|
94
|
-
try {
|
|
95
|
-
return {
|
|
96
|
-
record,
|
|
97
|
-
alias,
|
|
98
|
-
graph: loadGraph(join(record.graphDir, 'graph.json'), {repoRoot: record.repoPath}),
|
|
99
|
-
reason: `${reason}; stale registered graph used`,
|
|
100
|
-
publicStatus: {...publicStatus, reason: `${reason}; stale registered graph used`},
|
|
101
|
-
}
|
|
102
|
-
} catch (error) {
|
|
103
|
-
const loadReason = `${reason}; registered graph could not be loaded: ${safeRefreshReason(record, error)}`
|
|
104
|
-
return {record, alias, graph: null, reason: loadReason, publicStatus: {...publicStatus, status: 'FAILED', reason: loadReason}}
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
37
|
export async function tTraceApiContract(_g, args = {}, ctx = {}) {
|
|
109
38
|
const graphHome = ctx.graphHome || graphHomeDir()
|
|
110
39
|
const records = liveRepositoryRecords(graphHome)
|
|
@@ -246,6 +175,39 @@ export async function tTraceApiContract(_g, args = {}, ctx = {}) {
|
|
|
246
175
|
? 'Completeness: complete within the declared repository graphs, supported static models and fresh revision-bound runtime capture.'
|
|
247
176
|
: `Completeness: partial — ${completeness.reasons.join('; ')}.`,
|
|
248
177
|
]
|
|
178
|
+
let evidencePage
|
|
179
|
+
try {
|
|
180
|
+
evidencePage = paginateContractEvidence({
|
|
181
|
+
analysis,
|
|
182
|
+
transportAnalysis,
|
|
183
|
+
args,
|
|
184
|
+
fingerprintParts: [
|
|
185
|
+
backend.repositoryId,
|
|
186
|
+
backendGraph.graph?.graphRevision || null,
|
|
187
|
+
...clientGraphs.flatMap((item) => [item.record.repositoryId, item.graph?.graphRevision || null]),
|
|
188
|
+
selectedTransport,
|
|
189
|
+
args.method || null,
|
|
190
|
+
args.path || null,
|
|
191
|
+
Array.isArray(args.changed_files) ? [...args.changed_files].sort() : [],
|
|
192
|
+
args.include_tests === true,
|
|
193
|
+
],
|
|
194
|
+
})
|
|
195
|
+
} catch (error) {
|
|
196
|
+
if (!(error instanceof ContractCursorError)) throw error
|
|
197
|
+
return toolResult(
|
|
198
|
+
`VERDICT INVALID_CURSOR — ${error.message}. Restart from the first page without cursor.`,
|
|
199
|
+
{status: 'INVALID_CURSOR', code: error.code, reason: error.message},
|
|
200
|
+
{page: {status: 'INVALID_CURSOR'}},
|
|
201
|
+
)
|
|
202
|
+
}
|
|
203
|
+
const transportSummary = {
|
|
204
|
+
transportContractsV: transportAnalysis.transportContractsV,
|
|
205
|
+
transport: transportAnalysis.transport,
|
|
206
|
+
status: transportAnalysis.status,
|
|
207
|
+
completeness: transportAnalysis.completeness,
|
|
208
|
+
totals: transportAnalysis.totals,
|
|
209
|
+
runtimeEvidence: transportAnalysis.runtimeEvidence,
|
|
210
|
+
}
|
|
249
211
|
const result = {
|
|
250
212
|
crossRepoHttpContractV: CROSS_REPO_HTTP_CONTRACT_V,
|
|
251
213
|
verdict,
|
|
@@ -253,14 +215,28 @@ export async function tTraceApiContract(_g, args = {}, ctx = {}) {
|
|
|
253
215
|
backend: publicRecord(backend, backendAlias),
|
|
254
216
|
clients: clients.map(({record, alias}) => publicRecord(record, alias)),
|
|
255
217
|
},
|
|
256
|
-
|
|
257
|
-
|
|
218
|
+
httpContractsV: analysis.httpContractsV,
|
|
219
|
+
filters: analysis.filters,
|
|
220
|
+
limits: analysis.limits,
|
|
221
|
+
totals: analysis.totals,
|
|
222
|
+
wrapperDiscovery: analysis.wrapperDiscovery,
|
|
223
|
+
transportContracts: transportSummary,
|
|
224
|
+
evidencePage,
|
|
258
225
|
status: completeness.status,
|
|
259
226
|
graphReconciliation,
|
|
260
227
|
completeness,
|
|
261
228
|
}
|
|
262
229
|
return toolResult(lines.join('\n'), result, {
|
|
263
230
|
completeness,
|
|
231
|
+
page: {
|
|
232
|
+
detail: evidencePage.detail,
|
|
233
|
+
offset: evidencePage.offset,
|
|
234
|
+
pageSize: evidencePage.pageSize,
|
|
235
|
+
totalItems: evidencePage.totalItems,
|
|
236
|
+
returnedItems: evidencePage.returnedItems,
|
|
237
|
+
hasMore: evidencePage.hasMore,
|
|
238
|
+
nextCursor: evidencePage.nextCursor,
|
|
239
|
+
},
|
|
264
240
|
warnings: completeness.complete ? [] : [{code: 'CROSS_REPO_ANALYSIS_PARTIAL', message: completeness.reasons.join('; ')}],
|
|
265
241
|
})
|
|
266
242
|
} catch (error) {
|
|
@@ -6,7 +6,7 @@ import {sourceExcerpt} from './tools-source.mjs'
|
|
|
6
6
|
import {createPathClassifier, hasPathClass} from '../path-classification.js'
|
|
7
7
|
|
|
8
8
|
const MAX_LINE_SAMPLES = 5
|
|
9
|
-
const CONTEXT_NON_PRODUCT = Object.freeze(['test', 'e2e', 'generated', 'mock', 'story', 'docs', 'benchmark', 'temp'])
|
|
9
|
+
const CONTEXT_NON_PRODUCT = Object.freeze(['test', 'e2e', 'generated', 'vendored', 'mock', 'story', 'docs', 'benchmark', 'temp'])
|
|
10
10
|
|
|
11
11
|
const fileOf = (g, id) => {
|
|
12
12
|
const node = g.byId.get(String(id))
|
|
@@ -4,7 +4,7 @@ import {createPathClassifier, hasPathClass} from '../path-classification.js'
|
|
|
4
4
|
import {sourceExcerpt} from './tools-source.mjs'
|
|
5
5
|
import {toolResult} from './tool-result.mjs'
|
|
6
6
|
|
|
7
|
-
const NON_PRODUCT = ['test', 'e2e', 'generated', 'mock', 'story', 'docs', 'benchmark', 'temp']
|
|
7
|
+
const NON_PRODUCT = ['test', 'e2e', 'generated', 'vendored', 'mock', 'story', 'docs', 'benchmark', 'temp']
|
|
8
8
|
|
|
9
9
|
const symbolLine = (node) => Number(String(node?.source_location || '').match(/L(\d+)/)?.[1]
|
|
10
10
|
|| String(node?.id || '').match(/@(\d+)$/)?.[1] || 0)
|
|
@@ -3,7 +3,7 @@ import {connList} from './graph-context.mjs'
|
|
|
3
3
|
import {createPathClassifier, hasPathClass} from '../path-classification.js'
|
|
4
4
|
|
|
5
5
|
const isCompileTimeEdge = (edge) => edge?.typeOnly === true || edge?.compileOnly === true
|
|
6
|
-
const NON_PRODUCT_CLASSES = Object.freeze(['test', 'e2e', 'generated', 'mock', 'story', 'docs', 'benchmark', 'temp'])
|
|
6
|
+
const NON_PRODUCT_CLASSES = Object.freeze(['test', 'e2e', 'generated', 'vendored', 'mock', 'story', 'docs', 'benchmark', 'temp'])
|
|
7
7
|
const sourceFileOf = (node) => String(node?.source_file || (node?.file_type === 'code' ? node?.id : '') || '').replace(/\\/g, '/')
|
|
8
8
|
|
|
9
9
|
export function tGodNodes(g, {top_n = 10, include_classified = false} = {}, ctx = {}) {
|
|
@@ -148,10 +148,17 @@ export async function tVerifiedChange(g, args = {}, ctx = {}, tools = {}, permis
|
|
|
148
148
|
const baseRef = String(args.base_ref || 'HEAD').trim()
|
|
149
149
|
const graph = phase === 'verify' ? await baselineProof(ctx, baseRef, currentGraph) : {state: 'PLANNED', baseline: baseRef}
|
|
150
150
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
151
|
+
const duplicateRatchetEnabled = args.duplicate_ratchet !== false
|
|
152
|
+
let duplicates
|
|
153
|
+
if (!duplicateRatchetEnabled) {
|
|
154
|
+
duplicates = {state: 'SKIPPED', enabled: false, reason: 'duplicate ratchet disabled'}
|
|
155
|
+
} else if (phase !== 'verify') {
|
|
156
|
+
duplicates = {state: 'PLANNED', enabled: true, reason: 'duplicate ratchet runs during verify phase'}
|
|
157
|
+
} else {
|
|
158
|
+
duplicates = permissions.health
|
|
159
|
+
? {enabled: true, ...await compareDuplicateGroups({repoRoot: ctx.repoRoot, graphPath: ctx.graphPath, currentGraph, baseRef, changedFiles, args: {mode: 'renamed', min_similarity: 80, min_tokens: 50}})}
|
|
160
|
+
: {state: 'UNKNOWN', enabled: true, reason: 'health capability is not enabled'}
|
|
161
|
+
}
|
|
155
162
|
|
|
156
163
|
let api = {state: 'SKIPPED', reason: 'no api_contract scope was requested'}
|
|
157
164
|
if (args.api_contract) {
|
|
@@ -4,11 +4,12 @@
|
|
|
4
4
|
import { openSync, closeSync, readSync, readFileSync, statSync } from "node:fs";
|
|
5
5
|
import { createRepoBoundary } from "./repo-path.js";
|
|
6
6
|
|
|
7
|
-
export const PATH_CLASS_NAMES = Object.freeze(["test", "e2e", "generated", "mock", "story", "docs", "benchmark", "temp"]);
|
|
7
|
+
export const PATH_CLASS_NAMES = Object.freeze(["test", "e2e", "generated", "vendored", "mock", "story", "docs", "benchmark", "temp"]);
|
|
8
8
|
export const PATH_CLASS_QUERY_TERMS = Object.freeze({
|
|
9
9
|
test: Object.freeze(["test", "tests", "testing", "unit"]),
|
|
10
10
|
e2e: Object.freeze(["e2e", "playwright", "cypress"]),
|
|
11
11
|
generated: Object.freeze(["generated", "autogenerated", "dist"]),
|
|
12
|
+
vendored: Object.freeze(["vendor", "vendored", "third-party", "third_party"]),
|
|
12
13
|
mock: Object.freeze(["mock", "mocks", "fixture", "fixtures", "fake"]),
|
|
13
14
|
story: Object.freeze(["story", "stories", "storybook"]),
|
|
14
15
|
docs: Object.freeze(["doc", "docs", "documentation", "readme", "guide"]),
|
|
@@ -72,6 +73,11 @@ const DEFAULT_RULES = [
|
|
|
72
73
|
pattern: "conventional build output path",
|
|
73
74
|
regex: /(^|\/)(?:dist|build|out|coverage|\.next)(\/|$)/i,
|
|
74
75
|
},
|
|
76
|
+
{
|
|
77
|
+
category: "vendored",
|
|
78
|
+
pattern: "vendored or third-party source root",
|
|
79
|
+
regex: /(^|\/)(?:vendor|vendored|third[-_]?party)(\/|$)/i,
|
|
80
|
+
},
|
|
75
81
|
{
|
|
76
82
|
category: "mock",
|
|
77
83
|
pattern: "mock/fixture path or mockData filename",
|