weavatrix 0.2.14 → 0.2.15

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 CHANGED
@@ -392,6 +392,22 @@ metrics are not persisted or transmitted by Weavatrix. If a source checkout's pa
392
392
  while an old daemon remains alive, `initialize`, `tools/list`, and tool calls fail loudly with
393
393
  `STALE_RUNTIME` until the client reconnects; the opt-out is reserved for deliberate development.
394
394
 
395
+ ### 0.2.15 self-audit precision patch
396
+
397
+ - Dependency review now scopes nested manifests correctly, recognizes framework peer packages and
398
+ package references assembled from bounded dynamic path fragments, and avoids misclassifying
399
+ dependency-owned source as an unused root declaration.
400
+ - Reachability follows source-owned HTML asset references, so maintained browser entry scripts and
401
+ styles remain part of the production surface instead of appearing as dead files.
402
+ - Malware heuristics distinguish inert placeholders, comments, documentation URLs, and ordinary
403
+ Unicode text from executable registry or download behavior while retaining explicit review
404
+ evidence for actionable patterns.
405
+ - Duplicate review ignores policy-like numeric tables and stable ordered-member declarations where
406
+ repetition is intentional, and the remaining reverse-reach, Git-output, member-order, path-term,
407
+ and retrieval logic now uses shared owners instead of drifting copies.
408
+
409
+ Full patch notes: [docs/releases/v0.2.15.md](docs/releases/v0.2.15.md).
410
+
395
411
  ### 0.2.14 typed flows, honest Health, and architecture bootstrap
396
412
 
397
413
  - Go call resolution now follows receiver types through parameters, locals, constructor returns,
package/SECURITY.md CHANGED
@@ -4,8 +4,8 @@
4
4
 
5
5
  | Version | Supported |
6
6
  |---|---|
7
- | `0.2.14.x` | Yes |
8
- | `0.2.13` and older | Upgrade to the latest release |
7
+ | `0.2.15.x` | Yes |
8
+ | `0.2.14` and older | Upgrade to the latest release |
9
9
 
10
10
  Security fixes are provided for the latest published version of Weavatrix. Upgrade before reporting
11
11
  an issue that may already be resolved.
@@ -0,0 +1,37 @@
1
+ # Weavatrix 0.2.15
2
+
3
+ 0.2.15 is a focused self-audit precision release. It keeps the 38-tool MCP surface and all wire and
4
+ security boundaries stable while removing false-positive dependency, dead-code, malware, and
5
+ duplicate findings discovered by running Weavatrix against its own repository.
6
+
7
+ ## Dependency and reachability precision
8
+
9
+ - Dependency verification now respects the owning nested manifest before deciding whether an import
10
+ is missing or a declaration is unused.
11
+ - Framework peer dependencies and bounded package paths assembled from static string fragments are
12
+ recognized without treating arbitrary runtime values as proof.
13
+ - Source-owned HTML asset references participate in reachability, protecting maintained browser
14
+ scripts and styles from false dead-file findings.
15
+
16
+ ## Safer review queues
17
+
18
+ - Malware scanning distinguishes placeholders, comments, documentation URLs, and ordinary Unicode
19
+ text from executable registry/download behavior. Static heuristic evidence remains review-only and
20
+ does not claim execution or compromise.
21
+ - Duplicate detection suppresses policy-like numeric tables and stable ordered declarations where
22
+ repeated structure carries meaning, while preserving clone evidence for executable code.
23
+ - Reverse reachability, Git output parsing, ordered-member comparison, path terminology, task
24
+ retrieval, and duplicate evidence now share focused owners instead of maintaining divergent copies.
25
+
26
+ ## Compatibility and security
27
+
28
+ - There are no MCP tool-schema, capability-profile, Hosted payload, sync-consent, or architecture
29
+ contract changes in this release.
30
+ - The default profile remains offline and local-first. Advisory and Hosted network capabilities
31
+ remain explicit opt-ins.
32
+
33
+ ## Verification
34
+
35
+ The release is gated by the full Node suite, six-language fixture benchmark, six-repository semantic
36
+ regression gate, release-manifest verification, packed npm and MCPB stdio checks, and npm audit. The
37
+ exact counts and artifacts are recorded by the immutable GitHub release workflow.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "weavatrix",
3
- "version": "0.2.14",
3
+ "version": "0.2.15",
4
4
  "type": "module",
5
5
  "description": "Local repository intelligence MCP for AI coding agents: reusable architecture graph for fast application understanding, Health, dead code, clones, history, blast radius and architecture safeguards.",
6
6
  "author": "Sergii Ziborov <sergii.ziborov@gmail.com>",
@@ -26,7 +26,7 @@
26
26
  "skill",
27
27
  "scripts/run-agent-task-benchmark.mjs",
28
28
  "docs/agent-task-benchmark.md",
29
- "docs/releases/v0.2.14.md",
29
+ "docs/releases/v0.2.15.md",
30
30
  "README.md",
31
31
  "SECURITY.md",
32
32
  "LICENSE"
@@ -7,7 +7,9 @@
7
7
  // script/config-text mention scanning + a config-ecosystem prefix rule, and we NEVER say "safe to
8
8
  // auto-remove", only "review".
9
9
  import { makeFinding } from "./findings.js";
10
+ import {FRAMEWORK_RUNTIME_PEERS, IMPLICIT_STYLE_COMPILERS} from './dependency/conventions.js'
10
11
  import {createScopedDepFindings} from './dependency/scoped-dependencies.js'
12
+ import {createPackageSourceMatcher} from './dependency/source-references.js'
11
13
 
12
14
  // Packages referenced by config CONVENTION, not imports (eslint extends "airbnb" → eslint-config-airbnb).
13
15
  // Flagged only at low confidence when nothing mentions them anywhere.
@@ -21,22 +23,6 @@ const BIN_PKG = {
21
23
  "electron-rebuild": "@electron/rebuild", playwright: "@playwright/test",
22
24
  };
23
25
 
24
- // Required peers consumed inside a framework/build tool rather than imported by application source.
25
- // These contracts are package-scope local and deliberately narrow; declaring the provider is required
26
- // for suppression, so an unrelated app still gets the normal unused-dependency finding.
27
- const FRAMEWORK_RUNTIME_PEERS = new Map([
28
- ["next", ["react-dom"]], ["vinext", ["@vitejs/plugin-react", "@vitejs/plugin-rsc", "react-server-dom-webpack", "vite"]],
29
- ["electron-vite", ["vite"]], ["@cloudflare/vite-plugin", ["vite", "wrangler"]],
30
- ]);
31
-
32
- // Style preprocessors are compiler inputs rather than JavaScript imports. Their presence is proven by
33
- // source extensions in the same package scope, so do not report them as unused merely because Vite,
34
- // webpack, etc. load them internally. Keep this list to exact compiler/package contracts.
35
- const IMPLICIT_STYLE_COMPILERS = new Map([
36
- ["sass", /\.(?:scss|sass)$/i], ["sass-embedded", /\.(?:scss|sass)$/i],
37
- ["less", /\.less$/i], ["stylus", /\.styl(?:us)?$/i],
38
- ]);
39
-
40
26
  const isStylesheetSpecifier = (spec) => /\.(?:css|scss|sass|less|styl(?:us)?)(?:[?#].*)?$/i.test(String(spec || ""));
41
27
  const escRe = (s) => String(s).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
42
28
  // word-ish mention: the name not embedded inside a longer identifier/path segment
@@ -47,9 +33,10 @@ const mentioned = (blob, name) => new RegExp(`(^|[^\\w@.-])${escRe(name)}($|[^\\
47
33
  // pkg — parsed package.json ({} for non-JS repos → no dep findings)
48
34
  // workspacePkgNames — Set of monorepo-local package names (never "missing")
49
35
  // configTexts — Map<fileName, text> of root config files + CI workflows (mention scanning)
36
+ // sourceTexts — Map<fileName, text> for narrow dynamic/package-path literal evidence
50
37
  export function computeDepFindings({
51
38
  externalImports = [], pkg = {}, workspacePkgNames = new Set(), configTexts = new Map(),
52
- aliases = [], scope = "", manifest = "package.json", nonRuntimeRoots = [], sourceFiles = [],
39
+ sourceTexts = new Map(), aliases = [], scope = "", manifest = "package.json", nonRuntimeRoots = [], sourceFiles = [],
53
40
  } = {}) {
54
41
  const findings = [];
55
42
  const meta = { scope: scope || ".", manifest };
@@ -137,12 +124,13 @@ export function computeDepFindings({
137
124
  return false;
138
125
  };
139
126
  const typesBase = (name) => (name.startsWith("@types/") ? name.slice(7).replace(/^(.+?)__(.+)$/, "@$1/$2") : null); // @types/babel__core → @babel/core
127
+ const sourceReferenced = createPackageSourceMatcher(sourceTexts);
140
128
 
141
129
  // ---- unused dependencies (per section; prod vs dev differ in severity/confidence) ----
142
130
  for (const [section, deps] of Object.entries(sections)) {
143
131
  if (section === "peerDependencies") continue; // peers are consumer-facing contracts, not usage
144
132
  for (const name of Object.keys(deps)) {
145
- if (name === selfName || usedPackages.has(name) || frameworkRuntime.has(name) || implicitCompilerUsage.has(name)) continue;
133
+ if (name === selfName || usedPackages.has(name) || frameworkRuntime.has(name) || implicitCompilerUsage.has(name) || sourceReferenced(name)) continue;
146
134
  const tb = typesBase(name);
147
135
  if (tb) { // @types/x is used iff x is used (or it types the Node builtins)
148
136
  if (tb === "node" ? builtinUsed : usedPackages.has(tb) || frameworkRuntime.has(tb) || mentioned(configBlob, tb)) continue;
@@ -159,7 +147,9 @@ export function computeDepFindings({
159
147
  title: `Unused ${section === "dependencies" ? "dependency" : section.replace(/ies$/, "y")}: ${name}`,
160
148
  reason: "No indexed package import, package-script command, recognized config mention, framework peer contract, or implicit style-compiler input uses this declaration.",
161
149
  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.`,
150
+ file: manifest,
162
151
  package: name,
152
+ evidence: [{ file: manifest, line: 0, snippet: `declared in ${section}` }],
163
153
  source: "internal",
164
154
  actionability: "MANIFEST_REVIEW_REQUIRED",
165
155
  autoRemove: false,
@@ -0,0 +1,16 @@
1
+ // Exact package contracts consumed by frameworks/build tools without a source
2
+ // import. Keep them package-scope local; these are not repo-wide allowlists.
3
+ export const FRAMEWORK_RUNTIME_PEERS = new Map([
4
+ ['next', ['react-dom']],
5
+ ['vinext', ['@vitejs/plugin-react', '@vitejs/plugin-rsc', 'react-server-dom-webpack', 'vite']],
6
+ ['electron-vite', ['vite']],
7
+ ['@cloudflare/vite-plugin', ['vite', 'wrangler']],
8
+ ])
9
+
10
+ // Style preprocessors are compiler inputs proven by source extensions.
11
+ export const IMPLICIT_STYLE_COMPILERS = new Map([
12
+ ['sass', /\.(?:scss|sass)$/i],
13
+ ['sass-embedded', /\.(?:scss|sass)$/i],
14
+ ['less', /\.less$/i],
15
+ ['stylus', /\.styl(?:us)?$/i],
16
+ ])
@@ -5,13 +5,14 @@ const ownsFile = (scope, file) => !scope || file === scope || String(file || '')
5
5
  export function createScopedDepFindings(computeDepFindings) {
6
6
  return function computeScopedDepFindings({
7
7
  externalImports = [], packageScopes = [], workspacePkgNames = new Set(), configTexts = new Map(),
8
- nonRuntimeRoots = [], sourceFiles = [],
8
+ sourceTexts = new Map(), nonRuntimeRoots = [], sourceFiles = [],
9
9
  } = {}) {
10
10
  const scopes = packageScopes.length
11
11
  ? packageScopes.map((scope) => ({...scope, root: normalizeScope(scope.root)})).sort((a, b) => b.root.length - a.root.length)
12
12
  : [{root: '', manifest: 'package.json', pkg: {}, aliases: []}]
13
13
  const importsByScope = new Map(scopes.map((scope) => [scope, []]))
14
14
  const sourceFilesByScope = new Map(scopes.map((scope) => [scope, []]))
15
+ const sourceTextsByScope = new Map(scopes.map((scope) => [scope, new Map()]))
15
16
  for (const item of externalImports) {
16
17
  const owner = scopes.find((scope) => ownsFile(scope.root, item.file)) || scopes[scopes.length - 1]
17
18
  importsByScope.get(owner).push(item)
@@ -20,6 +21,10 @@ export function createScopedDepFindings(computeDepFindings) {
20
21
  const owner = scopes.find((scope) => ownsFile(scope.root, file)) || scopes[scopes.length - 1]
21
22
  sourceFilesByScope.get(owner).push(file)
22
23
  }
24
+ for (const [file, text] of sourceTexts) {
25
+ const owner = scopes.find((scope) => ownsFile(scope.root, file)) || scopes[scopes.length - 1]
26
+ sourceTextsByScope.get(owner).set(file, text)
27
+ }
23
28
  const configOwner = new Map()
24
29
  for (const [file] of configTexts) configOwner.set(file, scopes.find((scope) => ownsFile(scope.root, file)) || scopes[scopes.length - 1])
25
30
  const findings = [], usedPackages = new Map(), declared = new Set()
@@ -27,7 +32,7 @@ export function createScopedDepFindings(computeDepFindings) {
27
32
  const scopeConfig = new Map([...configTexts].filter(([file]) => configOwner.get(file) === scope))
28
33
  const result = computeDepFindings({
29
34
  externalImports: importsByScope.get(scope), pkg: scope.pkg || {}, workspacePkgNames,
30
- configTexts: scopeConfig, aliases: scope.aliases || [], scope: scope.root,
35
+ configTexts: scopeConfig, sourceTexts: sourceTextsByScope.get(scope), aliases: scope.aliases || [], scope: scope.root,
31
36
  manifest: scope.manifest || (scope.root ? `${scope.root}/package.json` : 'package.json'),
32
37
  nonRuntimeRoots, sourceFiles: sourceFilesByScope.get(scope),
33
38
  })
@@ -0,0 +1,21 @@
1
+ // Dynamic package loaders often construct a node_modules path instead of using
2
+ // import/require, so they never become graph externalImports. Restrict this
3
+ // fallback to executable npm source: manifests merely declare every dependency
4
+ // and must never count as usage evidence.
5
+ const NPM_SOURCE_RE = /\.(?:[cm]?[jt]sx?|vue|svelte)$/i
6
+ const escRe = (value) => String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
7
+ const mentioned = (blob, name) => new RegExp(`(^|[^\\w@.-])${escRe(name)}($|[^\\w.-])`).test(blob)
8
+
9
+ export function createPackageSourceMatcher(sourceTexts = new Map()) {
10
+ const blob = [...sourceTexts]
11
+ .filter(([file]) => NPM_SOURCE_RE.test(String(file || '')))
12
+ .map(([, text]) => String(text || ''))
13
+ .join('\n')
14
+ return (name) => {
15
+ if (mentioned(blob, name)) return true
16
+ if (!name.startsWith('@') || !name.includes('/')) return false
17
+ const [namespace, base] = name.split('/', 2)
18
+ // join(root, "node_modules", "@scope", "package", ...)
19
+ return new RegExp(`["'\x60]${escRe(namespace)}["'\x60][\\s\\S]{0,96}?["'\x60]${escRe(base)}["'\x60]`).test(blob)
20
+ }
21
+ }
@@ -149,11 +149,17 @@ export function extractLargeStrings(text, { py = false, cs = false, minLines = 6
149
149
 
150
150
  // one raw token stream; the two modes differ only in identifier canonicalization
151
151
  export function tokenize(text) {
152
- const raw = text.match(/[A-Za-z_$][\w$]*|\d+(?:\.\d+)?|[^\s\w]/g) || [];
152
+ const raw = text.match(/[A-Za-z_$][\w$]*|(?:0[xX][\dA-Fa-f_]+|0[bB][01_]+|0[oO][0-7_]+|\d[\d_]*(?:\.\d[\d_]*)?(?:[eE][+-]?\d[\d_]*)?)n?|[^\s\w]/g) || [];
153
153
  const strict = [];
154
154
  const renamed = [];
155
155
  for (const t of raw) {
156
- if (/^\d/.test(t)) { strict.push("N"); renamed.push("N"); continue; }
156
+ if (/^\d/.test(t)) {
157
+ // Numeric constants are behavior, not renameable identifiers. Preserve
158
+ // their value so same-shape limit/config tables with different numbers do
159
+ // not become perfect clones; a small constant edit still leaves the rest
160
+ // of a genuine copied body highly similar.
161
+ strict.push(`N:${t}`); renamed.push(`N:${t}`); continue;
162
+ }
157
163
  if (/^[A-Za-z_$]/.test(t) && !KEYWORDS.has(t)) { strict.push(t); renamed.push("I"); continue; }
158
164
  strict.push(t); renamed.push(t);
159
165
  }
@@ -178,5 +184,9 @@ export function fingerprints(toks) {
178
184
  for (let j = i; j < Math.min(i + W, hashes.length); j++) if (hashes[j] < min) min = hashes[j];
179
185
  fp.add(min);
180
186
  }
187
+ // Winnowing can skip every changed literal in a repetitive policy table.
188
+ // Exact numeric anchors keep behavior-defining limits in the similarity
189
+ // denominator while leaving ordinary copied bodies tolerant of a small edit.
190
+ for (const token of toks) if (token.startsWith("N:")) fp.add(`numeric:${token}`);
181
191
  return fp;
182
192
  }
@@ -98,6 +98,26 @@ export function entryFiles(graph, pkgOrScopes, dynamicTargets = new Set(), { dec
98
98
  const f = n.source_file;
99
99
  if (ENTRY_FILE.test(f) || isFrameworkEntryFile(f) || /\.d\.[cm]?ts$/i.test(f) || TEST_FILE_RE.test(f) || /\.html?$/i.test(f) || pe.has(f) || /(^|\/)[^/]*\.config\.[a-z]+$/i.test(f)) entries.add(f);
100
100
  }
101
+ // Classic/static HTML pages load scripts, styles and other assets without a
102
+ // JavaScript import edge. Treat an exact local src/href target as an entry
103
+ // surface so a deployed asset is not mislabeled orphan/dead.
104
+ for (const [rawFile, rawText] of sources || []) {
105
+ const htmlFile = String(rawFile || "").replace(/\\/g, "/");
106
+ if (!/\.html?$/i.test(htmlFile)) continue;
107
+ const htmlDir = posix.dirname(htmlFile);
108
+ const attr = /\b(?:src|href)\s*=\s*["']([^"']+)["']/gi;
109
+ let match;
110
+ while ((match = attr.exec(String(rawText || "")))) {
111
+ const rawTarget = String(match[1] || "").trim().split(/[?#]/, 1)[0];
112
+ if (!rawTarget || /^(?:[a-z][a-z0-9+.-]*:|\/\/|#)/i.test(rawTarget)) continue;
113
+ const relative = rawTarget.replace(/^\/+/, "");
114
+ const candidates = rawTarget.startsWith("/")
115
+ ? [posix.normalize(posix.join(htmlDir, relative)), posix.normalize(relative)]
116
+ : [posix.normalize(posix.join(htmlDir, relative))];
117
+ const target = candidates.find((candidate) => fileSet.has(candidate));
118
+ if (target) entries.add(target);
119
+ }
120
+ }
101
121
  for (const evidence of springConventionEntries(sources, fileSet)) {
102
122
  entries.add(evidence.file);
103
123
  conventionEvidence.push(evidence);
@@ -86,6 +86,7 @@ export async function runInternalAudit(repoPath, {
86
86
  packageScopes,
87
87
  workspacePkgNames: workspacePkgNames(repoPath, pkg),
88
88
  configTexts,
89
+ sourceTexts: sources,
89
90
  nonRuntimeRoots,
90
91
  sourceFiles: [...sources.keys()],
91
92
  });
@@ -1,19 +1,8 @@
1
- import {createPathClassifier, hasPathClass} from '../path-classification.js'
1
+ import {PATH_CLASS_NAMES, PATH_CLASS_TASK_QUERY_TERMS, createPathClassifier, hasPathClass} from '../path-classification.js'
2
2
 
3
3
  const words = (value) => new Set(String(value || '').toLowerCase().match(/[\p{L}_$][\p{L}\p{N}_$-]{2,}/gu) || [])
4
4
  const fileOf = (node) => String(node?.source_file || (String(node?.id || '').includes('#') ? String(node.id).split('#')[0] : node?.id || '')).replace(/\\/g, '/')
5
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
6
 
18
7
  const INTENT_TRANSLATIONS = [
19
8
  [/авториз|аутентиф|логин|сесси|токен/iu, 'auth authentication login session token'],
@@ -64,7 +53,7 @@ export function retrieveTaskContext(g, {
64
53
  } = {}) {
65
54
  const expandedTask = expandTaskQuery(task)
66
55
  const taskWords = words(expandedTask)
67
- const requestedClasses = new Set(Object.entries(CLASS_TERMS)
56
+ const requestedClasses = new Set(Object.entries(PATH_CLASS_TASK_QUERY_TERMS)
68
57
  .filter(([, terms]) => terms.some((term) => taskWords.has(term)))
69
58
  .map(([name]) => name))
70
59
  if (requestedClasses.has('test')) requestedClasses.add('e2e')
@@ -77,7 +66,7 @@ export function retrieveTaskContext(g, {
77
66
  if (!file || changedFiles.has(file) || includeClassified === true) return true
78
67
  if (!classificationCache.has(file)) classificationCache.set(file, classifier.explain(file, {content: ''}))
79
68
  const info = classificationCache.get(file)
80
- const classes = NON_PRODUCT_CLASSES.filter((name) => hasPathClass(info, name))
69
+ const classes = PATH_CLASS_NAMES.filter((name) => hasPathClass(info, name))
81
70
  if (!info?.excluded && !classes.length) return true
82
71
  return classes.some((name) => requestedClasses.has(name))
83
72
  }
@@ -0,0 +1,8 @@
1
+ const compareText = (left, right) => String(left).localeCompare(String(right), 'en')
2
+
3
+ // Snapshot construction and hosted-payload validation must use the same stable
4
+ // member order or equivalent evidence can hash differently across the boundary.
5
+ export function compareDuplicateMember(left, right) {
6
+ return compareText(left.file, right.file) || left.startLine - right.startLine ||
7
+ left.endLine - right.endLine || compareText(left.graphNodeId || '', right.graphNodeId || '')
8
+ }
@@ -3,6 +3,7 @@ import {computeDuplicates} from '../analysis/duplicates.js'
3
3
  import {
4
4
  CAPS, STATE, VERDICT, bounded, compareText, graphId, repoRelativePath,
5
5
  } from './evidence-snapshot.common.mjs'
6
+ import {compareDuplicateMember} from './evidence/duplicate-member-order.mjs'
6
7
 
7
8
  const CLONE_MIN_SIMILARITY = 80
8
9
  const DIVERGENCE_MAX_SIMILARITY = 45
@@ -51,11 +52,6 @@ function memberIdentity(value) {
51
52
  return `${value.file}\0${value.startLine}\0${value.endLine}\0${value.graphNodeId || ''}`
52
53
  }
53
54
 
54
- function compareMember(a, b) {
55
- return compareText(a.file, b.file) || a.startLine - b.startLine || a.endLine - b.endLine ||
56
- compareText(a.graphNodeId || '', b.graphNodeId || '')
57
- }
58
-
59
55
  function eligibleMembers(fragments) {
60
56
  const out = new Map()
61
57
  for (let index = 0; index < fragments.length; index++) {
@@ -107,7 +103,7 @@ function cloneGroups(data, membersByIndex) {
107
103
  const value = membersByIndex.get(index)
108
104
  unique.set(memberIdentity(value), value)
109
105
  }
110
- const members = [...unique.values()].sort(compareMember)
106
+ const members = [...unique.values()].sort(compareDuplicateMember)
111
107
  if (members.length < 2) continue
112
108
  const indexSet = new Set(indices)
113
109
  const similarities = links
@@ -141,7 +137,7 @@ function divergenceCandidates(data, membersByIndex) {
141
137
  if (!Number.isInteger(left) || !Number.isInteger(right) || left === right || !leftMember || !rightMember ||
142
138
  leftMember.file === rightMember.file || !Number.isFinite(similarity) ||
143
139
  similarity < 0 || similarity > DIVERGENCE_MAX_SIMILARITY) continue
144
- const members = [leftMember, rightMember].sort(compareMember)
140
+ const members = [leftMember, rightMember].sort(compareDuplicateMember)
145
141
  pairs.push({
146
142
  similarity: Math.trunc(similarity),
147
143
  totalTokens: members[0].tokens + members[1].tokens,
@@ -0,0 +1,10 @@
1
+ import {spawnSync} from 'node:child_process'
2
+ import {childProcessEnv} from '../child-env.js'
3
+
4
+ export function gitLines(repoRoot, args) {
5
+ const result = spawnSync('git', ['-C', repoRoot, ...args], {
6
+ encoding: 'utf8', timeout: 8000, env: childProcessEnv(), windowsHide: true,
7
+ })
8
+ if (result.status !== 0) return null
9
+ return String(result.stdout || '').split(/\r?\n/).map((line) => line.trim()).filter(Boolean)
10
+ }
@@ -1,4 +1,4 @@
1
- import {createPathClassifier, hasPathClass} from '../../path-classification.js'
1
+ import {PATH_CLASS_NAMES, PATH_CLASS_QUERY_TERMS, createPathClassifier, hasPathClass} from '../../path-classification.js'
2
2
  import {degreeOf, isSymbol, uniqueConnCount} from './context-core.mjs'
3
3
 
4
4
  const QUERY_STOP = new Set('a an and are around architecture best code do does exact explain find focus focused for from how identify in inspect inspection is logic me of or path production project repository request requests rest show symbol symbols the through to trace what where which with'.split(' '))
@@ -17,17 +17,6 @@ const TOOL_EXECUTION_TERMS = QUERY_INTENTS.find(([id]) => id === 'tool-execution
17
17
  const TOOL_EXECUTION_TRIGGERS = new Set(['tool', 'tools', 'tooling', 'mcp'])
18
18
  const wordsOf = (value) => String(value ?? '').replace(/([a-z0-9])([A-Z])/g, '$1 $2').toLowerCase().split(/[^a-z0-9_]+/).filter(Boolean)
19
19
  const normPath = (value) => String(value ?? '').replace(/\\/g, '/').replace(/^\.\//, '').toLowerCase()
20
- const NON_PRODUCT_CLASSES = Object.freeze(['test', 'e2e', 'generated', 'mock', 'story', 'docs', 'benchmark', 'temp'])
21
- const CLASS_QUERY_TERMS = Object.freeze({
22
- test: ['test', 'tests', 'testing', 'unit'],
23
- e2e: ['e2e', 'playwright', 'cypress'],
24
- generated: ['generated', 'autogenerated', 'dist'],
25
- mock: ['mock', 'mocks', 'fixture', 'fixtures', 'fake'],
26
- story: ['story', 'stories', 'storybook'],
27
- docs: ['doc', 'docs', 'documentation', 'readme', 'guide'],
28
- benchmark: ['benchmark', 'benchmarks', 'bench'],
29
- temp: ['temp', 'temporary', 'tmp'],
30
- })
31
20
  const CODE_FILE_RE = /\.(?:[cm]?[jt]sx?|py|go|java|rs|kt|kts|cs|rb|php)$/i
32
21
  const DATA_OR_PROSE_RE = /\.(?:json|ya?ml|toml|ini|md|mdx|rst|adoc|html?|css|scss|less|svg)$/i
33
22
  const LANGUAGE_EXTENSIONS = Object.freeze({
@@ -56,7 +45,7 @@ const matchesLanguage = (node, extensions) => {
56
45
 
57
46
  export function requestedPathClasses(query) {
58
47
  const words = new Set(wordsOf(query)), requested = new Set()
59
- for (const [category, terms] of Object.entries(CLASS_QUERY_TERMS)) if (terms.some((term) => words.has(term))) requested.add(category)
48
+ for (const [category, terms] of Object.entries(PATH_CLASS_QUERY_TERMS)) if (terms.some((term) => words.has(term))) requested.add(category)
60
49
  if (requested.has('test')) requested.add('e2e')
61
50
  if (requested.has('e2e')) requested.add('test')
62
51
  return requested
@@ -67,7 +56,7 @@ function isQueryEligible(node, requestedClasses, classificationCache, classifier
67
56
  if (!source) return true
68
57
  if (!classificationCache.has(source)) classificationCache.set(source, classifier.explain(source, {content: ''}))
69
58
  const info = classificationCache.get(source)
70
- const classified = NON_PRODUCT_CLASSES.filter((category) => hasPathClass(info, category))
59
+ const classified = PATH_CLASS_NAMES.filter((category) => hasPathClass(info, category))
71
60
  return classified.length === 0 || classified.some((category) => requestedClasses.has(category))
72
61
  }
73
62
 
@@ -0,0 +1,42 @@
1
+ import {isStructuralRelation} from '../../graph/relations.js'
2
+
3
+ // Shared reverse dependency walk for get_dependents and change_impact. Keep one
4
+ // representation so runtime/compile-time depth and edge provenance cannot drift
5
+ // between the two public tools.
6
+ export function reverseReach(g, seeds, maxDepth) {
7
+ const states = new Map([...seeds].map((id) => [String(id), {
8
+ runtimeDepth: 0, runtimeRelation: null, runtimeProvenance: null,
9
+ compileDepth: null, compileRelation: null, compileProvenance: null,
10
+ }]))
11
+ const frontier = [...seeds].map((id) => ({id: String(id), depth: 0, compileOnly: false}))
12
+ for (let cursor = 0; cursor < frontier.length; cursor++) {
13
+ const current = frontier[cursor]
14
+ if (current.depth >= maxDepth) continue
15
+ for (const edge of g.inn.get(current.id) || []) {
16
+ if (isStructuralRelation(edge.relation) || edge.barrelProxy === true) continue
17
+ const id = String(edge.id)
18
+ const compileOnly = current.compileOnly || edge.typeOnly === true || edge.compileOnly === true
19
+ const depth = current.depth + 1
20
+ const entry = states.get(id) || {
21
+ runtimeDepth: null, runtimeRelation: null, runtimeProvenance: null,
22
+ compileDepth: null, compileRelation: null, compileProvenance: null,
23
+ }
24
+ const depthKey = compileOnly ? 'compileDepth' : 'runtimeDepth'
25
+ const relationKey = compileOnly ? 'compileRelation' : 'runtimeRelation'
26
+ const provenanceKey = compileOnly ? 'compileProvenance' : 'runtimeProvenance'
27
+ if (entry[depthKey] != null && entry[depthKey] <= depth) continue
28
+ entry[depthKey] = depth
29
+ entry[relationKey] = edge.relation || 'rel'
30
+ entry[provenanceKey] = edge.provenance || 'UNKNOWN'
31
+ states.set(id, entry)
32
+ frontier.push({id, depth, compileOnly})
33
+ }
34
+ }
35
+ return new Map([...states].map(([id, entry]) => [id, {
36
+ ...entry,
37
+ depth: entry.runtimeDepth ?? entry.compileDepth ?? 0,
38
+ compileOnly: entry.runtimeDepth == null,
39
+ relation: entry.runtimeDepth != null ? entry.runtimeRelation : entry.compileRelation,
40
+ provenance: entry.runtimeDepth != null ? entry.runtimeProvenance : entry.compileProvenance,
41
+ }]))
42
+ }
@@ -2,6 +2,7 @@ import {
2
2
  CAPS, DUPLICATE_THRESHOLDS, bool, compare, count, graphId, int, list,
3
3
  path, reasons, state, text, verdict,
4
4
  } from './evidence-common.mjs'
5
+ import {compareDuplicateMember} from '../evidence/duplicate-member-order.mjs'
5
6
 
6
7
  function duplicateEvidenceId(value) {
7
8
  const id = text(value, 64)
@@ -19,10 +20,6 @@ function duplicateMember(value) {
19
20
  return out
20
21
  }
21
22
 
22
- function compareDuplicateMember(a, b) {
23
- return compare(a.file, b.file) || a.startLine - b.startLine || a.endLine - b.endLine || compare(a.graphNodeId || '', b.graphNodeId || '')
24
- }
25
-
26
23
  function duplicateMembers(values, cap = CAPS.duplicateMembers) {
27
24
  const raw = Array.isArray(values) ? values : []
28
25
  const unique = new Map()
@@ -5,16 +5,11 @@ import {childProcessEnv} from '../child-env.js'
5
5
  import {classifyChangeImpact} from '../analysis/change-classification.js'
6
6
  import {readCoverageForRepo} from '../analysis/coverage-reports.js'
7
7
  import {computeStaticTestReachability} from '../analysis/static-test-reachability.js'
8
- import {isStructuralRelation} from '../graph/relations.js'
9
8
  import {degreeOf, labelOf, rawGraph} from './graph-context.mjs'
9
+ import {reverseReach} from './graph/reverse-reach.mjs'
10
+ import {gitLines} from './git-output.mjs'
10
11
  import {toolResult} from './tool-result.mjs'
11
12
 
12
- function gitLines(repoRoot, args) {
13
- const result = spawnSync('git', ['-C', repoRoot, ...args], {encoding: 'utf8', timeout: 8000, env: childProcessEnv()})
14
- if (result.status !== 0) return null
15
- return String(result.stdout || '').split(/\r?\n/).map((line) => line.trim()).filter(Boolean)
16
- }
17
-
18
13
  function gitValue(repoRoot, args) {
19
14
  const result = spawnSync('git', ['-C', repoRoot, ...args], {encoding: 'utf8', timeout: 8000, env: childProcessEnv()})
20
15
  if (result.status !== 0) return null
@@ -30,37 +25,6 @@ function resolveImpactBase(repoRoot, requested) {
30
25
  return null
31
26
  }
32
27
 
33
- function reverseReach(g, seeds, maxDepth) {
34
- const states = new Map([...seeds].map((id) => [String(id), {
35
- runtimeDepth: 0, runtimeRelation: null, compileDepth: null, compileRelation: null,
36
- }]))
37
- const frontier = [...seeds].map((id) => ({id: String(id), depth: 0, compileOnly: false}))
38
- for (let cursor = 0; cursor < frontier.length; cursor++) {
39
- const current = frontier[cursor]
40
- if (current.depth >= maxDepth) continue
41
- for (const edge of g.inn.get(current.id) || []) {
42
- if (isStructuralRelation(edge.relation) || edge.barrelProxy === true) continue
43
- const id = String(edge.id)
44
- const compileOnly = current.compileOnly || edge.typeOnly === true || edge.compileOnly === true
45
- const depth = current.depth + 1
46
- const entry = states.get(id) || {runtimeDepth: null, runtimeRelation: null, compileDepth: null, compileRelation: null}
47
- const depthKey = compileOnly ? 'compileDepth' : 'runtimeDepth'
48
- const relationKey = compileOnly ? 'compileRelation' : 'runtimeRelation'
49
- if (entry[depthKey] != null && entry[depthKey] <= depth) continue
50
- entry[depthKey] = depth
51
- entry[relationKey] = edge.relation || 'rel'
52
- states.set(id, entry)
53
- frontier.push({id, depth, compileOnly})
54
- }
55
- }
56
- return new Map([...states].map(([id, entry]) => [id, {
57
- ...entry,
58
- depth: entry.runtimeDepth ?? entry.compileDepth ?? 0,
59
- compileOnly: entry.runtimeDepth == null,
60
- relation: entry.runtimeDepth != null ? entry.runtimeRelation : entry.compileRelation,
61
- }]))
62
- }
63
-
64
28
  const impactKind = (entry) => entry?.runtimeDepth != null
65
29
  ? (entry.compileDepth != null ? `runtime + compile-time(d${entry.compileDepth})` : 'runtime')
66
30
  : 'compile-time'
@@ -9,46 +9,11 @@ import {
9
9
  rawGraph, prevGraphPathFor, edgeEndpoint, diffGraphs, formatGraphDiff,
10
10
  } from './graph-context.mjs'
11
11
  import {readCoverageForRepo} from '../analysis/coverage-reports.js'
12
- import {isStructuralRelation} from '../graph/relations.js'
12
+ import {reverseReach} from './graph/reverse-reach.mjs'
13
+ import {gitLines} from './git-output.mjs'
13
14
  import {tChangeImpactV2} from './tools-impact-change.mjs'
14
15
  import {buildGraphAtGitRef} from '../analysis/git-ref-graph.js'
15
16
 
16
- function reverseReach(g, seeds, maxDepth) {
17
- const states = new Map([...seeds].map((id) => [String(id), {
18
- runtimeDepth: 0, runtimeRelation: null, compileDepth: null, compileRelation: null,
19
- }]))
20
- const frontier = [...seeds].map((id) => ({id: String(id), depth: 0, compileOnly: false}))
21
- for (let cursor = 0; cursor < frontier.length; cursor++) {
22
- const current = frontier[cursor]
23
- if (current.depth >= maxDepth) continue
24
- for (const e of g.inn.get(current.id) || []) {
25
- if (isStructuralRelation(e.relation) || e.barrelProxy === true) continue
26
- const id = String(e.id)
27
- const compileOnly = current.compileOnly || e.typeOnly === true || e.compileOnly === true
28
- const depth = current.depth + 1
29
- const entry = states.get(id) || {
30
- runtimeDepth: null, runtimeRelation: null, compileDepth: null, compileRelation: null,
31
- }
32
- const depthKey = compileOnly ? 'compileDepth' : 'runtimeDepth'
33
- const relationKey = compileOnly ? 'compileRelation' : 'runtimeRelation'
34
- const provenanceKey = compileOnly ? 'compileProvenance' : 'runtimeProvenance'
35
- if (entry[depthKey] != null && entry[depthKey] <= depth) continue
36
- entry[depthKey] = depth
37
- entry[relationKey] = e.relation || 'rel'
38
- entry[provenanceKey] = e.provenance || 'UNKNOWN'
39
- states.set(id, entry)
40
- frontier.push({id, depth, compileOnly})
41
- }
42
- }
43
- return new Map([...states].map(([id, entry]) => [id, {
44
- ...entry,
45
- depth: entry.runtimeDepth ?? entry.compileDepth ?? 0,
46
- compileOnly: entry.runtimeDepth == null,
47
- relation: entry.runtimeDepth != null ? entry.runtimeRelation : entry.compileRelation,
48
- provenance: entry.runtimeDepth != null ? entry.runtimeProvenance : entry.compileProvenance,
49
- }]))
50
- }
51
-
52
17
  const impactKind = (entry) => {
53
18
  if (entry?.runtimeDepth != null) {
54
19
  return entry.compileDepth != null ? `runtime + compile-time(d${entry.compileDepth})` : 'runtime'
@@ -141,12 +106,6 @@ export async function tGraphDiff(g, args, ctx) {
141
106
  }
142
107
 
143
108
  // ---- change impact -------------------------------------------------------------------------------
144
- function gitLines(repoRoot, args) {
145
- const res = spawnSync('git', ['-C', repoRoot, ...args], {encoding: 'utf8', timeout: 8000, env: childProcessEnv()})
146
- if (res.status !== 0) return null
147
- return String(res.stdout || '').split(/\r?\n/).map((s) => s.trim()).filter(Boolean)
148
- }
149
-
150
109
  function resolveImpactBase(repoRoot, requested) {
151
110
  const candidates = requested ? [requested] : ['origin/HEAD', 'origin/main', 'origin/master', 'main', 'master']
152
111
  for (const ref of candidates) {
@@ -5,6 +5,20 @@ import { openSync, closeSync, readSync, readFileSync, statSync } from "node:fs";
5
5
  import { createRepoBoundary } from "./repo-path.js";
6
6
 
7
7
  export const PATH_CLASS_NAMES = Object.freeze(["test", "e2e", "generated", "mock", "story", "docs", "benchmark", "temp"]);
8
+ export const PATH_CLASS_QUERY_TERMS = Object.freeze({
9
+ test: Object.freeze(["test", "tests", "testing", "unit"]),
10
+ e2e: Object.freeze(["e2e", "playwright", "cypress"]),
11
+ generated: Object.freeze(["generated", "autogenerated", "dist"]),
12
+ mock: Object.freeze(["mock", "mocks", "fixture", "fixtures", "fake"]),
13
+ story: Object.freeze(["story", "stories", "storybook"]),
14
+ docs: Object.freeze(["doc", "docs", "documentation", "readme", "guide"]),
15
+ benchmark: Object.freeze(["benchmark", "benchmarks", "bench"]),
16
+ temp: Object.freeze(["temp", "temporary", "tmp"]),
17
+ });
18
+ export const PATH_CLASS_TASK_QUERY_TERMS = Object.freeze({
19
+ ...PATH_CLASS_QUERY_TERMS,
20
+ test: Object.freeze([...PATH_CLASS_QUERY_TERMS.test, "spec", "coverage", "verify"]),
21
+ });
8
22
  const PATH_CLASS_SET = new Set(PATH_CLASS_NAMES);
9
23
  const MAX_CONFIG_BYTES = 64 * 1024;
10
24
  const MAX_RULES_PER_CLASS = 64;
@@ -74,7 +74,7 @@ export async function rgSweep(rg, contentRoots, timeoutMs) {
74
74
  const lineText = String(obj.data?.lines?.text || "");
75
75
  const subText = String(obj.data?.submatches?.[0]?.match?.text || "");
76
76
  for (const at of matchRuleOffsets(rule, lineText, subText)) {
77
- const text = (lineText ? lineText.slice(Math.max(0, at - 60), at + 180) : subText).trim().slice(0, 240);
77
+ const text = (lineText ? lineText.slice(Math.max(0, at - 160), at + 180) : subText).trim().slice(0, 340);
78
78
  hits.push({ pkg, file, line: obj.data?.line_number || 0, text, rule });
79
79
  }
80
80
  }
@@ -88,6 +88,7 @@ const BENIGN_URL_HOSTS = [
88
88
  "apache.org", "opensource.org", "gnu.org", "creativecommons.org", "choosealicense.com",
89
89
  "mozilla.org", "nodejs.org", "npmjs.com", "npmjs.org", "yarnpkg.com", "python.org", "pypi.org",
90
90
  "golang.org", "go.dev", "godoc.org", "readthedocs.io", "readthedocs.org", "editorconfig.org",
91
+ "aka.ms", "example.com", "example.org", "example.net",
91
92
  ];
92
93
  const DOC_URL_PATH_RE = /\/(docs?|documentation|wiki|issues?|pull|blob|tree|releases|licenses?|rfc\d*|help|manual|guide|spec|schemas?)([/#?.]|$)/i;
93
94
  const URL_TOKEN_RE = /https?:\/\/[^\s'"`)\\<>]+/gi;
@@ -109,7 +110,7 @@ export function isBenignUrlContext(text) {
109
110
  const s = String(text || "");
110
111
  const urls = [...s.matchAll(URL_TOKEN_RE)];
111
112
  if (!urls.length) return false;
112
- if (COMMENT_MARKER_RE.test(s.slice(Math.max(0, urls[0].index - 80), urls[0].index))) return true;
113
+ if (COMMENT_MARKER_RE.test(s.slice(0, urls[0].index))) return true;
113
114
  return urls.every((m) => {
114
115
  const pre = s.slice(Math.max(0, m.index - 80), m.index);
115
116
  const post = s.slice(m.index + m[0].length, m.index + m[0].length + 48);
@@ -14,7 +14,7 @@ export const MALWARE_ALLOWLIST = new Set([
14
14
  // webhook.site / oast.* / canarytokens / dnslog.cn: Shai-Hulud-era exfil + OOB-callback services.
15
15
  const EXFIL_HOSTS = "discord(app)?\\.com/api/webhooks|hooks\\.slack\\.com/services|api\\.telegram\\.org/bot|pastebin\\.com/raw|burpcollaborator|oastify\\.com|oast\\.(pro|live|fun|me|site|online)|interact\\.sh|pipedream\\.net|requestbin|webhook\\.site|canarytokens\\.(com|org)|dnslog\\.cn";
16
16
  // external URL that is NOT an exfil endpoint (owned by exfil-url) and NOT a raw IP (owned by exfil-ip)
17
- const PLAIN_EXTERNAL_URL = `https?://(?!(${EXFIL_HOSTS})|[0-9]{1,3}\\.|localhost\\b|127\\.|0\\.0\\.0\\.0|10\\.|192\\.168\\.|169\\.254\\.|172\\.(1[6-9]|2\\d|3[01])\\.)[^'"\`\\s)\\\\]+`;
17
+ const PLAIN_EXTERNAL_URL = `https?://(?!(${EXFIL_HOSTS})|[0-9]{1,3}\\.|localhost\\b|127\\.|0\\.0\\.0\\.0|10\\.|192\\.168\\.|169\\.254\\.|172\\.(1[6-9]|2\\d|3[01])\\.)[a-z0-9][^'"\`\\s)\\\\]+`;
18
18
 
19
19
  // ---- content rules (scanned over node_modules file text; `pattern` is ripgrep-safe ERE, `re` is the
20
20
  // JS validator used both to classify rg hits and by the no-rg fallback) ----
@@ -181,8 +181,8 @@ export const CONTENT_RULES = [
181
181
  severity: "low", // minified/legit code look-alikes — only escalates with a second signal
182
182
  nearZeroFp: false,
183
183
  noisy: true,
184
- pattern: "eval\\(atob\\(|eval\\(Buffer\\.from\\(|_0x[a-f0-9]{4}",
185
- re: /eval\(atob\(|eval\(Buffer\.from\([^)]*base64|_0x[a-f0-9]{4,}/,
184
+ pattern: "eval\\(atob\\(|eval\\(Buffer\\.from\\(|\\b_0x[a-f0-9]{4,}\\b",
185
+ re: /eval\(atob\(|eval\(Buffer\.from\([^)]*base64|\b_0x[a-f0-9]{4,}\b/,
186
186
  what: "obfuscation marker (eval-of-decoded payload / string-array obfuscator)",
187
187
  },
188
188
  ];
@@ -1,93 +0,0 @@
1
- # Weavatrix 0.2.14
2
-
3
- 0.2.14 is a trust and maintainability release found by dogfooding the MCP on a Go BGP service and a
4
- Java event-driven application. It keeps the 38-tool surface and the local-first/Hosted wire boundary,
5
- while closing false dead-code, false clean-health, noisy flow, weak architecture-bootstrap, and stale
6
- daemon failure modes.
7
-
8
- ## Go receiver-aware call flow
9
-
10
- - Go call resolution now carries receiver types from function parameters, explicit and inferred
11
- locals, constructor return types (including the first value of `value, err := pkg.New()`), imported
12
- package types, and struct fields.
13
- - Calls such as `bgpSpeaker.RemoveMitigator(update)` resolve to
14
- `(*Speaker).RemoveMitigator` instead of leaving the method falsely test-only.
15
- - Unknown or ambiguous same-name receivers still fail closed rather than inventing a call edge.
16
-
17
- ## Focused generic flows
18
-
19
- - `query_graph` accepts exact `seed_symbols`, a `relation_filter`, and
20
- `flow_direction: forward|backward|both`. The same bounded graph can now trace Kafka listeners,
21
- queues, workers, cron jobs, CLI entry points, and other event/data flows without one tool per
22
- framework.
23
- - Exact seeds remain exclusive unless `augment_seeds:true` is requested. CamelCase identifiers such
24
- as `FlowSpec` no longer opt tests into production traversal merely because they contain `spec`.
25
- - `context_bundle` ranks production callers before classified test callers and uses diverse,
26
- non-overlapping excerpts around decisive call sites.
27
-
28
- ## Honest Health and endpoint state
29
-
30
- - `run_audit` returns an explicit capability matrix for structure, dependencies, runtime correctness,
31
- concurrency, advisories, malware scanning, and measured coverage. Each row has independent status
32
- and completeness.
33
- - Maven and Gradle manifests are detected and inventoried, but unsupported import-to-artifact
34
- verification returns `NOT_SUPPORTED`/`PARTIAL` instead of a misleading complete zero.
35
- - Conservative source checks can flag selected Go fixed-index slice/discriminator hazards and Java
36
- interruption/retry candidates, including conditional `while` and `do/while` loops with failure plus
37
- wait behavior but no visible attempt/deadline/cancellation policy. The result explicitly does not
38
- claim compiler, runtime, CFG, race, or full concurrency proof.
39
- - Ordinary Go grouped imports (`import (...)`) no longer trigger the JavaScript dynamic-import caveat;
40
- real Go reflection evidence remains visible independently.
41
- - Spring endpoint inventory includes conditional activation evidence and whether a controller is
42
- inactive by default.
43
-
44
- ## Safe target-architecture bootstrap
45
-
46
- - The starter discovers adaptive Maven/Gradle source roots and nested monorepo product territories
47
- instead of grouping an entire Java application under its first directory. Oversized Java package
48
- branches split recursively at real child packages, while a large flat package remains an honest
49
- size hotspot instead of being divided into invented components.
50
- - Observed dependency directions are proposed separately as `OBSERVED_NOT_ENFORCED`; inventory is not
51
- silently promoted to intended architecture.
52
- - Only universal starter guards (`runtimeCycles=0`, `maxFileLoc=300`) are active. Generic function,
53
- complexity, cohesion, and boundary thresholds are returned as `CANDIDATE_NOT_ENFORCED` so a first
54
- preview does not manufacture hundreds of policy violations.
55
- - `maxFileLoc` now reads physical file LOC stored on file nodes, not the last declaration's end line;
56
- older graphs rebuild before this budget can report a pass.
57
- - `get_architecture_contract action=preview` returns the materialized candidate, verification, exact
58
- file content/hash, and a short-lived token without writing. `action=approve` rechecks repository and
59
- graph identity, then creates only a missing `.weavatrix/architecture.json` with the exact reviewed
60
- content. Existing policy is never overwritten.
61
- - `baseline_mode=accept-current` is an explicit owner action for ratcheting accepted current debt; the
62
- default records no baseline findings.
63
-
64
- ## Runtime and codebase integrity
65
-
66
- - The MCP compares its running version with the package version on disk. A mismatch blocks
67
- `initialize`, `tools/list`, and tool calls with `STALE_RUNTIME` until the client reconnects. The
68
- environment override is reserved for deliberate source development.
69
- - Large MCP, graph, analysis, precision, and test modules were split into owner-focused folders with
70
- stable hot-reload facades. The watcher and cache-busting chain include the new graph, Health,
71
- action, and architecture owner modules, so supported tool edits still go live without reconnecting.
72
- - The public page keeps the same design and continuous canvas graph animation while CSS and animation
73
- code move into focused assets. A release test enforces a physical 300-line ceiling for
74
- JavaScript/TypeScript under `src`, `bin`, `scripts`, and `test`, plus site HTML/CSS/JavaScript.
75
- - The 38-tool catalog and profile counts remain unchanged: 31 pinned, 34 offline, 35 OSV, and 38
76
- hosted/full.
77
-
78
- ## Compatibility and security
79
-
80
- - Hosted payload v2/v3, `preview_sync` confirmation, and architecture-contract transport are
81
- unchanged. This release does not modify the separate Hosted frontend/backend application.
82
- - Local architecture approval writes only inside the active repository boundary, uses an exclusive
83
- create, expires and consumes tokens, rejects repository/graph drift, and refuses an existing target.
84
- - Source bodies, snippets, absolute paths, credentials, environment values, and Git remotes remain
85
- excluded from Hosted sync payloads.
86
-
87
- ## Verification
88
-
89
- The release is gated by the full Node suite (538 tests: 535 passed and three platform skips), the
90
- six-language fixture benchmark, a 6/6 real-repository semantic-regression gate, zero npm audit
91
- vulnerabilities, packed npm and MCPB stdio checks, plus direct Go and Java dogfooding. Cold timings
92
- vary with machine load and repository snapshots; the public README keeps a dated representative
93
- run instead of presenting one release machine's timing as a universal result.