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 +16 -0
- package/SECURITY.md +2 -2
- package/docs/releases/v0.2.15.md +37 -0
- package/package.json +2 -2
- package/src/analysis/dep-check.js +8 -18
- package/src/analysis/dependency/conventions.js +16 -0
- package/src/analysis/dependency/scoped-dependencies.js +7 -2
- package/src/analysis/dependency/source-references.js +21 -0
- package/src/analysis/duplicates.tokenize.js +12 -2
- package/src/analysis/internal-audit.reach.js +20 -0
- package/src/analysis/internal-audit.run.js +1 -0
- package/src/analysis/task-retrieval.js +3 -14
- package/src/mcp/evidence/duplicate-member-order.mjs +8 -0
- package/src/mcp/evidence-snapshot.duplicates.mjs +3 -7
- package/src/mcp/git-output.mjs +10 -0
- package/src/mcp/graph/context-seeds.mjs +3 -14
- package/src/mcp/graph/reverse-reach.mjs +42 -0
- package/src/mcp/sync/evidence-duplicates.mjs +1 -4
- package/src/mcp/tools-impact-change.mjs +2 -38
- package/src/mcp/tools-impact.mjs +2 -43
- package/src/path-classification.js +14 -0
- package/src/security/malware-heuristics.sweep.js +1 -1
- package/src/security/registry-sig.classify.js +2 -1
- package/src/security/registry-sig.rules.js +3 -3
- package/docs/releases/v0.2.14.md +0 -93
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.
|
|
8
|
-
| `0.2.
|
|
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.
|
|
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.
|
|
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$]
|
|
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)) {
|
|
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);
|
|
@@ -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(
|
|
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 =
|
|
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(
|
|
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(
|
|
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(
|
|
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 =
|
|
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'
|
package/src/mcp/tools-impact.mjs
CHANGED
|
@@ -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 {
|
|
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 -
|
|
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(
|
|
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\\(
|
|
185
|
-
re: /eval\(atob\(|eval\(Buffer\.from\([^)]*base64
|
|
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
|
];
|
package/docs/releases/v0.2.14.md
DELETED
|
@@ -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.
|