weavatrix 0.2.13 → 0.2.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +83 -39
- package/SECURITY.md +2 -2
- package/docs/releases/v0.2.14.md +93 -0
- package/package.json +2 -2
- package/skill/SKILL.md +57 -38
- package/src/analysis/architecture/contract-graph.js +119 -0
- package/src/analysis/architecture/contract-schema.js +110 -0
- package/src/analysis/architecture/contract-storage.js +35 -0
- package/src/analysis/architecture/contract-verification.js +168 -0
- package/src/analysis/architecture-contract.js +16 -343
- package/src/analysis/change-classification/diff-parser.js +103 -0
- package/src/analysis/change-classification/options.js +40 -0
- package/src/analysis/change-classification/symbol-classifier.js +177 -0
- package/src/analysis/change-classification.js +120 -519
- package/src/analysis/dead-code-review/policy.js +23 -0
- package/src/analysis/dead-code-review.js +9 -19
- package/src/analysis/dep-check.js +10 -57
- package/src/analysis/dep-rules.js +10 -303
- package/src/analysis/dependency/scoped-dependencies.js +40 -0
- package/src/analysis/endpoints/common.js +62 -0
- package/src/analysis/endpoints/extract.js +107 -0
- package/src/analysis/endpoints/inventory.js +84 -0
- package/src/analysis/endpoints/mounts.js +129 -0
- package/src/analysis/endpoints-java.js +80 -3
- package/src/analysis/endpoints.js +3 -448
- package/src/analysis/git-history/analytics.js +177 -0
- package/src/analysis/git-history/collector.js +109 -0
- package/src/analysis/git-history/options.js +68 -0
- package/src/analysis/git-history.js +128 -577
- package/src/analysis/health-capabilities.js +82 -0
- package/src/analysis/http-contracts/analysis.js +169 -0
- package/src/analysis/http-contracts/client-call-detection.js +81 -0
- package/src/analysis/http-contracts/client-call-parser.js +255 -0
- package/src/analysis/http-contracts/graph-context.js +143 -0
- package/src/analysis/http-contracts/matching.js +61 -0
- package/src/analysis/http-contracts/shared.js +86 -0
- package/src/analysis/http-contracts.js +6 -822
- package/src/analysis/internal-audit/dependency-health.js +111 -0
- package/src/analysis/internal-audit/python-manifests.js +65 -0
- package/src/analysis/internal-audit/repo-files.js +55 -0
- package/src/analysis/internal-audit/supply-chain.js +132 -0
- package/src/analysis/internal-audit.collect.js +5 -106
- package/src/analysis/internal-audit.run.js +113 -200
- package/src/analysis/jvm-dependency-evidence.js +69 -0
- package/src/analysis/source-correctness/retry-patterns.js +93 -0
- package/src/analysis/source-correctness.js +225 -0
- package/src/analysis/structure/dependency-graph.js +156 -0
- package/src/analysis/structure/findings.js +142 -0
- package/src/graph/builder/go-receiver-resolution.js +112 -0
- package/src/graph/builder/js/queries.js +48 -0
- package/src/graph/builder/lang-go.js +89 -4
- package/src/graph/builder/lang-js.js +4 -44
- package/src/graph/builder/pass2-resolution.js +68 -0
- package/src/graph/freshness-probe.js +2 -1
- package/src/graph/incremental-refresh.js +3 -2
- package/src/graph/internal-builder.build.js +22 -183
- package/src/graph/internal-builder.pass2.js +161 -0
- package/src/graph/internal-builder.resolvers.js +2 -105
- package/src/graph/resolvers/rust.js +117 -0
- package/src/mcp/actions/advisories.mjs +18 -0
- package/src/mcp/actions/graph-lifecycle.mjs +148 -0
- package/src/mcp/actions/graph-sync.mjs +195 -0
- package/src/mcp/actions/hosted-architecture.mjs +57 -0
- package/src/mcp/architecture-bootstrap.mjs +168 -0
- package/src/mcp/architecture-starter.mjs +234 -0
- package/src/mcp/catalog.mjs +20 -6
- package/src/mcp/evidence/bun-lock-graph.mjs +209 -0
- package/src/mcp/evidence/package-graph-common.mjs +115 -0
- package/src/mcp/evidence/package-lock-graph.mjs +92 -0
- package/src/mcp/evidence-snapshot.package-graph.mjs +5 -474
- package/src/mcp/graph/context-core.mjs +111 -0
- package/src/mcp/graph/context-seeds.mjs +208 -0
- package/src/mcp/graph/context-state.mjs +86 -0
- package/src/mcp/graph/tools-core.mjs +143 -0
- package/src/mcp/graph/tools-query.mjs +223 -0
- package/src/mcp/graph-context.mjs +4 -496
- package/src/mcp/health/audit-format.mjs +172 -0
- package/src/mcp/health/audit.mjs +171 -0
- package/src/mcp/health/dead-code.mjs +110 -0
- package/src/mcp/health/duplicates.mjs +83 -0
- package/src/mcp/health/endpoints.mjs +42 -0
- package/src/mcp/health/structure.mjs +219 -0
- package/src/mcp/runtime-version.mjs +32 -0
- package/src/mcp/server/auto-refresh.mjs +66 -0
- package/src/mcp/server/runtime-config.mjs +43 -0
- package/src/mcp/server/shutdown.mjs +40 -0
- package/src/mcp/sync/evidence-architecture.mjs +54 -0
- package/src/mcp/sync/evidence-common.mjs +88 -0
- package/src/mcp/sync/evidence-duplicates.mjs +100 -0
- package/src/mcp/sync/evidence-health.mjs +56 -0
- package/src/mcp/sync/evidence-packages.mjs +95 -0
- package/src/mcp/sync/payload-common.mjs +97 -0
- package/src/mcp/sync/payload-v2.mjs +53 -0
- package/src/mcp/sync/payload-v3.mjs +79 -0
- package/src/mcp/sync-evidence.mjs +7 -402
- package/src/mcp/sync-payload.mjs +10 -244
- package/src/mcp/tools-actions.mjs +18 -414
- package/src/mcp/tools-architecture.mjs +18 -70
- package/src/mcp/tools-context.mjs +56 -15
- package/src/mcp/tools-endpoints.mjs +4 -1
- package/src/mcp/tools-graph.mjs +17 -331
- package/src/mcp/tools-health.mjs +24 -705
- package/src/mcp-server.mjs +35 -146
- package/src/precision/lsp-client/constants.js +12 -0
- package/src/precision/lsp-client/environment.js +20 -0
- package/src/precision/lsp-client/errors.js +15 -0
- package/src/precision/lsp-client/lifecycle.js +127 -0
- package/src/precision/lsp-client/message-parser.js +81 -0
- package/src/precision/lsp-client/protocol.js +212 -0
- package/src/precision/lsp-client/registry.js +58 -0
- package/src/precision/lsp-client/repo-uri.js +60 -0
- package/src/precision/lsp-client/stdio-client.js +151 -0
- package/src/precision/lsp-client.js +10 -682
- package/src/precision/lsp-overlay/build.js +238 -0
- package/src/precision/lsp-overlay/contract.js +61 -0
- package/src/precision/lsp-overlay/reference-results.js +108 -0
- package/src/precision/lsp-overlay/semantic-inputs.js +62 -0
- package/src/precision/lsp-overlay/source-session.js +134 -0
- package/src/precision/lsp-overlay/store.js +150 -0
- package/src/precision/lsp-overlay/target-index.js +147 -0
- package/src/precision/lsp-overlay/target-query.js +55 -0
- package/src/precision/lsp-overlay.js +11 -868
- package/src/precision/typescript-lsp-provider.js +12 -682
- package/src/precision/typescript-provider/client.js +69 -0
- package/src/precision/typescript-provider/discovery.js +124 -0
- package/src/precision/typescript-provider/project-host.js +249 -0
- package/src/precision/typescript-provider/project-safety.js +161 -0
- package/src/precision/typescript-provider/reference-usage.js +53 -0
- package/src/version.js +7 -0
- package/docs/releases/v0.2.13.md +0 -48
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import {hasPathClass} from '../../path-classification.js'
|
|
2
|
+
|
|
3
|
+
export const DEAD_CODE_CONFIDENCE_RANK = Object.freeze({high: 0, medium: 1, low: 2})
|
|
4
|
+
export const DYNAMIC_CODE_RE = /(?:\brequire\s*\(\s*(?!["'])|\bcreateRequire\s*\(|\b__import__\s*\(|\bimportlib\.|(?:^|[^\w.$])(?:eval|exec)\s*\()/m
|
|
5
|
+
const JS_DYNAMIC_IMPORT_RE = /\bimport\s*\(/
|
|
6
|
+
const JS_LIKE_PATH_RE = /\.(?:[cm]?js|jsx|[cm]?ts|tsx)$/i
|
|
7
|
+
export const REFLECTION_CODE_RE = /(?:\b(?:Class\.forName|get(?:Declared)?Method|getattr|setattr|hasattr|Method\.Invoke|GetMethod|GetProcAddress|dlsym)\s*\(|\b(?:globals|locals)\s*\(\s*\)\s*\[|\breflect\.[A-Za-z_$][\w$]*\s*\()/i
|
|
8
|
+
export const normalizedReviewPath = (value) => String(value || '').replace(/\\/g, '/').replace(/^\.\//, '')
|
|
9
|
+
|
|
10
|
+
export function hasDynamicCode(source, file = '') {
|
|
11
|
+
const text = String(source || '')
|
|
12
|
+
return DYNAMIC_CODE_RE.test(text) || (JS_LIKE_PATH_RE.test(String(file)) && JS_DYNAMIC_IMPORT_RE.test(text))
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const NON_PRODUCT_CLASSES = Object.freeze(['generated', 'mock', 'story', 'docs', 'benchmark', 'temp'])
|
|
16
|
+
|
|
17
|
+
export function deadCodePathAllowed(info, {includeTests, includeClassified}) {
|
|
18
|
+
if (!includeTests && hasPathClass(info, 'test', 'e2e')) return {ok: false, bucket: 'tests'}
|
|
19
|
+
if (!includeClassified && (info?.excluded || hasPathClass(info, ...NON_PRODUCT_CLASSES))) {
|
|
20
|
+
return {ok: false, bucket: 'classified'}
|
|
21
|
+
}
|
|
22
|
+
return {ok: true}
|
|
23
|
+
}
|
|
@@ -3,14 +3,12 @@
|
|
|
3
3
|
// never deletion instructions: framework entry, dynamic loading, reflection and public API surfaces
|
|
4
4
|
// lower confidence and remain explicit in the returned record.
|
|
5
5
|
import { computeDead, isFrameworkEntryFile } from "./dead-check.js";
|
|
6
|
-
import { createPathClassifier
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const normalizedPath = (value) => String(value || "").replace(/\\/g, "/").replace(/^\.\//, "");
|
|
6
|
+
import { createPathClassifier } from "../path-classification.js";
|
|
7
|
+
import {
|
|
8
|
+
DEAD_CODE_CONFIDENCE_RANK as CONFIDENCE_RANK, hasDynamicCode,
|
|
9
|
+
REFLECTION_CODE_RE as REFLECTION_RE, deadCodePathAllowed as pathAllowed,
|
|
10
|
+
normalizedReviewPath as normalizedPath,
|
|
11
|
+
} from './dead-code-review/policy.js'
|
|
14
12
|
const lineOf = (node) => {
|
|
15
13
|
const match = /@(\d+)$/.exec(String(node?.id || "")) || /L(\d+)/.exec(String(node?.source_location || ""));
|
|
16
14
|
return match ? Number(match[1]) : 0;
|
|
@@ -32,14 +30,6 @@ function isPublicSurface(node) {
|
|
|
32
30
|
return node?.exported === true || visibility === "public" || visibility === "protected";
|
|
33
31
|
}
|
|
34
32
|
|
|
35
|
-
function pathAllowed(info, { includeTests, includeClassified }) {
|
|
36
|
-
if (!includeTests && hasPathClass(info, "test", "e2e")) return { ok: false, bucket: "tests" };
|
|
37
|
-
if (!includeClassified && (info?.excluded || hasPathClass(info, ...CLASSIFIED_NON_PRODUCT))) {
|
|
38
|
-
return { ok: false, bucket: "classified" };
|
|
39
|
-
}
|
|
40
|
-
return { ok: true };
|
|
41
|
-
}
|
|
42
|
-
|
|
43
33
|
function symbolCandidate(item, node, context) {
|
|
44
34
|
const file = normalizedPath(item.file);
|
|
45
35
|
const source = String(context.sources.get(file) || "");
|
|
@@ -48,7 +38,7 @@ function symbolCandidate(item, node, context) {
|
|
|
48
38
|
const testOnly = item.testOnly === true;
|
|
49
39
|
const externalEntry = context.entrySet.has(file) || isFrameworkEntryFile(file);
|
|
50
40
|
const framework = context.frameworkByFile.get(file) || null;
|
|
51
|
-
const dynamicFile = context.dynamicTargets.has(file) ||
|
|
41
|
+
const dynamicFile = context.dynamicTargets.has(file) || hasDynamicCode(source, file);
|
|
52
42
|
const reflectionFile = REFLECTION_RE.test(source);
|
|
53
43
|
const kind = kindOf(node);
|
|
54
44
|
const exactNoReference = context.exactNoReferenceIds.has(String(item.id));
|
|
@@ -143,7 +133,7 @@ function fileCandidate(item, symbols, context) {
|
|
|
143
133
|
const source = String(context.sources.get(file) || "");
|
|
144
134
|
const pathInfo = context.classify(file, source);
|
|
145
135
|
const publicSymbols = symbols.filter((symbol) => symbol.publicApi);
|
|
146
|
-
const dynamicFile = context.dynamicTargets.has(file) ||
|
|
136
|
+
const dynamicFile = context.dynamicTargets.has(file) || hasDynamicCode(source, file);
|
|
147
137
|
const reflectionFile = REFLECTION_RE.test(source);
|
|
148
138
|
// Whole-file liveness always remains at most medium: external launchers/manifests can exist outside
|
|
149
139
|
// the indexed import graph even when every internal symbol signal is otherwise strong.
|
|
@@ -219,7 +209,7 @@ export function computeDeadCodeReview(graph, sources, options = {}) {
|
|
|
219
209
|
return classificationCache.get(file);
|
|
220
210
|
};
|
|
221
211
|
const repoSignals = {
|
|
222
|
-
dynamicLoading: (graph.externalImports || []).some((entry) => entry?.dynamic) || [...sources
|
|
212
|
+
dynamicLoading: (graph.externalImports || []).some((entry) => entry?.dynamic) || [...sources].some(([file, text]) => hasDynamicCode(text, file)),
|
|
223
213
|
reflection: [...sources.values()].some((text) => REFLECTION_RE.test(String(text || ""))),
|
|
224
214
|
};
|
|
225
215
|
const includeTests = options.includeTests === true;
|
|
@@ -7,47 +7,37 @@
|
|
|
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 {createScopedDepFindings} from './dependency/scoped-dependencies.js'
|
|
10
11
|
|
|
11
12
|
// Packages referenced by config CONVENTION, not imports (eslint extends "airbnb" → eslint-config-airbnb).
|
|
12
13
|
// Flagged only at low confidence when nothing mentions them anywhere.
|
|
13
|
-
const CONFIG_ECOSYSTEM_RE =
|
|
14
|
-
/^(eslint-(config|plugin)-|@typescript-eslint\/|@eslint\/|prettier-plugin-|postcss-|autoprefixer$|tailwindcss$|babel-(plugin|preset)-|@babel\/(plugin|preset)-|stylelint-|@commitlint\/|commitlint-|remark-|rehype-|@semantic-release\/|karma-|grunt-|gulp-)/;
|
|
14
|
+
const CONFIG_ECOSYSTEM_RE = /^(eslint-(config|plugin)-|@typescript-eslint\/|@eslint\/|prettier-plugin-|postcss-|autoprefixer$|tailwindcss$|babel-(plugin|preset)-|@babel\/(plugin|preset)-|stylelint-|@commitlint\/|commitlint-|remark-|rehype-|@semantic-release\/|karma-|grunt-|gulp-)/;
|
|
15
15
|
|
|
16
16
|
// CLI name → package name, for script commands whose binary doesn't equal the package
|
|
17
17
|
// (`tsc` comes from typescript, `depcruise` from dependency-cruiser, …).
|
|
18
18
|
const BIN_PKG = {
|
|
19
|
-
tsc: "typescript",
|
|
20
|
-
|
|
21
|
-
"
|
|
22
|
-
ng: "@angular/cli",
|
|
23
|
-
nest: "@nestjs/cli",
|
|
24
|
-
sb: "storybook",
|
|
25
|
-
"electron-rebuild": "@electron/rebuild",
|
|
26
|
-
playwright: "@playwright/test",
|
|
19
|
+
tsc: "typescript", depcruise: "dependency-cruiser", "vue-cli-service": "@vue/cli-service",
|
|
20
|
+
ng: "@angular/cli", nest: "@nestjs/cli", sb: "storybook",
|
|
21
|
+
"electron-rebuild": "@electron/rebuild", playwright: "@playwright/test",
|
|
27
22
|
};
|
|
28
23
|
|
|
29
24
|
// Required peers consumed inside a framework/build tool rather than imported by application source.
|
|
30
25
|
// These contracts are package-scope local and deliberately narrow; declaring the provider is required
|
|
31
26
|
// for suppression, so an unrelated app still gets the normal unused-dependency finding.
|
|
32
27
|
const FRAMEWORK_RUNTIME_PEERS = new Map([
|
|
33
|
-
["next", ["react-dom"]],
|
|
34
|
-
["
|
|
35
|
-
["electron-vite", ["vite"]],
|
|
36
|
-
["@cloudflare/vite-plugin", ["vite", "wrangler"]],
|
|
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"]],
|
|
37
30
|
]);
|
|
38
31
|
|
|
39
32
|
// Style preprocessors are compiler inputs rather than JavaScript imports. Their presence is proven by
|
|
40
33
|
// source extensions in the same package scope, so do not report them as unused merely because Vite,
|
|
41
34
|
// webpack, etc. load them internally. Keep this list to exact compiler/package contracts.
|
|
42
35
|
const IMPLICIT_STYLE_COMPILERS = new Map([
|
|
43
|
-
["sass", /\.(?:scss|sass)$/i],
|
|
44
|
-
["
|
|
45
|
-
["less", /\.less$/i],
|
|
46
|
-
["stylus", /\.styl(?:us)?$/i],
|
|
36
|
+
["sass", /\.(?:scss|sass)$/i], ["sass-embedded", /\.(?:scss|sass)$/i],
|
|
37
|
+
["less", /\.less$/i], ["stylus", /\.styl(?:us)?$/i],
|
|
47
38
|
]);
|
|
48
39
|
|
|
49
40
|
const isStylesheetSpecifier = (spec) => /\.(?:css|scss|sass|less|styl(?:us)?)(?:[?#].*)?$/i.test(String(spec || ""));
|
|
50
|
-
|
|
51
41
|
const escRe = (s) => String(s).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
52
42
|
// word-ish mention: the name not embedded inside a longer identifier/path segment
|
|
53
43
|
const mentioned = (blob, name) => new RegExp(`(^|[^\\w@.-])${escRe(name)}($|[^\\w.-])`).test(blob);
|
|
@@ -305,43 +295,6 @@ export function computeDepFindings({
|
|
|
305
295
|
return { findings, usedPackages, declared: allDeclared };
|
|
306
296
|
}
|
|
307
297
|
|
|
308
|
-
const
|
|
309
|
-
const ownsFile = (scope, file) => !scope || file === scope || String(file || "").startsWith(`${scope}/`);
|
|
310
|
-
|
|
311
|
-
// Judge every import against its nearest ancestor package.json. This is the dependency equivalent of
|
|
312
|
-
// Node's package scope and prevents nested Next/Vite apps from inheriting the root manifest by accident.
|
|
313
|
-
export function computeScopedDepFindings({
|
|
314
|
-
externalImports = [], packageScopes = [], workspacePkgNames = new Set(), configTexts = new Map(),
|
|
315
|
-
nonRuntimeRoots = [], sourceFiles = [],
|
|
316
|
-
} = {}) {
|
|
317
|
-
const scopes = packageScopes.length
|
|
318
|
-
? packageScopes.map((s) => ({ ...s, root: normScope(s.root) })).sort((a, b) => b.root.length - a.root.length)
|
|
319
|
-
: [{ root: "", manifest: "package.json", pkg: {}, aliases: [] }];
|
|
320
|
-
const importsByScope = new Map(scopes.map((s) => [s, []]));
|
|
321
|
-
const sourceFilesByScope = new Map(scopes.map((s) => [s, []]));
|
|
322
|
-
for (const e of externalImports) {
|
|
323
|
-
const owner = scopes.find((s) => ownsFile(s.root, e.file)) || scopes[scopes.length - 1];
|
|
324
|
-
importsByScope.get(owner).push(e);
|
|
325
|
-
}
|
|
326
|
-
for (const file of sourceFiles) {
|
|
327
|
-
const owner = scopes.find((s) => ownsFile(s.root, file)) || scopes[scopes.length - 1];
|
|
328
|
-
sourceFilesByScope.get(owner).push(file);
|
|
329
|
-
}
|
|
330
|
-
const configOwner = new Map();
|
|
331
|
-
for (const [file] of configTexts) configOwner.set(file, scopes.find((scope) => ownsFile(scope.root, file)) || scopes[scopes.length - 1]);
|
|
332
|
-
const findings = [], usedPackages = new Map(), declared = new Set();
|
|
333
|
-
for (const s of scopes) {
|
|
334
|
-
const scopeConfig = new Map([...configTexts].filter(([f]) => configOwner.get(f) === s));
|
|
335
|
-
const r = computeDepFindings({
|
|
336
|
-
externalImports: importsByScope.get(s), pkg: s.pkg || {}, workspacePkgNames, configTexts: scopeConfig,
|
|
337
|
-
aliases: s.aliases || [], scope: s.root, manifest: s.manifest || (s.root ? `${s.root}/package.json` : "package.json"),
|
|
338
|
-
nonRuntimeRoots, sourceFiles: sourceFilesByScope.get(s),
|
|
339
|
-
});
|
|
340
|
-
findings.push(...r.findings);
|
|
341
|
-
for (const [name, use] of r.usedPackages) usedPackages.set(`${s.root || "."}:${name}`, use);
|
|
342
|
-
for (const name of r.declared) declared.add(`${s.root || "."}:${name}`);
|
|
343
|
-
}
|
|
344
|
-
return { findings, usedPackages, declared };
|
|
345
|
-
}
|
|
298
|
+
export const computeScopedDepFindings = createScopedDepFindings(computeDepFindings)
|
|
346
299
|
|
|
347
300
|
export { computeGoDepFindings, computePyDepFindings } from "./dep-check-ecosystems.js";
|
|
@@ -1,303 +1,10 @@
|
|
|
1
|
-
//
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const TEST_FILE_RE = /(^|[/])(test|tests|__tests__|spec|e2e|__mocks__)([/]|$)|[._-](test|spec)\.[a-z0-9]+$/i;
|
|
12
|
-
// config/data/docs: never "orphans" — nothing imports them by design
|
|
13
|
-
const NON_CODE_RE = /\.(json|ya?ml|sh|ps1|md|txt|html?|css|scss|less)$|(^|[/])(dockerfile|containerfile)/i;
|
|
14
|
-
const ep = (v) => String(v && typeof v === "object" ? v.id : v);
|
|
15
|
-
const fileOf = (v) => { const s = ep(v); const h = s.indexOf("#"); return h < 0 ? s : s.slice(0, h); };
|
|
16
|
-
const dirOf = (f) => (f.includes("/") ? f.slice(0, f.lastIndexOf("/")) : "");
|
|
17
|
-
// File-level import adjacency. Go same-directory edges are excluded: a Go package IS the directory, the
|
|
18
|
-
// compiler forbids real cross-package cycles, and our suffix-fallback Go resolution could fake them.
|
|
19
|
-
export function buildFileImportGraph(graph, { includeTypeOnly = false, includeCompileOnly = false } = {}) {
|
|
20
|
-
const fileIds = new Set();
|
|
21
|
-
for (const n of graph.nodes || []) if (!String(n.id).includes("#")) fileIds.add(String(n.id));
|
|
22
|
-
const runtimeAdj = new Map(); // runtime/value imports only
|
|
23
|
-
const allAdj = new Map(); // runtime + compile-time-only, used to describe architectural coupling
|
|
24
|
-
const edges = [];
|
|
25
|
-
const allEdges = [];
|
|
26
|
-
const typeOnlyEdges = [];
|
|
27
|
-
const compileOnlyEdges = [];
|
|
28
|
-
const compileTimeEdges = [];
|
|
29
|
-
const runtimeSeen = new Set(), allSeen = new Set(), typeSeen = new Set(), compileSeen = new Set(), compileTimeSeen = new Set();
|
|
30
|
-
const add = (map, a, b) => {
|
|
31
|
-
let set = map.get(a);
|
|
32
|
-
if (!set) map.set(a, (set = new Set()));
|
|
33
|
-
set.add(b);
|
|
34
|
-
};
|
|
35
|
-
for (const l of graph.links || []) {
|
|
36
|
-
if (l.relation !== "imports" && l.relation !== "re_exports") continue;
|
|
37
|
-
const a = ep(l.source), b = ep(l.target);
|
|
38
|
-
if (!fileIds.has(a) || !fileIds.has(b) || a === b) continue;
|
|
39
|
-
if (a.endsWith(".go") && b.endsWith(".go") && dirOf(a) === dirOf(b)) continue;
|
|
40
|
-
const key = `${a}\0${b}`;
|
|
41
|
-
if (!allSeen.has(key)) { allSeen.add(key); add(allAdj, a, b); allEdges.push([a, b]); }
|
|
42
|
-
if (l.typeOnly === true || l.compileOnly === true) {
|
|
43
|
-
if (l.typeOnly === true && !typeSeen.has(key)) { typeSeen.add(key); typeOnlyEdges.push([a, b]); }
|
|
44
|
-
if (l.compileOnly === true && !compileSeen.has(key)) { compileSeen.add(key); compileOnlyEdges.push([a, b]); }
|
|
45
|
-
if (!compileTimeSeen.has(key)) { compileTimeSeen.add(key); compileTimeEdges.push([a, b]); }
|
|
46
|
-
continue;
|
|
47
|
-
}
|
|
48
|
-
if (!runtimeSeen.has(key)) { runtimeSeen.add(key); add(runtimeAdj, a, b); edges.push([a, b]); }
|
|
49
|
-
}
|
|
50
|
-
const pureTypeOnlyEdges = typeOnlyEdges.filter(([a, b]) => !runtimeSeen.has(`${a}\0${b}`));
|
|
51
|
-
const pureCompileOnlyEdges = compileOnlyEdges.filter(([a, b]) => !runtimeSeen.has(`${a}\0${b}`));
|
|
52
|
-
const pureCompileTimeEdges = compileTimeEdges.filter(([a, b]) => !runtimeSeen.has(`${a}\0${b}`));
|
|
53
|
-
return {
|
|
54
|
-
fileIds,
|
|
55
|
-
adj: includeTypeOnly || includeCompileOnly ? allAdj : runtimeAdj,
|
|
56
|
-
edges: includeTypeOnly || includeCompileOnly ? allEdges : edges,
|
|
57
|
-
runtimeAdj,
|
|
58
|
-
runtimeEdges: edges,
|
|
59
|
-
allAdj,
|
|
60
|
-
allEdges,
|
|
61
|
-
typeOnlyEdges: pureTypeOnlyEdges,
|
|
62
|
-
compileOnlyEdges: pureCompileOnlyEdges,
|
|
63
|
-
compileTimeEdges: pureCompileTimeEdges,
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
// Iterative Tarjan (deep graphs must not blow the JS stack). Returns only non-trivial SCCs (size > 1).
|
|
67
|
-
export function findSccs(adj) {
|
|
68
|
-
const index = new Map(), low = new Map(), onStack = new Set(), S = [];
|
|
69
|
-
let counter = 0;
|
|
70
|
-
const sccs = [];
|
|
71
|
-
for (const root of adj.keys()) {
|
|
72
|
-
if (index.has(root)) continue;
|
|
73
|
-
index.set(root, counter); low.set(root, counter); counter++;
|
|
74
|
-
S.push(root); onStack.add(root);
|
|
75
|
-
const stack = [{ v: root, ci: 0, neigh: [...(adj.get(root) || [])] }];
|
|
76
|
-
while (stack.length) {
|
|
77
|
-
const fr = stack[stack.length - 1];
|
|
78
|
-
if (fr.ci < fr.neigh.length) {
|
|
79
|
-
const w = fr.neigh[fr.ci++];
|
|
80
|
-
if (!index.has(w)) {
|
|
81
|
-
index.set(w, counter); low.set(w, counter); counter++;
|
|
82
|
-
S.push(w); onStack.add(w);
|
|
83
|
-
stack.push({ v: w, ci: 0, neigh: [...(adj.get(w) || [])] });
|
|
84
|
-
} else if (onStack.has(w)) {
|
|
85
|
-
low.set(fr.v, Math.min(low.get(fr.v), index.get(w)));
|
|
86
|
-
}
|
|
87
|
-
} else {
|
|
88
|
-
stack.pop();
|
|
89
|
-
if (stack.length) { const p = stack[stack.length - 1]; low.set(p.v, Math.min(low.get(p.v), low.get(fr.v))); }
|
|
90
|
-
if (low.get(fr.v) === index.get(fr.v)) {
|
|
91
|
-
const comp = [];
|
|
92
|
-
let w;
|
|
93
|
-
do { w = S.pop(); onStack.delete(w); comp.push(w); } while (w !== fr.v);
|
|
94
|
-
if (comp.length > 1) sccs.push(comp);
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
return sccs;
|
|
100
|
-
}
|
|
101
|
-
// One representative (shortest) cycle through an SCC's lexicographically-first file — a readable path,
|
|
102
|
-
// not the full Johnson enumeration (which explodes combinatorially on big tangles).
|
|
103
|
-
export function representativeCycle(adj, scc) {
|
|
104
|
-
const inScc = new Set(scc);
|
|
105
|
-
const start = [...scc].sort()[0];
|
|
106
|
-
const prev = new Map([[start, null]]);
|
|
107
|
-
const q = [start];
|
|
108
|
-
while (q.length) {
|
|
109
|
-
const cur = q.shift();
|
|
110
|
-
for (const nxt of [...(adj.get(cur) || [])].sort()) {
|
|
111
|
-
if (nxt === start) {
|
|
112
|
-
const path = [];
|
|
113
|
-
for (let c = cur; c != null; c = prev.get(c)) path.push(c);
|
|
114
|
-
return [...path.reverse(), start];
|
|
115
|
-
}
|
|
116
|
-
if (!inScc.has(nxt) || prev.has(nxt)) continue;
|
|
117
|
-
prev.set(nxt, cur);
|
|
118
|
-
q.push(nxt);
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
const fallback = [...scc].sort();
|
|
122
|
-
return [...fallback, fallback[0]]; // unreachable in a true SCC; deterministic safe fallback
|
|
123
|
-
}
|
|
124
|
-
// Orphans: file nodes with ZERO non-contains graph degree (nothing in, nothing out — imports, calls,
|
|
125
|
-
// references all collapsed to files). Entries/tests/config-data are exempt. A file that DOES import
|
|
126
|
-
// npm packages (externalImports) is a working script, not an island — confidence drops, not the verdict.
|
|
127
|
-
export function findOrphans(graph, { entrySet = new Set(), externalImportFiles = new Set() } = {}) {
|
|
128
|
-
const deg = new Map();
|
|
129
|
-
for (const l of graph.links || []) {
|
|
130
|
-
if (l.relation === "contains") continue;
|
|
131
|
-
const a = fileOf(l.source), b = fileOf(l.target);
|
|
132
|
-
if (a === b) continue;
|
|
133
|
-
deg.set(a, (deg.get(a) || 0) + 1);
|
|
134
|
-
deg.set(b, (deg.get(b) || 0) + 1);
|
|
135
|
-
}
|
|
136
|
-
const out = [];
|
|
137
|
-
for (const n of graph.nodes || []) {
|
|
138
|
-
const id = String(n.id);
|
|
139
|
-
if (id.includes("#")) continue;
|
|
140
|
-
if ((deg.get(id) || 0) > 0) continue;
|
|
141
|
-
const f = n.source_file;
|
|
142
|
-
if (entrySet.has(f) || ENTRY_FILE.test(f) || TEST_FILE_RE.test(f) || NON_CODE_RE.test(f)) continue;
|
|
143
|
-
out.push({ file: f, importsExternals: externalImportFiles.has(f) });
|
|
144
|
-
}
|
|
145
|
-
return out;
|
|
146
|
-
}
|
|
147
|
-
// Boundary DSL — the useful subset of depcruise's forbidden rules, as plain JSON globs:
|
|
148
|
-
// { "forbidden": [{ "name", "comment"?, "severity"?, "from": "main/**", "to": "renderer/**" }],
|
|
149
|
-
// "allowedOnly":[{ "name", "comment"?, "severity"?, "from": "src/ui/**", "to": ["src/ui/**", "src/shared/**"] }] }
|
|
150
|
-
// forbidden fires when an edge matches from AND to; allowedOnly fires when from matches and to matches NONE.
|
|
151
|
-
export function globToRe(glob) {
|
|
152
|
-
// split on ** first so single-* expansion cannot touch the multi-segment wildcards
|
|
153
|
-
const parts = String(glob).split("**").map((p) => p.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, "[^/]*").replace(/\?/g, "[^/]"));
|
|
154
|
-
let s = parts.join(".*");
|
|
155
|
-
s = s.replace(/\/\.\*\//g, "/(?:.*/)?"); // a/**/b also matches a/b (zero middle dirs)
|
|
156
|
-
s = s.replace(/^\.\*\//, "(?:.*/)?"); // **/b also matches root-level b
|
|
157
|
-
return new RegExp(`^${s}$`);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
export function checkBoundaries(edges, rules = {}) {
|
|
161
|
-
const violations = [];
|
|
162
|
-
const forbidden = (rules.forbidden || []).map((r) => ({ ...r, fromRe: globToRe(r.from), toRe: globToRe(r.to) }));
|
|
163
|
-
const allowedOnly = (rules.allowedOnly || []).map((r) => ({ ...r, fromRe: globToRe(r.from), toRes: (Array.isArray(r.to) ? r.to : [r.to]).map(globToRe) }));
|
|
164
|
-
for (const [a, b] of edges) {
|
|
165
|
-
for (const r of forbidden) if (r.fromRe.test(a) && r.toRe.test(b)) violations.push({ name: r.name, comment: r.comment || "", severity: r.severity, from: a, to: b, kind: "forbidden" });
|
|
166
|
-
for (const r of allowedOnly) if (r.fromRe.test(a) && !r.toRes.some((re) => re.test(b))) violations.push({ name: r.name, comment: r.comment || "", severity: r.severity, from: a, to: b, kind: "allowedOnly" });
|
|
167
|
-
}
|
|
168
|
-
return violations;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
const MAX_CYCLE_FINDINGS = 50;
|
|
172
|
-
const MAX_BOUNDARY_FINDINGS = 100;
|
|
173
|
-
// Assemble everything into unified Findings. rules comes from the repo's .weavatrix-deps.json (optional).
|
|
174
|
-
export function computeStructureFindings(graph, { rules = {}, entrySet = new Set(), externalImportFiles = new Set() } = {}) {
|
|
175
|
-
const { adj, edges, allAdj, allEdges, typeOnlyEdges, compileOnlyEdges, compileTimeEdges } = buildFileImportGraph(graph);
|
|
176
|
-
const findings = [];
|
|
177
|
-
const sccs = findSccs(adj).sort((a, b) => b.length - a.length);
|
|
178
|
-
for (const scc of sccs.slice(0, MAX_CYCLE_FINDINGS)) {
|
|
179
|
-
const cycle = representativeCycle(adj, scc);
|
|
180
|
-
const cycleRoute = formatRepresentativeCycle(cycle);
|
|
181
|
-
findings.push(makeFinding({
|
|
182
|
-
category: "structure",
|
|
183
|
-
rule: "circular-dep",
|
|
184
|
-
severity: scc.length > 4 ? "high" : "medium",
|
|
185
|
-
confidence: "high",
|
|
186
|
-
title: `Circular dependency: ${scc.length} files`,
|
|
187
|
-
detail: `${cycleRoute}${scc.length + 1 > cycle.length ? ` (representative loop; the tangle spans ${scc.length} files)` : ""}. Break the cycle by extracting the shared piece or inverting one import.`,
|
|
188
|
-
cycleRoute,
|
|
189
|
-
cycleMembers: [...scc].sort(),
|
|
190
|
-
file: cycle[0],
|
|
191
|
-
graphNodeId: cycle[0],
|
|
192
|
-
evidence: cycle.map((f) => ({ file: f, line: 0, snippet: "" })),
|
|
193
|
-
source: "internal",
|
|
194
|
-
fixHint: "extract the shared code into a module both sides import, or invert the weaker dependency",
|
|
195
|
-
}));
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
// TypeScript `import type` and Rust module/use edges are compile-time coupling. They can reveal real
|
|
199
|
-
// architecture, but cannot create an initialization-order/runtime cycle. Report SCCs that require
|
|
200
|
-
// either classification separately so agents do not churn working code for a phantom runtime hazard.
|
|
201
|
-
const runtimeKeys = new Set(sccs.map((s) => [...s].sort().join("\0")));
|
|
202
|
-
const allSccs = findSccs(allAdj).sort((a, b) => b.length - a.length);
|
|
203
|
-
const compileTimeCouplings = allSccs.filter((s) => !runtimeKeys.has([...s].sort().join("\0")));
|
|
204
|
-
const edgeCountIn = (scc, list) => {
|
|
205
|
-
const inside = new Set(scc);
|
|
206
|
-
return list.reduce((n, [a, b]) => n + (inside.has(a) && inside.has(b) ? 1 : 0), 0);
|
|
207
|
-
};
|
|
208
|
-
for (const scc of compileTimeCouplings.slice(0, MAX_CYCLE_FINDINGS)) {
|
|
209
|
-
const cycle = representativeCycle(allAdj, scc);
|
|
210
|
-
const cycleRoute = formatRepresentativeCycle(cycle);
|
|
211
|
-
const runtimeInside = edgeCountIn(scc, edges);
|
|
212
|
-
const typeInside = edgeCountIn(scc, typeOnlyEdges);
|
|
213
|
-
const compileInside = edgeCountIn(scc, compileOnlyEdges);
|
|
214
|
-
const containsRuntimeCycle = sccs.some((runtime) => runtime.every((f) => scc.includes(f)));
|
|
215
|
-
const typeSpecific = compileInside === 0;
|
|
216
|
-
findings.push(makeFinding({
|
|
217
|
-
category: "structure",
|
|
218
|
-
rule: typeSpecific ? "type-coupling" : "compile-time-coupling",
|
|
219
|
-
severity: "info",
|
|
220
|
-
confidence: "high",
|
|
221
|
-
title: `${containsRuntimeCycle
|
|
222
|
-
? (typeSpecific ? "Type imports expand dependency coupling" : "Compile-time edges expand dependency coupling")
|
|
223
|
-
: (typeSpecific ? "Type-induced dependency cycle (no runtime cycle)" : "Compile-time dependency cycle (no runtime cycle)")}: ${scc.length} files`,
|
|
224
|
-
detail: `${cycleRoute}. This strongly-connected group needs compile-time-only edges to close; it contains ${runtimeInside} runtime edge(s), ${typeInside} type-only edge(s), and ${compileInside} compile-only edge(s)${containsRuntimeCycle ? ", with a smaller runtime cycle reported separately" : ", while its runtime import graph is acyclic"}. Treat this as design coupling, not an initialization-order failure.`,
|
|
225
|
-
cycleRoute,
|
|
226
|
-
cycleMembers: [...scc].sort(),
|
|
227
|
-
file: cycle[0],
|
|
228
|
-
graphNodeId: cycle[0],
|
|
229
|
-
evidence: cycle.map((f) => ({ file: f, line: 0, snippet: "" })),
|
|
230
|
-
source: "internal",
|
|
231
|
-
fixHint: "review the compile-time ownership only if the coupling impedes changes; no runtime-cycle fix is required",
|
|
232
|
-
}));
|
|
233
|
-
}
|
|
234
|
-
if (sccs.length > MAX_CYCLE_FINDINGS) {
|
|
235
|
-
findings.push(makeFinding({
|
|
236
|
-
category: "structure", rule: "circular-dep", severity: "info", confidence: "high",
|
|
237
|
-
title: `…and ${sccs.length - MAX_CYCLE_FINDINGS} more dependency cycles`,
|
|
238
|
-
detail: `Cycle findings are capped at ${MAX_CYCLE_FINDINGS}; ${sccs.length} strongly-connected groups exist in total.`,
|
|
239
|
-
source: "internal",
|
|
240
|
-
}));
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
for (const o of findOrphans(graph, { entrySet, externalImportFiles })) {
|
|
244
|
-
findings.push(makeFinding({
|
|
245
|
-
category: "structure",
|
|
246
|
-
rule: "orphan-file",
|
|
247
|
-
severity: "info",
|
|
248
|
-
confidence: o.importsExternals ? "low" : "medium",
|
|
249
|
-
title: `Orphan file: ${o.file}`,
|
|
250
|
-
detail: `No repo file imports it and it imports/calls nothing in the repo${o.importsExternals ? " (it does use npm packages — possibly a standalone script or tool)" : ""}. Possibly dead, possibly an undeclared entry point.`,
|
|
251
|
-
file: o.file,
|
|
252
|
-
graphNodeId: o.file,
|
|
253
|
-
source: "internal",
|
|
254
|
-
}));
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
// Architecture boundaries describe executable dependencies by default. Type-only contract sharing is
|
|
258
|
-
// visible in typeCouplings above, but must not be presented as a runtime layer violation.
|
|
259
|
-
const violations = checkBoundaries(edges, rules);
|
|
260
|
-
for (const v of violations.slice(0, MAX_BOUNDARY_FINDINGS)) {
|
|
261
|
-
findings.push(makeFinding({
|
|
262
|
-
category: "structure",
|
|
263
|
-
rule: "boundary-violation",
|
|
264
|
-
severity: ["critical", "high", "medium", "low", "info"].includes(v.severity) ? v.severity : "medium",
|
|
265
|
-
confidence: "high",
|
|
266
|
-
title: `Boundary violation (${v.name}): ${v.from} → ${v.to}`,
|
|
267
|
-
detail: `${v.kind === "allowedOnly" ? "Import leaves the allowed set" : "Forbidden import"}${v.comment ? `: ${v.comment}` : ""}.`,
|
|
268
|
-
file: v.from,
|
|
269
|
-
graphNodeId: v.from,
|
|
270
|
-
evidence: [{ file: v.from, line: 0, snippet: `imports ${v.to}` }],
|
|
271
|
-
source: "internal",
|
|
272
|
-
}));
|
|
273
|
-
}
|
|
274
|
-
if (violations.length > MAX_BOUNDARY_FINDINGS) {
|
|
275
|
-
findings.push(makeFinding({
|
|
276
|
-
category: "structure", rule: "boundary-violation", severity: "info", confidence: "high",
|
|
277
|
-
title: `…and ${violations.length - MAX_BOUNDARY_FINDINGS} more boundary violations`,
|
|
278
|
-
detail: `Boundary findings are capped at ${MAX_BOUNDARY_FINDINGS}; ${violations.length} edges violate the rules in total.`,
|
|
279
|
-
source: "internal",
|
|
280
|
-
}));
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
return {
|
|
284
|
-
findings,
|
|
285
|
-
stats: {
|
|
286
|
-
importEdges: allEdges.length,
|
|
287
|
-
runtimeImportEdges: edges.length,
|
|
288
|
-
typeOnlyImportEdges: typeOnlyEdges.length,
|
|
289
|
-
compileOnlyImportEdges: compileOnlyEdges.length,
|
|
290
|
-
compileTimeImportEdges: compileTimeEdges.length,
|
|
291
|
-
cycles: sccs.length,
|
|
292
|
-
runtimeCycles: sccs.length,
|
|
293
|
-
largestCycle: sccs[0]?.length || 0,
|
|
294
|
-
// Backward-compatible aliases remain for edgeTypesV 1 consumers.
|
|
295
|
-
typeCouplings: compileTimeCouplings.length,
|
|
296
|
-
largestTypeCoupling: compileTimeCouplings[0]?.length || 0,
|
|
297
|
-
compileTimeCouplings: compileTimeCouplings.length,
|
|
298
|
-
largestCompileTimeCoupling: compileTimeCouplings[0]?.length || 0,
|
|
299
|
-
orphans: findings.filter((f) => f.rule === "orphan-file").length,
|
|
300
|
-
boundaryViolations: violations.length,
|
|
301
|
-
},
|
|
302
|
-
};
|
|
303
|
-
}
|
|
1
|
+
// Stable facade for structural dependency analysis.
|
|
2
|
+
export {
|
|
3
|
+
buildFileImportGraph,
|
|
4
|
+
checkBoundaries,
|
|
5
|
+
findOrphans,
|
|
6
|
+
findSccs,
|
|
7
|
+
globToRe,
|
|
8
|
+
representativeCycle,
|
|
9
|
+
} from './structure/dependency-graph.js'
|
|
10
|
+
export {computeStructureFindings} from './structure/findings.js'
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
const normalizeScope = (root) => String(root || '').replace(/\\/g, '/').replace(/^\.\//, '').replace(/^\/+|\/+$/g, '')
|
|
2
|
+
const ownsFile = (scope, file) => !scope || file === scope || String(file || '').startsWith(`${scope}/`)
|
|
3
|
+
|
|
4
|
+
// Bind the ecosystem-specific core without introducing a facade/helper import cycle.
|
|
5
|
+
export function createScopedDepFindings(computeDepFindings) {
|
|
6
|
+
return function computeScopedDepFindings({
|
|
7
|
+
externalImports = [], packageScopes = [], workspacePkgNames = new Set(), configTexts = new Map(),
|
|
8
|
+
nonRuntimeRoots = [], sourceFiles = [],
|
|
9
|
+
} = {}) {
|
|
10
|
+
const scopes = packageScopes.length
|
|
11
|
+
? packageScopes.map((scope) => ({...scope, root: normalizeScope(scope.root)})).sort((a, b) => b.root.length - a.root.length)
|
|
12
|
+
: [{root: '', manifest: 'package.json', pkg: {}, aliases: []}]
|
|
13
|
+
const importsByScope = new Map(scopes.map((scope) => [scope, []]))
|
|
14
|
+
const sourceFilesByScope = new Map(scopes.map((scope) => [scope, []]))
|
|
15
|
+
for (const item of externalImports) {
|
|
16
|
+
const owner = scopes.find((scope) => ownsFile(scope.root, item.file)) || scopes[scopes.length - 1]
|
|
17
|
+
importsByScope.get(owner).push(item)
|
|
18
|
+
}
|
|
19
|
+
for (const file of sourceFiles) {
|
|
20
|
+
const owner = scopes.find((scope) => ownsFile(scope.root, file)) || scopes[scopes.length - 1]
|
|
21
|
+
sourceFilesByScope.get(owner).push(file)
|
|
22
|
+
}
|
|
23
|
+
const configOwner = new Map()
|
|
24
|
+
for (const [file] of configTexts) configOwner.set(file, scopes.find((scope) => ownsFile(scope.root, file)) || scopes[scopes.length - 1])
|
|
25
|
+
const findings = [], usedPackages = new Map(), declared = new Set()
|
|
26
|
+
for (const scope of scopes) {
|
|
27
|
+
const scopeConfig = new Map([...configTexts].filter(([file]) => configOwner.get(file) === scope))
|
|
28
|
+
const result = computeDepFindings({
|
|
29
|
+
externalImports: importsByScope.get(scope), pkg: scope.pkg || {}, workspacePkgNames,
|
|
30
|
+
configTexts: scopeConfig, aliases: scope.aliases || [], scope: scope.root,
|
|
31
|
+
manifest: scope.manifest || (scope.root ? `${scope.root}/package.json` : 'package.json'),
|
|
32
|
+
nonRuntimeRoots, sourceFiles: sourceFilesByScope.get(scope),
|
|
33
|
+
})
|
|
34
|
+
findings.push(...result.findings)
|
|
35
|
+
for (const [name, use] of result.usedPackages) usedPackages.set(`${scope.root || '.'}:${name}`, use)
|
|
36
|
+
for (const name of result.declared) declared.add(`${scope.root || '.'}:${name}`)
|
|
37
|
+
}
|
|
38
|
+
return {findings, usedPackages, declared}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
export const MAX_ENDPOINT_FILES = 3_000;
|
|
2
|
+
export const MAX_ENDPOINTS = 2_000;
|
|
3
|
+
export const HTTP_METHODS = new Set(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS", "TRACE", "CONNECT", "ALL", "ANY"]);
|
|
4
|
+
|
|
5
|
+
export function lineAt(text, index) {
|
|
6
|
+
let line = 1;
|
|
7
|
+
for (let i = 0; i < index && i < text.length; i++) if (text[i] === "\n") line++;
|
|
8
|
+
return line;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Preserve offsets and literals while hiding comments from regex extractors.
|
|
12
|
+
export function maskComments(text, { hashComments = false } = {}) {
|
|
13
|
+
const chars = String(text || "").split("");
|
|
14
|
+
let quote = "", escaped = false, lineComment = false, blockComment = false;
|
|
15
|
+
for (let i = 0; i < chars.length; i++) {
|
|
16
|
+
const ch = chars[i], next = chars[i + 1];
|
|
17
|
+
if (lineComment) {
|
|
18
|
+
if (ch === "\n" || ch === "\r") lineComment = false;
|
|
19
|
+
else chars[i] = " ";
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
if (blockComment) {
|
|
23
|
+
if (ch === "*" && next === "/") { chars[i] = chars[i + 1] = " "; i++; blockComment = false; }
|
|
24
|
+
else if (ch !== "\n" && ch !== "\r") chars[i] = " ";
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
if (quote) {
|
|
28
|
+
if (escaped) escaped = false;
|
|
29
|
+
else if (ch === "\\") escaped = true;
|
|
30
|
+
else if (ch === quote) quote = "";
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (ch === '"' || ch === "'" || ch === "`") { quote = ch; continue; }
|
|
34
|
+
if (ch === "/" && next === "/") { chars[i] = chars[i + 1] = " "; i++; lineComment = true; continue; }
|
|
35
|
+
if (ch === "/" && next === "*") { chars[i] = chars[i + 1] = " "; i++; blockComment = true; continue; }
|
|
36
|
+
if (hashComments && ch === "#") { chars[i] = " "; lineComment = true; }
|
|
37
|
+
}
|
|
38
|
+
return chars.join("");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function handlerName(expr) {
|
|
42
|
+
const source = String(expr || "").trim();
|
|
43
|
+
if (!source || /=>/.test(source) || /^\s*(async\s+)?function\b/.test(source) || /^\s*(?:async\s+)?(?:move\s+)?\|[^|]*\|/.test(source)) return "";
|
|
44
|
+
const turbofish = /(?:^|::)([A-Za-z_][\w]*)\s*::<[\s\S]*>\s*$/.exec(source);
|
|
45
|
+
if (turbofish) return turbofish[1];
|
|
46
|
+
const identifiers = source.match(/[A-Za-z_$][\w$]*/g);
|
|
47
|
+
if (!identifiers) return "";
|
|
48
|
+
const skipped = new Set(["async", "function", "await", "req", "res", "ctx", "request", "response", "next", "return"]);
|
|
49
|
+
for (let i = identifiers.length - 1; i >= 0; i--) if (!skipped.has(identifiers[i])) return identifiers[i];
|
|
50
|
+
return "";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function handlerReference(expr) {
|
|
54
|
+
const source = String(expr || "").trim();
|
|
55
|
+
if (!source || /=>/.test(source) || /^\s*(async\s+)?function\b/.test(source)) return "";
|
|
56
|
+
const refs = [...source.matchAll(/([A-Za-z_$][\w$]*(?:\s*\.\s*[A-Za-z_$][\w$]*)+)/g)];
|
|
57
|
+
return refs.length ? refs.at(-1)[1].replace(/\s+/g, "") : "";
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export const looksLikePath = (path) => typeof path === "string" && /^\/[\w\-./:{}*$?]*$/.test(path) && !path.includes("://");
|
|
61
|
+
export const cleanPath = (path) => String(path || "").replace(/\/+$/, "") || "/";
|
|
62
|
+
export const normalizedFile = (file) => String(file || "").replace(/\\/g, "/").replace(/^\.\//, "");
|