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.
Files changed (34) hide show
  1. package/README.md +9 -4
  2. package/docs/releases/v0.3.13.md +34 -0
  3. package/docs/releases/v0.3.14.md +37 -0
  4. package/package.json +3 -1
  5. package/src/analysis/coverage-reports-tarpaulin.js +112 -0
  6. package/src/analysis/coverage-reports.js +13 -2
  7. package/src/analysis/dead-code-review/policy.js +1 -1
  8. package/src/analysis/duplicate-groups.js +1 -1
  9. package/src/analysis/endpoints/common.js +18 -1
  10. package/src/analysis/endpoints/extract.js +13 -0
  11. package/src/analysis/graph-analysis.aggregate.js +4 -3
  12. package/src/analysis/graph-analysis.refs.js +48 -0
  13. package/src/analysis/hot-path-review.js +1 -1
  14. package/src/analysis/internal-audit.reach.js +28 -0
  15. package/src/analysis/internal-audit.run.js +1 -1
  16. package/src/analysis/static-test-reachability.js +1 -1
  17. package/src/mcp/architecture-starter.mjs +1 -1
  18. package/src/mcp/catalog.mjs +9 -5
  19. package/src/mcp/company-contract-page.mjs +142 -0
  20. package/src/mcp/company-contract-reconcile.mjs +87 -0
  21. package/src/mcp/evidence-snapshot.duplicates.mjs +1 -1
  22. package/src/mcp/graph/context-seeds.mjs +0 -7
  23. package/src/mcp/graph/tools-query.mjs +34 -7
  24. package/src/mcp/health/audit-format.mjs +2 -2
  25. package/src/mcp/health/audit.mjs +29 -4
  26. package/src/mcp/health/duplicates.mjs +43 -7
  27. package/src/mcp/health/endpoints.mjs +1 -1
  28. package/src/mcp/health/structure.mjs +4 -3
  29. package/src/mcp/tools-company.mjs +52 -76
  30. package/src/mcp/tools-context.mjs +1 -1
  31. package/src/mcp/tools-endpoints.mjs +1 -1
  32. package/src/mcp/tools-graph-hubs.mjs +1 -1
  33. package/src/mcp/tools-verified-change.mjs +11 -4
  34. 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 {loadGraph} from './graph-context.mjs'
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 = 3
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
- ...analysis,
257
- transportContracts: transportAnalysis,
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
- let duplicates = {state: 'SKIPPED', reason: 'duplicate ratchet disabled'}
152
- if (phase === 'verify' && args.duplicate_ratchet !== false) duplicates = permissions.health
153
- ? await compareDuplicateGroups({repoRoot: ctx.repoRoot, graphPath: ctx.graphPath, currentGraph, baseRef, changedFiles, args: {mode: 'renamed', min_similarity: 80, min_tokens: 50}})
154
- : {state: 'UNKNOWN', reason: 'health capability is not enabled'}
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",