weavatrix 0.2.13 → 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.
Files changed (145) hide show
  1. package/README.md +99 -39
  2. package/SECURITY.md +2 -2
  3. package/docs/releases/v0.2.15.md +37 -0
  4. package/package.json +2 -2
  5. package/skill/SKILL.md +57 -38
  6. package/src/analysis/architecture/contract-graph.js +119 -0
  7. package/src/analysis/architecture/contract-schema.js +110 -0
  8. package/src/analysis/architecture/contract-storage.js +35 -0
  9. package/src/analysis/architecture/contract-verification.js +168 -0
  10. package/src/analysis/architecture-contract.js +16 -343
  11. package/src/analysis/change-classification/diff-parser.js +103 -0
  12. package/src/analysis/change-classification/options.js +40 -0
  13. package/src/analysis/change-classification/symbol-classifier.js +177 -0
  14. package/src/analysis/change-classification.js +120 -519
  15. package/src/analysis/dead-code-review/policy.js +23 -0
  16. package/src/analysis/dead-code-review.js +9 -19
  17. package/src/analysis/dep-check.js +14 -71
  18. package/src/analysis/dep-rules.js +10 -303
  19. package/src/analysis/dependency/conventions.js +16 -0
  20. package/src/analysis/dependency/scoped-dependencies.js +45 -0
  21. package/src/analysis/dependency/source-references.js +21 -0
  22. package/src/analysis/duplicates.tokenize.js +12 -2
  23. package/src/analysis/endpoints/common.js +62 -0
  24. package/src/analysis/endpoints/extract.js +107 -0
  25. package/src/analysis/endpoints/inventory.js +84 -0
  26. package/src/analysis/endpoints/mounts.js +129 -0
  27. package/src/analysis/endpoints-java.js +80 -3
  28. package/src/analysis/endpoints.js +3 -448
  29. package/src/analysis/git-history/analytics.js +177 -0
  30. package/src/analysis/git-history/collector.js +109 -0
  31. package/src/analysis/git-history/options.js +68 -0
  32. package/src/analysis/git-history.js +128 -577
  33. package/src/analysis/health-capabilities.js +82 -0
  34. package/src/analysis/http-contracts/analysis.js +169 -0
  35. package/src/analysis/http-contracts/client-call-detection.js +81 -0
  36. package/src/analysis/http-contracts/client-call-parser.js +255 -0
  37. package/src/analysis/http-contracts/graph-context.js +143 -0
  38. package/src/analysis/http-contracts/matching.js +61 -0
  39. package/src/analysis/http-contracts/shared.js +86 -0
  40. package/src/analysis/http-contracts.js +6 -822
  41. package/src/analysis/internal-audit/dependency-health.js +111 -0
  42. package/src/analysis/internal-audit/python-manifests.js +65 -0
  43. package/src/analysis/internal-audit/repo-files.js +55 -0
  44. package/src/analysis/internal-audit/supply-chain.js +132 -0
  45. package/src/analysis/internal-audit.collect.js +5 -106
  46. package/src/analysis/internal-audit.reach.js +20 -0
  47. package/src/analysis/internal-audit.run.js +114 -200
  48. package/src/analysis/jvm-dependency-evidence.js +69 -0
  49. package/src/analysis/source-correctness/retry-patterns.js +93 -0
  50. package/src/analysis/source-correctness.js +225 -0
  51. package/src/analysis/structure/dependency-graph.js +156 -0
  52. package/src/analysis/structure/findings.js +142 -0
  53. package/src/analysis/task-retrieval.js +3 -14
  54. package/src/graph/builder/go-receiver-resolution.js +112 -0
  55. package/src/graph/builder/js/queries.js +48 -0
  56. package/src/graph/builder/lang-go.js +89 -4
  57. package/src/graph/builder/lang-js.js +4 -44
  58. package/src/graph/builder/pass2-resolution.js +68 -0
  59. package/src/graph/freshness-probe.js +2 -1
  60. package/src/graph/incremental-refresh.js +3 -2
  61. package/src/graph/internal-builder.build.js +22 -183
  62. package/src/graph/internal-builder.pass2.js +161 -0
  63. package/src/graph/internal-builder.resolvers.js +2 -105
  64. package/src/graph/resolvers/rust.js +117 -0
  65. package/src/mcp/actions/advisories.mjs +18 -0
  66. package/src/mcp/actions/graph-lifecycle.mjs +148 -0
  67. package/src/mcp/actions/graph-sync.mjs +195 -0
  68. package/src/mcp/actions/hosted-architecture.mjs +57 -0
  69. package/src/mcp/architecture-bootstrap.mjs +168 -0
  70. package/src/mcp/architecture-starter.mjs +234 -0
  71. package/src/mcp/catalog.mjs +20 -6
  72. package/src/mcp/evidence/bun-lock-graph.mjs +209 -0
  73. package/src/mcp/evidence/duplicate-member-order.mjs +8 -0
  74. package/src/mcp/evidence/package-graph-common.mjs +115 -0
  75. package/src/mcp/evidence/package-lock-graph.mjs +92 -0
  76. package/src/mcp/evidence-snapshot.duplicates.mjs +3 -7
  77. package/src/mcp/evidence-snapshot.package-graph.mjs +5 -474
  78. package/src/mcp/git-output.mjs +10 -0
  79. package/src/mcp/graph/context-core.mjs +111 -0
  80. package/src/mcp/graph/context-seeds.mjs +197 -0
  81. package/src/mcp/graph/context-state.mjs +86 -0
  82. package/src/mcp/graph/reverse-reach.mjs +42 -0
  83. package/src/mcp/graph/tools-core.mjs +143 -0
  84. package/src/mcp/graph/tools-query.mjs +223 -0
  85. package/src/mcp/graph-context.mjs +4 -496
  86. package/src/mcp/health/audit-format.mjs +172 -0
  87. package/src/mcp/health/audit.mjs +171 -0
  88. package/src/mcp/health/dead-code.mjs +110 -0
  89. package/src/mcp/health/duplicates.mjs +83 -0
  90. package/src/mcp/health/endpoints.mjs +42 -0
  91. package/src/mcp/health/structure.mjs +219 -0
  92. package/src/mcp/runtime-version.mjs +32 -0
  93. package/src/mcp/server/auto-refresh.mjs +66 -0
  94. package/src/mcp/server/runtime-config.mjs +43 -0
  95. package/src/mcp/server/shutdown.mjs +40 -0
  96. package/src/mcp/sync/evidence-architecture.mjs +54 -0
  97. package/src/mcp/sync/evidence-common.mjs +88 -0
  98. package/src/mcp/sync/evidence-duplicates.mjs +97 -0
  99. package/src/mcp/sync/evidence-health.mjs +56 -0
  100. package/src/mcp/sync/evidence-packages.mjs +95 -0
  101. package/src/mcp/sync/payload-common.mjs +97 -0
  102. package/src/mcp/sync/payload-v2.mjs +53 -0
  103. package/src/mcp/sync/payload-v3.mjs +79 -0
  104. package/src/mcp/sync-evidence.mjs +7 -402
  105. package/src/mcp/sync-payload.mjs +10 -244
  106. package/src/mcp/tools-actions.mjs +18 -414
  107. package/src/mcp/tools-architecture.mjs +18 -70
  108. package/src/mcp/tools-context.mjs +56 -15
  109. package/src/mcp/tools-endpoints.mjs +4 -1
  110. package/src/mcp/tools-graph.mjs +17 -331
  111. package/src/mcp/tools-health.mjs +24 -705
  112. package/src/mcp/tools-impact-change.mjs +2 -38
  113. package/src/mcp/tools-impact.mjs +2 -43
  114. package/src/mcp-server.mjs +35 -146
  115. package/src/path-classification.js +14 -0
  116. package/src/precision/lsp-client/constants.js +12 -0
  117. package/src/precision/lsp-client/environment.js +20 -0
  118. package/src/precision/lsp-client/errors.js +15 -0
  119. package/src/precision/lsp-client/lifecycle.js +127 -0
  120. package/src/precision/lsp-client/message-parser.js +81 -0
  121. package/src/precision/lsp-client/protocol.js +212 -0
  122. package/src/precision/lsp-client/registry.js +58 -0
  123. package/src/precision/lsp-client/repo-uri.js +60 -0
  124. package/src/precision/lsp-client/stdio-client.js +151 -0
  125. package/src/precision/lsp-client.js +10 -682
  126. package/src/precision/lsp-overlay/build.js +238 -0
  127. package/src/precision/lsp-overlay/contract.js +61 -0
  128. package/src/precision/lsp-overlay/reference-results.js +108 -0
  129. package/src/precision/lsp-overlay/semantic-inputs.js +62 -0
  130. package/src/precision/lsp-overlay/source-session.js +134 -0
  131. package/src/precision/lsp-overlay/store.js +150 -0
  132. package/src/precision/lsp-overlay/target-index.js +147 -0
  133. package/src/precision/lsp-overlay/target-query.js +55 -0
  134. package/src/precision/lsp-overlay.js +11 -868
  135. package/src/precision/typescript-lsp-provider.js +12 -682
  136. package/src/precision/typescript-provider/client.js +69 -0
  137. package/src/precision/typescript-provider/discovery.js +124 -0
  138. package/src/precision/typescript-provider/project-host.js +249 -0
  139. package/src/precision/typescript-provider/project-safety.js +161 -0
  140. package/src/precision/typescript-provider/reference-usage.js +53 -0
  141. package/src/security/malware-heuristics.sweep.js +1 -1
  142. package/src/security/registry-sig.classify.js +2 -1
  143. package/src/security/registry-sig.rules.js +3 -3
  144. package/src/version.js +7 -0
  145. 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, hasPathClass } from "../path-classification.js";
7
-
8
- const CONFIDENCE_RANK = Object.freeze({ high: 0, medium: 1, low: 2 });
9
- const CLASSIFIED_NON_PRODUCT = Object.freeze(["generated", "mock", "story", "docs", "benchmark", "temp"]);
10
- const DYNAMIC_RE = /(?:\bimport\s*\(|\brequire\s*\(\s*(?!["'])|\bcreateRequire\s*\(|\b__import__\s*\(|\bimportlib\.|(?:^|[^\w.$])(?:eval|exec)\s*\()/m;
11
- const REFLECTION_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;
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) || DYNAMIC_RE.test(source);
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) || DYNAMIC_RE.test(source);
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.values()].some((text) => DYNAMIC_RE.test(String(text || ""))),
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,23 @@
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'
11
+ import {createScopedDepFindings} from './dependency/scoped-dependencies.js'
12
+ import {createPackageSourceMatcher} from './dependency/source-references.js'
10
13
 
11
14
  // Packages referenced by config CONVENTION, not imports (eslint extends "airbnb" → eslint-config-airbnb).
12
15
  // 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-)/;
16
+ 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
17
 
16
18
  // CLI name → package name, for script commands whose binary doesn't equal the package
17
19
  // (`tsc` comes from typescript, `depcruise` from dependency-cruiser, …).
18
20
  const BIN_PKG = {
19
- tsc: "typescript",
20
- depcruise: "dependency-cruiser",
21
- "vue-cli-service": "@vue/cli-service",
22
- ng: "@angular/cli",
23
- nest: "@nestjs/cli",
24
- sb: "storybook",
25
- "electron-rebuild": "@electron/rebuild",
26
- playwright: "@playwright/test",
21
+ tsc: "typescript", depcruise: "dependency-cruiser", "vue-cli-service": "@vue/cli-service",
22
+ ng: "@angular/cli", nest: "@nestjs/cli", sb: "storybook",
23
+ "electron-rebuild": "@electron/rebuild", playwright: "@playwright/test",
27
24
  };
28
25
 
29
- // Required peers consumed inside a framework/build tool rather than imported by application source.
30
- // These contracts are package-scope local and deliberately narrow; declaring the provider is required
31
- // for suppression, so an unrelated app still gets the normal unused-dependency finding.
32
- const FRAMEWORK_RUNTIME_PEERS = new Map([
33
- ["next", ["react-dom"]],
34
- ["vinext", ["@vitejs/plugin-react", "@vitejs/plugin-rsc", "react-server-dom-webpack", "vite"]],
35
- ["electron-vite", ["vite"]],
36
- ["@cloudflare/vite-plugin", ["vite", "wrangler"]],
37
- ]);
38
-
39
- // Style preprocessors are compiler inputs rather than JavaScript imports. Their presence is proven by
40
- // source extensions in the same package scope, so do not report them as unused merely because Vite,
41
- // webpack, etc. load them internally. Keep this list to exact compiler/package contracts.
42
- const IMPLICIT_STYLE_COMPILERS = new Map([
43
- ["sass", /\.(?:scss|sass)$/i],
44
- ["sass-embedded", /\.(?:scss|sass)$/i],
45
- ["less", /\.less$/i],
46
- ["stylus", /\.styl(?:us)?$/i],
47
- ]);
48
-
49
26
  const isStylesheetSpecifier = (spec) => /\.(?:css|scss|sass|less|styl(?:us)?)(?:[?#].*)?$/i.test(String(spec || ""));
50
-
51
27
  const escRe = (s) => String(s).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
52
28
  // word-ish mention: the name not embedded inside a longer identifier/path segment
53
29
  const mentioned = (blob, name) => new RegExp(`(^|[^\\w@.-])${escRe(name)}($|[^\\w.-])`).test(blob);
@@ -57,9 +33,10 @@ const mentioned = (blob, name) => new RegExp(`(^|[^\\w@.-])${escRe(name)}($|[^\\
57
33
  // pkg — parsed package.json ({} for non-JS repos → no dep findings)
58
34
  // workspacePkgNames — Set of monorepo-local package names (never "missing")
59
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
60
37
  export function computeDepFindings({
61
38
  externalImports = [], pkg = {}, workspacePkgNames = new Set(), configTexts = new Map(),
62
- aliases = [], scope = "", manifest = "package.json", nonRuntimeRoots = [], sourceFiles = [],
39
+ sourceTexts = new Map(), aliases = [], scope = "", manifest = "package.json", nonRuntimeRoots = [], sourceFiles = [],
63
40
  } = {}) {
64
41
  const findings = [];
65
42
  const meta = { scope: scope || ".", manifest };
@@ -147,12 +124,13 @@ export function computeDepFindings({
147
124
  return false;
148
125
  };
149
126
  const typesBase = (name) => (name.startsWith("@types/") ? name.slice(7).replace(/^(.+?)__(.+)$/, "@$1/$2") : null); // @types/babel__core → @babel/core
127
+ const sourceReferenced = createPackageSourceMatcher(sourceTexts);
150
128
 
151
129
  // ---- unused dependencies (per section; prod vs dev differ in severity/confidence) ----
152
130
  for (const [section, deps] of Object.entries(sections)) {
153
131
  if (section === "peerDependencies") continue; // peers are consumer-facing contracts, not usage
154
132
  for (const name of Object.keys(deps)) {
155
- 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;
156
134
  const tb = typesBase(name);
157
135
  if (tb) { // @types/x is used iff x is used (or it types the Node builtins)
158
136
  if (tb === "node" ? builtinUsed : usedPackages.has(tb) || frameworkRuntime.has(tb) || mentioned(configBlob, tb)) continue;
@@ -169,7 +147,9 @@ export function computeDepFindings({
169
147
  title: `Unused ${section === "dependencies" ? "dependency" : section.replace(/ies$/, "y")}: ${name}`,
170
148
  reason: "No indexed package import, package-script command, recognized config mention, framework peer contract, or implicit style-compiler input uses this declaration.",
171
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,
172
151
  package: name,
152
+ evidence: [{ file: manifest, line: 0, snippet: `declared in ${section}` }],
173
153
  source: "internal",
174
154
  actionability: "MANIFEST_REVIEW_REQUIRED",
175
155
  autoRemove: false,
@@ -305,43 +285,6 @@ export function computeDepFindings({
305
285
  return { findings, usedPackages, declared: allDeclared };
306
286
  }
307
287
 
308
- const normScope = (root) => String(root || "").replace(/\\/g, "/").replace(/^\.\//, "").replace(/^\/+|\/+$/g, "");
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
- }
288
+ export const computeScopedDepFindings = createScopedDepFindings(computeDepFindings)
346
289
 
347
290
  export { computeGoDepFindings, computePyDepFindings } from "./dep-check-ecosystems.js";
@@ -1,303 +1,10 @@
1
- // Pure structure checker (replaces dependency-cruiser's core): circular dependencies (iterative Tarjan
2
- // SCC + one representative cycle each), orphan files, and a small glob boundary-rule DSL evaluated over
3
- // the graph's file-level import edges. NO filesystem — internal-audit.js feeds it. DEPS_SECURITY_PLAN P2.
4
- //
5
- // Honest gaps vs depcruise: we don't follow imports INTO node_modules and don't run enhanced-resolve
6
- // (package.json#exports / webpack resolution), so some cycles it sees we can't. Cycles we DO report are
7
- // built from EXTRACTED import edges → high confidence.
8
- import { makeFinding } from "./findings.js";
9
- import { ENTRY_FILE } from "./dead-check.js";
10
- import { formatRepresentativeCycle } from "./cycle-route.js";
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,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
+ ])
@@ -0,0 +1,45 @@
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
+ sourceTexts = new Map(), 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
+ const sourceTextsByScope = new Map(scopes.map((scope) => [scope, new Map()]))
16
+ for (const item of externalImports) {
17
+ const owner = scopes.find((scope) => ownsFile(scope.root, item.file)) || scopes[scopes.length - 1]
18
+ importsByScope.get(owner).push(item)
19
+ }
20
+ for (const file of sourceFiles) {
21
+ const owner = scopes.find((scope) => ownsFile(scope.root, file)) || scopes[scopes.length - 1]
22
+ sourceFilesByScope.get(owner).push(file)
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
+ }
28
+ const configOwner = new Map()
29
+ for (const [file] of configTexts) configOwner.set(file, scopes.find((scope) => ownsFile(scope.root, file)) || scopes[scopes.length - 1])
30
+ const findings = [], usedPackages = new Map(), declared = new Set()
31
+ for (const scope of scopes) {
32
+ const scopeConfig = new Map([...configTexts].filter(([file]) => configOwner.get(file) === scope))
33
+ const result = computeDepFindings({
34
+ externalImports: importsByScope.get(scope), pkg: scope.pkg || {}, workspacePkgNames,
35
+ configTexts: scopeConfig, sourceTexts: sourceTextsByScope.get(scope), aliases: scope.aliases || [], scope: scope.root,
36
+ manifest: scope.manifest || (scope.root ? `${scope.root}/package.json` : 'package.json'),
37
+ nonRuntimeRoots, sourceFiles: sourceFilesByScope.get(scope),
38
+ })
39
+ findings.push(...result.findings)
40
+ for (const [name, use] of result.usedPackages) usedPackages.set(`${scope.root || '.'}:${name}`, use)
41
+ for (const name of result.declared) declared.add(`${scope.root || '.'}:${name}`)
42
+ }
43
+ return {findings, usedPackages, declared}
44
+ }
45
+ }
@@ -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
+ }