sparda-mcp 0.69.1 → 0.70.0

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/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "sparda-mcp",
3
- "version": "0.69.1",
3
+ "version": "0.70.0",
4
4
  "mcpName": "io.github.zyx77550/sparda-mcp",
5
- "description": "AI writes. SPARDA proves. A deterministic, offline gate that catches when an AI edit removes a guard, exposes a route, or breaks an invariant no API key, right in the agent edit loop.",
5
+ "description": "AI writes. SPARDA proves. A deterministic, offline gate that catches when an AI edit removes a guard, exposes a route, or breaks an invariant \u2014 no API key, right in the agent edit loop.",
6
6
  "type": "module",
7
7
  "repository": {
8
8
  "type": "git",
@@ -17,7 +17,7 @@
17
17
  },
18
18
  "scripts": {
19
19
  "test": "vitest run",
20
- "prepublishOnly": "vitest run",
20
+ "prepublishOnly": "node scripts/release-gate.mjs",
21
21
  "corpus": "node scripts/corpus-oracle.mjs",
22
22
  "corpus:update": "node scripts/corpus-oracle.mjs --update",
23
23
  "test:router": "node tests/router-selftest.cjs",
@@ -29,7 +29,8 @@
29
29
  "format:check": "prettier --check \"**/*.{js,cjs,mjs}\"",
30
30
  "bench:check": "node bench/check-readme.mjs",
31
31
  "mutation": "node tests/mutation/run.mjs",
32
- "wedge": "node bench/wedge.mjs"
32
+ "wedge": "node bench/wedge.mjs",
33
+ "release:check": "node scripts/release-gate.mjs"
33
34
  },
34
35
  "files": [
35
36
  "src",
@@ -799,6 +799,36 @@ export function resolveExportedFunction(mod, name, seen = new Set()) {
799
799
  return null;
800
800
  }
801
801
 
802
+ // The CLASS twin of `resolveExportedFunction`, and it exists for the same reason one
803
+ // level up. A monorepo package (`@novu/dal`, `@novu/application-generic`) resolves to its
804
+ // entry file, and that entry file is a BARREL — sixty `export * from './repositories/…'`
805
+ // lines and not one class declaration. `classInModule` looks for a class DECLARED in the
806
+ // module it was handed, so every DI hop into a workspace package died on the barrel:
807
+ // measured on novu, 1479 of 2039 constructor-DI hops resolved to nothing, `PinoLogger`
808
+ // and the repository classes at the top of the list. The route's real behavior — every
809
+ // db_write the repository performs — was simply absent from the graph.
810
+ //
811
+ // Bounded by `seen` (a barrel cycle terminates) and memoized by the caller, because a
812
+ // wide barrel is walked once per hop otherwise.
813
+ export function resolveExportedClass(mod, name, seen = new Set()) {
814
+ if (!mod || mod.error) return null;
815
+ const direct = classInModule(mod, name);
816
+ if (direct) return { cls: direct, mod };
817
+ const named = mod.reexports.get(name); // export { X } from './x'
818
+ if (named && !seen.has(named)) {
819
+ seen.add(named);
820
+ const hit = resolveExportedClass(parseModule(named), name, seen);
821
+ if (hit) return hit;
822
+ }
823
+ for (const file of mod.starReexports ?? []) {
824
+ if (seen.has(file)) continue;
825
+ seen.add(file);
826
+ const hit = resolveExportedClass(parseModule(file), name, seen);
827
+ if (hit) return hit;
828
+ }
829
+ return null;
830
+ }
831
+
802
832
  function collectTopLevel(node, facts, absFile, exported) {
803
833
  if (node.type === 'ExportNamedDeclaration' && node.declaration) {
804
834
  collectTopLevel(node.declaration, facts, absFile, true);
@@ -26,6 +26,7 @@ import {
26
26
  methodInClassChain,
27
27
  computeThisSymbols,
28
28
  resolveExportedFunction,
29
+ resolveExportedClass,
29
30
  collectRepoFields,
30
31
  collectReqDerived,
31
32
  reqParamName,
@@ -156,6 +157,7 @@ function seedTaint(fn, args, callerReq) {
156
157
  // live here, scoped to the run.
157
158
  export function createResolver({ cwd, scannedFiles, helpers }) {
158
159
  const classBundles = new Map(); // memo: per (class file, class.method[, symbols])
160
+ const classLookups = new Map(); // memo: per (module, class name) — barrel walks
159
161
  const rel = (abs) => relOf(cwd, abs);
160
162
 
161
163
  // ---- member-call following (imports, instantiated services, this/super) ----
@@ -411,9 +413,22 @@ export function createResolver({ cwd, scannedFiles, helpers }) {
411
413
  const file = mod.imports.get(className);
412
414
  const clsMod = file ? parseModule(file) : mod;
413
415
  if (clsMod.error) return null;
414
- const cls = classInModule(clsMod, className);
415
- if (!cls) return null;
416
- return classMethodBundle(cls, clsMod, methodName, depth, stack, thisSymbols);
416
+ const hit = classThroughBarrels(clsMod, className);
417
+ if (!hit) return null;
418
+ return classMethodBundle(hit.cls, hit.mod, methodName, depth, stack, thisSymbols);
419
+ }
420
+
421
+ // A class is looked up THROUGH barrels: declared here, or re-exported from a module
422
+ // this one re-exports. A monorepo package entry point is a barrel and nothing else, so
423
+ // without this every DI hop into a workspace package resolves to nothing at all —
424
+ // silently, since an unresolved hop leaves no trace of its own. Memoized per (module,
425
+ // class): a sixty-line barrel is otherwise re-walked once per hop.
426
+ function classThroughBarrels(clsMod, className) {
427
+ const key = `cls:${clsMod._file ?? ''}#${className}`;
428
+ if (classLookups.has(key)) return classLookups.get(key);
429
+ const hit = resolveExportedClass(clsMod, className);
430
+ classLookups.set(key, hit);
431
+ return hit;
417
432
  }
418
433
 
419
434
  // The fully-resolved scan of `<topCls>.<methodName>` — its body plus everything