plumb-tools 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Shashank Gupta
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,95 @@
1
+ # plumb-tools
2
+
3
+ LLM-free TypeScript architecture and boundary analysis toolset. Provides the static-analysis engine behind [`plumb-mcp-server`](https://www.npmjs.com/package/plumb-mcp-server) and the [Plumb](https://plumb.sgupta.dev) case study.
4
+
5
+ No API key required. No inference. Returns grounded structural facts that an agent or a human can reason over.
6
+
7
+ ---
8
+
9
+ ## What it does
10
+
11
+ Three exported `AnalysisTool` implementations, each wrapping a well-known OSS static-analysis tool:
12
+
13
+ | Export | Underlying tool | What it returns |
14
+ |---|---|---|
15
+ | `dependencyGraphTool` | dependency-cruiser | Flat edge list: every import in the repo with `from`, `to`, `importPath`, `line`, `external`, `dynamic` |
16
+ | `publicSurfaceTool` | ts-morph | Per-package public API surface: the set of modules reachable from each `src/index.ts` entrypoint |
17
+ | `detectCyclesTool` | madge | Circular dependency rings as lists of repo-relative file paths |
18
+
19
+ These three tools are composed by the agent layer (in `plumb-mcp-server` and the Plumb arch-reviewer agent) to classify dependency edges and surface architectural boundary violations.
20
+
21
+ ---
22
+
23
+ ## Installation
24
+
25
+ ```sh
26
+ pnpm add plumb-tools
27
+ # or
28
+ npm install plumb-tools
29
+ ```
30
+
31
+ Requires Node.js 18+.
32
+
33
+ ---
34
+
35
+ ## Library usage
36
+
37
+ ```ts
38
+ import {
39
+ analyzeDependencies,
40
+ analyzePublicSurface,
41
+ detectCycles,
42
+ } from 'plumb-tools';
43
+
44
+ const repoPath = '/absolute/path/to/your/repo';
45
+
46
+ const graph = await analyzeDependencies({ repoPath });
47
+ // graph.edges: DependencyEdge[]
48
+
49
+ const surface = await analyzePublicSurface({ repoPath });
50
+ // surface.packages: PackageSurface[]
51
+
52
+ const cycles = await detectCycles({ repoPath });
53
+ // cycles.cycles: DependencyCycle[]
54
+ ```
55
+
56
+ You can also invoke the pre-wired `AnalysisTool` objects directly (these are what `plumb-mcp-server` registers as MCP tools):
57
+
58
+ ```ts
59
+ import { dependencyGraphTool, publicSurfaceTool, detectCyclesTool } from 'plumb-tools';
60
+
61
+ const result = await dependencyGraphTool.execute({ repoPath });
62
+ ```
63
+
64
+ ---
65
+
66
+ ## Exported types
67
+
68
+ ```ts
69
+ // dependency graph
70
+ type DependencyEdge = { from: string; to: string | null; importPath: string; line: number | null; external: boolean; dynamic: boolean };
71
+ type DependencyGraphFacts = { repoPath: string; modules: string[]; edges: DependencyEdge[]; generatedWith: { tool: 'dependency-cruiser'; version: string } };
72
+
73
+ // public surface
74
+ type PackageSurface = { packageRoot: string; entrypoint: string | null; publicModules: string[] };
75
+ type PublicSurfaceFacts = { repoPath: string; packages: PackageSurface[]; generatedWith: string };
76
+ type ImportSurfaceClass = 'via-entrypoint' | 'bypasses-entrypoint' | 'intra-package' | 'not-a-package-import';
77
+
78
+ // cycles
79
+ type DependencyCycle = { members: string[] };
80
+ type CycleFacts = { repoPath: string; cycles: DependencyCycle[]; generatedWith: string };
81
+ ```
82
+
83
+ ---
84
+
85
+ ## Known limitations
86
+
87
+ - The `doNotFollow` field on `dependencyGraphTool`'s input schema is accepted but not forwarded to dependency-cruiser at runtime. It is a documented stub.
88
+ - No timeout or file-count limit is imposed on analysis. Point it at a developer-sized repository; avoid enormous monorepos or pathologically deep trees.
89
+ - `analyzePublicSurface` only recognises packages that have a `src/index.ts` entrypoint. Packages without one get `entrypoint: null` and `publicModules: []`.
90
+
91
+ ---
92
+
93
+ ## License
94
+
95
+ MIT — see `LICENSE`.
@@ -0,0 +1,23 @@
1
+ export interface DependencyEdge {
2
+ from: string;
3
+ to: string | null;
4
+ importPath: string;
5
+ line: number | null;
6
+ external: boolean;
7
+ dynamic: boolean;
8
+ }
9
+ export interface DependencyGraphFacts {
10
+ repoPath: string;
11
+ modules: string[];
12
+ edges: DependencyEdge[];
13
+ generatedWith: {
14
+ tool: 'dependency-cruiser';
15
+ version: string;
16
+ };
17
+ }
18
+ export interface AnalyzeOptions {
19
+ doNotFollow?: string;
20
+ }
21
+ export declare function findImportLine(absSourcePath: string, importPath: string): number | null;
22
+ export declare function analyzeDependencies(repoPath: string, options?: AnalyzeOptions): Promise<DependencyGraphFacts>;
23
+ //# sourceMappingURL=analyze-dependencies.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"analyze-dependencies.d.ts","sourceRoot":"","sources":["../src/analyze-dependencies.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IAInB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,QAAQ,EAAE,OAAO,CAAC;IAClB,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,oBAAoB;IACnC,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,KAAK,EAAE,cAAc,EAAE,CAAC;IACxB,aAAa,EAAE;QAAE,IAAI,EAAE,oBAAoB,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;CAChE;AAED,MAAM,WAAW,cAAc;IAC7B,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAYD,wBAAgB,cAAc,CAAC,aAAa,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAmCvF;AAoCD,wBAAsB,mBAAmB,CACvC,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE,cAAc,GACvB,OAAO,CAAC,oBAAoB,CAAC,CA+C/B"}
@@ -0,0 +1,127 @@
1
+ import { cruise } from 'dependency-cruiser';
2
+ import { readFileSync } from 'node:fs';
3
+ import { dirname, join } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ // dependency-cruiser does not emit per-import line numbers. Scan the source
6
+ // for the specifier as an import/export/require/dynamic-import token — first
7
+ // matching line wins.
8
+ //
9
+ // WHY anchored to import syntax: a bare quoted-string search would match any
10
+ // comment or string literal that mentions the path. Anchoring to the actual
11
+ // import/export/require call sites means only real module references are cited.
12
+ //
13
+ // Returns null when the specifier cannot be found, or when the file cannot be
14
+ // read — so callers see an honest "unknown" rather than a misleading line 1.
15
+ export function findImportLine(absSourcePath, importPath) {
16
+ let lines;
17
+ try {
18
+ lines = readFileSync(absSourcePath, 'utf-8').split('\n');
19
+ }
20
+ catch {
21
+ return null;
22
+ }
23
+ const escaped = importPath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
24
+ // Matches:
25
+ // import ... from 'path' (static import)
26
+ // export ... from 'path' (re-export)
27
+ // require('path') (CommonJS)
28
+ // import('path') (dynamic import)
29
+ // Both single and double quotes are accepted.
30
+ //
31
+ // Comment-aware: a line whose non-whitespace content begins with '//' or
32
+ // '*' (line comment or block-comment continuation) is skipped even if it
33
+ // contains a syntactically valid import expression. This prevents a
34
+ // commented-out import like `// import 'x'` from being mis-cited.
35
+ const re = new RegExp(`(?:^|\\b)(?:import|export|require)\\b[^'"]*['"]${escaped}['"]` +
36
+ `|(?:^|\\b)import\\s*\\(['"]${escaped}['"]\\)`);
37
+ const idx = lines.findIndex((l) => {
38
+ const trimmed = l.trimStart();
39
+ if (trimmed.startsWith('//') || trimmed.startsWith('*') || trimmed.startsWith('/*'))
40
+ return false;
41
+ return re.test(l);
42
+ });
43
+ if (idx < 0) {
44
+ console.warn(`[tools] findImportLine: could not locate import of '${importPath}' in ${absSourcePath}`);
45
+ return null;
46
+ }
47
+ return idx + 1;
48
+ }
49
+ function isExternal(dep) {
50
+ return (dep.coreModule ||
51
+ dep.couldNotResolve ||
52
+ (!dep.dependencyTypes.includes('local') &&
53
+ !dep.dependencyTypes.includes('localmodule')));
54
+ }
55
+ function readDcVersion() {
56
+ try {
57
+ let dir = dirname(fileURLToPath(import.meta.resolve('dependency-cruiser')));
58
+ for (;;) {
59
+ try {
60
+ const pkg = JSON.parse(readFileSync(join(dir, 'package.json'), 'utf-8'));
61
+ if (pkg.name === 'dependency-cruiser' && pkg.version != null) {
62
+ return pkg.version;
63
+ }
64
+ }
65
+ catch {
66
+ // not a package root — keep walking up
67
+ }
68
+ const parent = dirname(dir);
69
+ if (parent === dir)
70
+ break;
71
+ dir = parent;
72
+ }
73
+ }
74
+ catch {
75
+ // ignore — return fallback below
76
+ }
77
+ return 'unknown';
78
+ }
79
+ export async function analyzeDependencies(repoPath, options) {
80
+ // Phase 1: doNotFollow reserved; dependency-cruiser config is hardcoded for now.
81
+ void options;
82
+ const raw = await cruise(['.'], {
83
+ baseDir: repoPath,
84
+ // Follow everything except node_modules; they still appear as edges (external: true).
85
+ doNotFollow: { path: 'node_modules' },
86
+ });
87
+ const result = raw.output;
88
+ const edgeList = [];
89
+ const seen = new Set();
90
+ for (const mod of result.modules) {
91
+ if (mod.source.includes('node_modules'))
92
+ continue;
93
+ seen.add(mod.source);
94
+ for (const dep of mod.dependencies) {
95
+ const ext = isExternal(dep);
96
+ edgeList.push({
97
+ from: mod.source,
98
+ to: ext ? null : dep.resolved,
99
+ importPath: dep.module,
100
+ line: findImportLine(join(repoPath, mod.source), dep.module),
101
+ external: ext,
102
+ dynamic: dep.dynamic,
103
+ });
104
+ }
105
+ }
106
+ // Deterministic order: primary key = from-file, secondary = line number
107
+ // (nulls sort last within a file).
108
+ edgeList.sort((a, b) => {
109
+ const c = a.from.localeCompare(b.from);
110
+ if (c !== 0)
111
+ return c;
112
+ if (a.line === null && b.line === null)
113
+ return 0;
114
+ if (a.line === null)
115
+ return 1;
116
+ if (b.line === null)
117
+ return -1;
118
+ return a.line - b.line;
119
+ });
120
+ return {
121
+ repoPath,
122
+ modules: [...seen].sort(),
123
+ edges: edgeList,
124
+ generatedWith: { tool: 'dependency-cruiser', version: readDcVersion() },
125
+ };
126
+ }
127
+ //# sourceMappingURL=analyze-dependencies.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"analyze-dependencies.js","sourceRoot":"","sources":["../src/analyze-dependencies.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,21 @@
1
+ import { z } from 'zod';
2
+ import type { AnalysisTool } from './tool.js';
3
+ export interface PackageSurface {
4
+ packageRoot: string;
5
+ entrypoint: string | null;
6
+ publicModules: string[];
7
+ }
8
+ export interface PublicSurfaceFacts {
9
+ repoPath: string;
10
+ packages: PackageSurface[];
11
+ generatedWith: string;
12
+ }
13
+ export type ImportSurfaceClass = 'via-entrypoint' | 'bypasses-entrypoint' | 'intra-package' | 'not-a-package-import' | 'unknown';
14
+ export declare function analyzePublicSurface(repoPath: string): Promise<PublicSurfaceFacts>;
15
+ export declare function classifyImport(facts: PublicSurfaceFacts, fromFile: string, resolvedTarget: string | null): ImportSurfaceClass;
16
+ export declare const PublicSurfaceInputSchema: z.ZodObject<{
17
+ repoPath: z.ZodString;
18
+ }, z.core.$strip>;
19
+ export type PublicSurfaceInput = z.infer<typeof PublicSurfaceInputSchema>;
20
+ export declare const publicSurfaceTool: AnalysisTool<PublicSurfaceInput, PublicSurfaceFacts>;
21
+ //# sourceMappingURL=analyze-public-surface.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"analyze-public-surface.d.ts","sourceRoot":"","sources":["../src/analyze-public-surface.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAE9C,MAAM,WAAW,cAAc;IAC7B,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,aAAa,EAAE,MAAM,EAAE,CAAC;CACzB;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,cAAc,EAAE,CAAC;IAC3B,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,MAAM,kBAAkB,GAC1B,gBAAgB,GAChB,qBAAqB,GACrB,eAAe,GACf,sBAAsB,GACtB,SAAS,CAAC;AAmFd,wBAAsB,oBAAoB,CACxC,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,kBAAkB,CAAC,CAwD7B;AAED,wBAAgB,cAAc,CAC5B,KAAK,EAAE,kBAAkB,EACzB,QAAQ,EAAE,MAAM,EAChB,cAAc,EAAE,MAAM,GAAG,IAAI,GAC5B,kBAAkB,CA6BpB;AAED,eAAO,MAAM,wBAAwB;;iBAEnC,CAAC;AAEH,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,wBAAwB,CAAC,CAAC;AAE1E,eAAO,MAAM,iBAAiB,EAAE,YAAY,CAAC,kBAAkB,EAAE,kBAAkB,CAQlF,CAAC"}
@@ -0,0 +1,157 @@
1
+ import { Project } from 'ts-morph';
2
+ import { readFileSync } from 'node:fs';
3
+ import { basename, dirname, join } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { z } from 'zod';
6
+ import { globSync } from 'node:fs';
7
+ function readTsMorphVersion() {
8
+ try {
9
+ let dir = dirname(fileURLToPath(import.meta.resolve('ts-morph')));
10
+ for (;;) {
11
+ try {
12
+ const pkg = JSON.parse(readFileSync(join(dir, 'package.json'), 'utf-8'));
13
+ if (pkg.name === 'ts-morph' && pkg.version != null) {
14
+ return pkg.version;
15
+ }
16
+ }
17
+ catch {
18
+ // not a package root — keep walking up
19
+ }
20
+ const parent = dirname(dir);
21
+ if (parent === dir)
22
+ break;
23
+ dir = parent;
24
+ }
25
+ }
26
+ catch {
27
+ // ignore
28
+ }
29
+ return 'unknown';
30
+ }
31
+ // Collect all repo-relative module paths reachable via re-export chains from entrypoint.
32
+ // Only follows `export * from '...'` and `export { ... } from '...'` declarations.
33
+ function collectPublicModules(project, entrypointAbsPath, repoPath) {
34
+ const visited = new Set();
35
+ const queue = [entrypointAbsPath];
36
+ while (queue.length > 0) {
37
+ const absPath = queue.shift();
38
+ if (absPath === undefined || visited.has(absPath))
39
+ continue;
40
+ visited.add(absPath);
41
+ const sf = project.getSourceFile(absPath);
42
+ if (sf === undefined) {
43
+ console.warn(`[tools] analyzePublicSurface: source file not found in project: ${absPath}`);
44
+ continue;
45
+ }
46
+ for (const exportDecl of sf.getExportDeclarations()) {
47
+ const moduleSpecifier = exportDecl.getModuleSpecifierValue();
48
+ if (moduleSpecifier === undefined)
49
+ continue;
50
+ const resolved = exportDecl.getModuleSpecifierSourceFile();
51
+ if (resolved === undefined) {
52
+ console.warn(`[tools] analyzePublicSurface: could not resolve re-export '${moduleSpecifier}' in ${absPath}`);
53
+ continue;
54
+ }
55
+ const resolvedPath = resolved.getFilePath();
56
+ if (!visited.has(resolvedPath)) {
57
+ queue.push(resolvedPath);
58
+ }
59
+ }
60
+ }
61
+ const result = [];
62
+ for (const absPath of visited) {
63
+ if (absPath.startsWith(repoPath + '/')) {
64
+ result.push(absPath.slice(repoPath.length + 1));
65
+ }
66
+ }
67
+ result.sort();
68
+ return result;
69
+ }
70
+ // Defensive: use basename so the predicate works whether Node passes a bare
71
+ // segment ("node_modules") or a path-joined form — behaviour is identical on
72
+ // current Node (bare segments), but this avoids a silent gap if that changes.
73
+ const excludeNodeModules = (name) => basename(String(name)) === 'node_modules';
74
+ export async function analyzePublicSurface(repoPath) {
75
+ // Discover all src/index.ts files to identify packages.
76
+ const entrypointPaths = globSync('**/src/index.ts', {
77
+ cwd: repoPath,
78
+ exclude: excludeNodeModules,
79
+ });
80
+ // Build one ts-morph project over the repo so cross-file resolution works.
81
+ const project = new Project({
82
+ skipAddingFilesFromTsConfig: true,
83
+ compilerOptions: {
84
+ allowJs: false,
85
+ resolveJsonModule: false,
86
+ },
87
+ });
88
+ // Add all discovered entrypoints and their transitive local dependencies.
89
+ for (const rel of entrypointPaths) {
90
+ project.addSourceFileAtPath(join(repoPath, rel));
91
+ }
92
+ // Also add every .ts file under those package roots so resolution succeeds.
93
+ const tsFiles = globSync('**/src/**/*.ts', {
94
+ cwd: repoPath,
95
+ exclude: excludeNodeModules,
96
+ });
97
+ for (const rel of tsFiles) {
98
+ const absPath = join(repoPath, rel);
99
+ if (project.getSourceFile(absPath) === undefined) {
100
+ project.addSourceFileAtPath(absPath);
101
+ }
102
+ }
103
+ const tsMorphVersion = readTsMorphVersion();
104
+ const packages = [];
105
+ for (const entrypointRel of entrypointPaths.sort()) {
106
+ // packageRoot is the directory above src/index.ts, repo-relative POSIX.
107
+ // entrypointRel looks like "shared/src/index.ts" → packageRoot = "shared"
108
+ const packageRoot = entrypointRel.replace(/\/src\/index\.ts$/, '');
109
+ const entrypointAbsPath = join(repoPath, entrypointRel);
110
+ const publicModules = collectPublicModules(project, entrypointAbsPath, repoPath);
111
+ packages.push({
112
+ packageRoot,
113
+ entrypoint: entrypointRel,
114
+ publicModules,
115
+ });
116
+ }
117
+ return {
118
+ repoPath,
119
+ packages,
120
+ generatedWith: `ts-morph@${tsMorphVersion}`,
121
+ };
122
+ }
123
+ export function classifyImport(facts, fromFile, resolvedTarget) {
124
+ if (resolvedTarget === null)
125
+ return 'unknown';
126
+ // Find the package P whose packageRoot contains resolvedTarget.
127
+ // Use the longest matching packageRoot to handle nested packages correctly
128
+ // (e.g. "remotes/analytics" must win over a hypothetical "remotes" package).
129
+ const targetPackage = facts.packages
130
+ .filter((p) => resolvedTarget === p.entrypoint ||
131
+ resolvedTarget.startsWith(p.packageRoot + '/'))
132
+ .reduce((best, p) => best === undefined || p.packageRoot.length > best.packageRoot.length
133
+ ? p
134
+ : best, undefined);
135
+ if (targetPackage === undefined)
136
+ return 'not-a-package-import';
137
+ // fromFile is inside P if it starts with packageRoot + '/'.
138
+ const fromIsInside = fromFile.startsWith(targetPackage.packageRoot + '/');
139
+ if (fromIsInside)
140
+ return 'intra-package';
141
+ // fromFile is outside P.
142
+ if (resolvedTarget === targetPackage.entrypoint)
143
+ return 'via-entrypoint';
144
+ return 'bypasses-entrypoint';
145
+ }
146
+ export const PublicSurfaceInputSchema = z.object({
147
+ repoPath: z.string(),
148
+ });
149
+ export const publicSurfaceTool = {
150
+ name: 'public-surface',
151
+ description: 'Analyses a repository to discover packages (directories containing src/index.ts) and their public module surface using ts-morph. Returns structural facts only — no findings, no LLM calls.',
152
+ inputSchema: PublicSurfaceInputSchema,
153
+ run(input) {
154
+ return analyzePublicSurface(input.repoPath);
155
+ },
156
+ };
157
+ //# sourceMappingURL=analyze-public-surface.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"analyze-public-surface.js","sourceRoot":"","sources":["../src/analyze-public-surface.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ import { z } from 'zod';
2
+ import type { AnalysisTool } from './tool.js';
3
+ export interface DependencyCycle {
4
+ members: string[];
5
+ }
6
+ export interface CycleFacts {
7
+ repoPath: string;
8
+ cycles: DependencyCycle[];
9
+ generatedWith: string;
10
+ }
11
+ export declare function detectCycles(repoPath: string): Promise<CycleFacts>;
12
+ export declare const DetectCyclesInputSchema: z.ZodObject<{
13
+ repoPath: z.ZodString;
14
+ }, z.core.$strip>;
15
+ export type DetectCyclesInput = z.infer<typeof DetectCyclesInputSchema>;
16
+ export declare const detectCyclesTool: AnalysisTool<DetectCyclesInput, CycleFacts>;
17
+ //# sourceMappingURL=detect-cycles.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"detect-cycles.d.ts","sourceRoot":"","sources":["../src/detect-cycles.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAE9C,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAED,MAAM,WAAW,UAAU;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,eAAe,EAAE,CAAC;IAC1B,aAAa,EAAE,MAAM,CAAC;CACvB;AA2BD,wBAAsB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAsBxE;AAED,eAAO,MAAM,uBAAuB;;iBAElC,CAAC;AAEH,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,uBAAuB,CAAC,CAAC;AAExE,eAAO,MAAM,gBAAgB,EAAE,YAAY,CAAC,iBAAiB,EAAE,UAAU,CAQxE,CAAC"}
@@ -0,0 +1,63 @@
1
+ import madge from 'madge';
2
+ import { readFileSync } from 'node:fs';
3
+ import { dirname, join } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { z } from 'zod';
6
+ function readMadgeVersion() {
7
+ try {
8
+ let dir = dirname(fileURLToPath(import.meta.resolve('madge')));
9
+ for (;;) {
10
+ try {
11
+ const pkg = JSON.parse(readFileSync(join(dir, 'package.json'), 'utf-8'));
12
+ if (pkg.name === 'madge' && pkg.version != null) {
13
+ return pkg.version;
14
+ }
15
+ }
16
+ catch {
17
+ // not a package root — keep walking up
18
+ }
19
+ const parent = dirname(dir);
20
+ if (parent === dir)
21
+ break;
22
+ dir = parent;
23
+ }
24
+ }
25
+ catch {
26
+ // ignore
27
+ }
28
+ return 'unknown';
29
+ }
30
+ export async function detectCycles(repoPath) {
31
+ let result;
32
+ try {
33
+ result = await madge(repoPath, {
34
+ fileExtensions: ['ts', 'tsx'],
35
+ detectiveOptions: {
36
+ ts: { skipTypeImports: true },
37
+ },
38
+ });
39
+ }
40
+ catch (err) {
41
+ console.warn('[tools] detectCycles: madge failed', err);
42
+ return { repoPath, cycles: [], generatedWith: `madge@${readMadgeVersion()}` };
43
+ }
44
+ const rawCycles = result.circular();
45
+ const cycles = rawCycles.map((members) => ({ members }));
46
+ return {
47
+ repoPath,
48
+ cycles,
49
+ generatedWith: `madge@${readMadgeVersion()}`,
50
+ };
51
+ }
52
+ export const DetectCyclesInputSchema = z.object({
53
+ repoPath: z.string(),
54
+ });
55
+ export const detectCyclesTool = {
56
+ name: 'detect-cycles',
57
+ description: 'Detects circular dependencies in a TypeScript repository using madge. Returns module-level cycles as lists of repo-relative file paths. Produces structural facts only — no findings, no LLM calls.',
58
+ inputSchema: DetectCyclesInputSchema,
59
+ run(input) {
60
+ return detectCycles(input.repoPath);
61
+ },
62
+ };
63
+ //# sourceMappingURL=detect-cycles.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"detect-cycles.js","sourceRoot":"","sources":["../src/detect-cycles.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAcxB,SAAS,gBAAgB;IACvB,IAAI,CAAC;QACH,IAAI,GAAG,GAAG,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;QAC/D,SAAS,CAAC;YACR,IAAI,CAAC;gBACH,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,CAAC,GAAG,EAAE,cAAc,CAAC,EAAE,OAAO,CAAC,CAGtE,CAAC;gBACF,IAAI,GAAG,CAAC,IAAI,KAAK,OAAO,IAAI,GAAG,CAAC,OAAO,IAAI,IAAI,EAAE,CAAC;oBAChD,OAAO,GAAG,CAAC,OAAO,CAAC;gBACrB,CAAC;YACH,CAAC;YAAC,MAAM,CAAC;gBACP,uCAAuC;YACzC,CAAC;YACD,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC;YAC5B,IAAI,MAAM,KAAK,GAAG;gBAAE,MAAM;YAC1B,GAAG,GAAG,MAAM,CAAC;QACf,CAAC;IACH,CAAC;IAAC,MAAM,CAAC;QACP,SAAS;IACX,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,QAAgB;IACjD,IAAI,MAA2B,CAAC;IAChC,IAAI,CAAC;QACH,MAAM,GAAG,MAAM,KAAK,CAAC,QAAQ,EAAE;YAC7B,cAAc,EAAE,CAAC,IAAI,EAAE,KAAK,CAAC;YAC7B,gBAAgB,EAAE;gBAChB,EAAE,EAAE,EAAE,eAAe,EAAE,IAAI,EAAE;aAC9B;SACF,CAAC,CAAC;IACL,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO,CAAC,IAAI,CAAC,oCAAoC,EAAE,GAAG,CAAC,CAAC;QACxD,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,aAAa,EAAE,SAAS,gBAAgB,EAAE,EAAE,EAAE,CAAC;IAChF,CAAC;IAED,MAAM,SAAS,GAAe,MAAM,CAAC,QAAQ,EAAE,CAAC;IAChD,MAAM,MAAM,GAAsB,SAAS,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC;IAE5E,OAAO;QACL,QAAQ;QACR,MAAM;QACN,aAAa,EAAE,SAAS,gBAAgB,EAAE,EAAE;KAC7C,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,MAAM,uBAAuB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC9C,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE;CACrB,CAAC,CAAC;AAIH,MAAM,CAAC,MAAM,gBAAgB,GAAgD;IAC3E,IAAI,EAAE,eAAe;IACrB,WAAW,EACT,qMAAqM;IACvM,WAAW,EAAE,uBAAuB;IACpC,GAAG,CAAC,KAAwB;QAC1B,OAAO,YAAY,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;IACtC,CAAC;CACF,CAAC"}
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Typed wrappers around static-analysis tools live here (later phases).
3
+ * This package never depends on an LLM provider.
4
+ */
5
+ export declare const version = "0.1.0";
6
+ export type { AnalyzeOptions, DependencyEdge, DependencyGraphFacts, } from './analyze-dependencies.js';
7
+ export { analyzeDependencies, findImportLine } from './analyze-dependencies.js';
8
+ export type { AnalysisTool, DependencyGraphInput } from './tool.js';
9
+ export { dependencyGraphTool, DependencyGraphInputSchema } from './tool.js';
10
+ export type { PackageSurface, PublicSurfaceFacts, ImportSurfaceClass, PublicSurfaceInput, } from './analyze-public-surface.js';
11
+ export { analyzePublicSurface, classifyImport, publicSurfaceTool, PublicSurfaceInputSchema, } from './analyze-public-surface.js';
12
+ export type { DependencyCycle, CycleFacts, DetectCyclesInput, } from './detect-cycles.js';
13
+ export { detectCycles, detectCyclesTool, DetectCyclesInputSchema, } from './detect-cycles.js';
14
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,eAAO,MAAM,OAAO,UAAU,CAAC;AAE/B,YAAY,EACV,cAAc,EACd,cAAc,EACd,oBAAoB,GACrB,MAAM,2BAA2B,CAAC;AACnC,OAAO,EAAE,mBAAmB,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAEhF,YAAY,EAAE,YAAY,EAAE,oBAAoB,EAAE,MAAM,WAAW,CAAC;AACpE,OAAO,EAAE,mBAAmB,EAAE,0BAA0B,EAAE,MAAM,WAAW,CAAC;AAE5E,YAAY,EACV,cAAc,EACd,kBAAkB,EAClB,kBAAkB,EAClB,kBAAkB,GACnB,MAAM,6BAA6B,CAAC;AACrC,OAAO,EACL,oBAAoB,EACpB,cAAc,EACd,iBAAiB,EACjB,wBAAwB,GACzB,MAAM,6BAA6B,CAAC;AAErC,YAAY,EACV,eAAe,EACf,UAAU,EACV,iBAAiB,GAClB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,YAAY,EACZ,gBAAgB,EAChB,uBAAuB,GACxB,MAAM,oBAAoB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Typed wrappers around static-analysis tools live here (later phases).
3
+ * This package never depends on an LLM provider.
4
+ */
5
+ export const version = '0.1.0';
6
+ export { analyzeDependencies, findImportLine } from './analyze-dependencies.js';
7
+ export { dependencyGraphTool, DependencyGraphInputSchema } from './tool.js';
8
+ export { analyzePublicSurface, classifyImport, publicSurfaceTool, PublicSurfaceInputSchema, } from './analyze-public-surface.js';
9
+ export { detectCycles, detectCyclesTool, DetectCyclesInputSchema, } from './detect-cycles.js';
10
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,CAAC,MAAM,OAAO,GAAG,OAAO,CAAC;AAO/B,OAAO,EAAE,mBAAmB,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAGhF,OAAO,EAAE,mBAAmB,EAAE,0BAA0B,EAAE,MAAM,WAAW,CAAC;AAQ5E,OAAO,EACL,oBAAoB,EACpB,cAAc,EACd,iBAAiB,EACjB,wBAAwB,GACzB,MAAM,6BAA6B,CAAC;AAOrC,OAAO,EACL,YAAY,EACZ,gBAAgB,EAChB,uBAAuB,GACxB,MAAM,oBAAoB,CAAC"}
package/dist/tool.d.ts ADDED
@@ -0,0 +1,15 @@
1
+ import { z } from 'zod';
2
+ import type { DependencyGraphFacts } from './analyze-dependencies.js';
3
+ export interface AnalysisTool<TInput, TOutput> {
4
+ name: string;
5
+ description: string;
6
+ inputSchema: z.ZodType<TInput>;
7
+ run(input: TInput): Promise<TOutput>;
8
+ }
9
+ export declare const DependencyGraphInputSchema: z.ZodObject<{
10
+ repoPath: z.ZodString;
11
+ doNotFollow: z.ZodOptional<z.ZodString>;
12
+ }, z.core.$strip>;
13
+ export type DependencyGraphInput = z.infer<typeof DependencyGraphInputSchema>;
14
+ export declare const dependencyGraphTool: AnalysisTool<DependencyGraphInput, DependencyGraphFacts>;
15
+ //# sourceMappingURL=tool.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tool.d.ts","sourceRoot":"","sources":["../src/tool.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAC;AAItE,MAAM,WAAW,YAAY,CAAC,MAAM,EAAE,OAAO;IAC3C,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;IAC/B,GAAG,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACtC;AAED,eAAO,MAAM,0BAA0B;;;iBAGrC,CAAC;AAEH,MAAM,MAAM,oBAAoB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,0BAA0B,CAAC,CAAC;AAE9E,eAAO,MAAM,mBAAmB,EAAE,YAAY,CAAC,oBAAoB,EAAE,oBAAoB,CASxF,CAAC"}
package/dist/tool.js ADDED
@@ -0,0 +1,16 @@
1
+ import { z } from 'zod';
2
+ import { analyzeDependencies } from './analyze-dependencies.js';
3
+ export const DependencyGraphInputSchema = z.object({
4
+ repoPath: z.string(),
5
+ doNotFollow: z.string().optional(),
6
+ });
7
+ export const dependencyGraphTool = {
8
+ name: 'dependency-graph',
9
+ description: 'Analyses module dependencies in a repository using dependency-cruiser and returns raw edges (from, to, importPath, line, external, dynamic). Produces facts only — no findings.',
10
+ inputSchema: DependencyGraphInputSchema,
11
+ run(input) {
12
+ const opts = input.doNotFollow !== undefined ? { doNotFollow: input.doNotFollow } : undefined;
13
+ return analyzeDependencies(input.repoPath, opts);
14
+ },
15
+ };
16
+ //# sourceMappingURL=tool.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tool.js","sourceRoot":"","sources":["../src/tool.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AAYhE,MAAM,CAAC,MAAM,0BAA0B,GAAG,CAAC,CAAC,MAAM,CAAC;IACjD,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE;IACpB,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;CACnC,CAAC,CAAC;AAIH,MAAM,CAAC,MAAM,mBAAmB,GAA6D;IAC3F,IAAI,EAAE,kBAAkB;IACxB,WAAW,EACT,iLAAiL;IACnL,WAAW,EAAE,0BAA0B;IACvC,GAAG,CAAC,KAA2B;QAC7B,MAAM,IAAI,GAAG,KAAK,CAAC,WAAW,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,KAAK,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;QAC9F,OAAO,mBAAmB,CAAC,KAAK,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;IACnD,CAAC;CACF,CAAC"}
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "plumb-tools",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "main": "./dist/index.js",
6
+ "types": "./dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "default": "./dist/index.js"
11
+ }
12
+ },
13
+ "description": "LLM-free TypeScript architecture and boundary analysis toolset: dependency-cruiser dependency graph, ts-morph public-surface mapping, and madge cycle detection",
14
+ "author": "Shashank Gupta",
15
+ "license": "MIT",
16
+ "keywords": [
17
+ "architecture",
18
+ "dependency-analysis",
19
+ "typescript",
20
+ "monorepo",
21
+ "boundaries",
22
+ "dependency-cruiser",
23
+ "madge",
24
+ "ts-morph",
25
+ "static-analysis"
26
+ ],
27
+ "files": [
28
+ "dist",
29
+ "README.md",
30
+ "LICENSE"
31
+ ],
32
+ "publishConfig": {
33
+ "access": "public"
34
+ },
35
+ "dependencies": {
36
+ "dependency-cruiser": "^17.4.3",
37
+ "madge": "^8.0.0",
38
+ "ts-morph": "^28.0.0",
39
+ "zod": "^4.0.0"
40
+ },
41
+ "devDependencies": {
42
+ "@types/madge": "^5.0.3"
43
+ },
44
+ "scripts": {
45
+ "typecheck": "tsc --noEmit",
46
+ "build": "tsc -p tsconfig.build.json"
47
+ }
48
+ }