@shrkcrft/architecture-guard 0.1.0-alpha.10

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 (34) hide show
  1. package/dist/checks/adapter-leak.d.ts +12 -0
  2. package/dist/checks/adapter-leak.d.ts.map +1 -0
  3. package/dist/checks/adapter-leak.js +75 -0
  4. package/dist/checks/barrel-risks.d.ts +14 -0
  5. package/dist/checks/barrel-risks.d.ts.map +1 -0
  6. package/dist/checks/barrel-risks.js +60 -0
  7. package/dist/checks/contract-checks.d.ts +17 -0
  8. package/dist/checks/contract-checks.d.ts.map +1 -0
  9. package/dist/checks/contract-checks.js +80 -0
  10. package/dist/checks/cycle-severity.d.ts +10 -0
  11. package/dist/checks/cycle-severity.d.ts.map +1 -0
  12. package/dist/checks/cycle-severity.js +85 -0
  13. package/dist/checks/public-api-misuse.d.ts +17 -0
  14. package/dist/checks/public-api-misuse.d.ts.map +1 -0
  15. package/dist/checks/public-api-misuse.js +75 -0
  16. package/dist/checks/run-arch-check.d.ts +27 -0
  17. package/dist/checks/run-arch-check.d.ts.map +1 -0
  18. package/dist/checks/run-arch-check.js +84 -0
  19. package/dist/contract/define-contract.d.ts +8 -0
  20. package/dist/contract/define-contract.d.ts.map +1 -0
  21. package/dist/contract/define-contract.js +23 -0
  22. package/dist/index.d.ts +11 -0
  23. package/dist/index.d.ts.map +1 -0
  24. package/dist/index.js +10 -0
  25. package/dist/schema/contract.d.ts +41 -0
  26. package/dist/schema/contract.d.ts.map +1 -0
  27. package/dist/schema/contract.js +11 -0
  28. package/dist/schema/violation.d.ts +29 -0
  29. package/dist/schema/violation.d.ts.map +1 -0
  30. package/dist/schema/violation.js +1 -0
  31. package/dist/store/arch-report-store.d.ts +51 -0
  32. package/dist/store/arch-report-store.d.ts.map +1 -0
  33. package/dist/store/arch-report-store.js +86 -0
  34. package/package.json +52 -0
@@ -0,0 +1,12 @@
1
+ import { type GraphQueryApi } from '@shrkcrft/graph';
2
+ import type { IArchViolation } from '../schema/violation.js';
3
+ /**
4
+ * Detect layer-skip imports: a file in one architectural role importing
5
+ * a file in a role that should be reached only via an intermediate
6
+ * layer.
7
+ *
8
+ * Heuristic only — names like `*.controller.ts` are the signal. Teams
9
+ * with non-conventional names should use the contract DSL instead.
10
+ */
11
+ export declare function detectAdapterLeaks(api: GraphQueryApi): readonly IArchViolation[];
12
+ //# sourceMappingURL=adapter-leak.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adapter-leak.d.ts","sourceRoot":"","sources":["../../src/checks/adapter-leak.ts"],"names":[],"mappings":"AAAA,OAAO,EAAY,KAAK,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAC/D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAqC7D;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,aAAa,GAAG,SAAS,cAAc,EAAE,CA8BhF"}
@@ -0,0 +1,75 @@
1
+ import { EdgeKind } from '@shrkcrft/graph';
2
+ /**
3
+ * Layer-skip patterns the check looks for by default. Each entry says
4
+ * "a file matching `from` should NOT directly import a file matching
5
+ * `to` — go through `via` instead."
6
+ *
7
+ * The defaults catch the most common backend layering anti-patterns
8
+ * (NestJS / Express-shaped apps + simple domain layering). Teams that
9
+ * want different rules use the contract DSL (`sharkcraft/arch.ts`).
10
+ */
11
+ const DEFAULT_PATTERNS = [
12
+ {
13
+ from: /\.(controller|controllers)\.[tj]sx?$/,
14
+ to: /\.(repository|repositories|repo)\.[tj]sx?$/,
15
+ via: 'service layer',
16
+ message: 'controller imports repository directly — route through a service.',
17
+ },
18
+ {
19
+ from: /\.(controller|controllers)\.[tj]sx?$/,
20
+ to: /\.(entity|entities|model|models)\.[tj]sx?$/,
21
+ via: 'service or DTO layer',
22
+ message: 'controller imports an entity / persistence model — route through a service or DTO.',
23
+ },
24
+ {
25
+ from: /\.(view|page|component)\.[tj]sx?$/,
26
+ to: /\.(repository|repositories|repo)\.[tj]sx?$/,
27
+ via: 'service / hook layer',
28
+ message: 'UI component imports repository directly — go through a service / hook.',
29
+ },
30
+ ];
31
+ /**
32
+ * Detect layer-skip imports: a file in one architectural role importing
33
+ * a file in a role that should be reached only via an intermediate
34
+ * layer.
35
+ *
36
+ * Heuristic only — names like `*.controller.ts` are the signal. Teams
37
+ * with non-conventional names should use the contract DSL instead.
38
+ */
39
+ export function detectAdapterLeaks(api) {
40
+ const out = [];
41
+ for (const file of api.allFiles()) {
42
+ if (!file.path)
43
+ continue;
44
+ const fromPath = file.path.toLowerCase();
45
+ const matchingPatterns = DEFAULT_PATTERNS.filter((p) => p.from.test(fromPath));
46
+ if (matchingPatterns.length === 0)
47
+ continue;
48
+ const neighbours = api.neighbours(file.id);
49
+ if (!neighbours)
50
+ continue;
51
+ for (const o of neighbours.out) {
52
+ if (o.edge.kind !== EdgeKind.ImportsFile)
53
+ continue;
54
+ const target = api.neighbours(o.edge.to)?.node;
55
+ if (!target?.path)
56
+ continue;
57
+ const toPath = target.path.toLowerCase();
58
+ for (const p of matchingPatterns) {
59
+ if (!p.to.test(toPath))
60
+ continue;
61
+ out.push({
62
+ kind: 'public-api-misuse',
63
+ severity: 'warning',
64
+ message: `adapter leak: ${file.path} → ${target.path} — ${p.message}`,
65
+ file: file.path,
66
+ line: o.edge.data?.['line'] ?? undefined,
67
+ targetFile: target.path,
68
+ suggestedFix: `Introduce the ${p.via} between these two files.`,
69
+ refs: [file.id, target.id],
70
+ });
71
+ }
72
+ }
73
+ }
74
+ return out;
75
+ }
@@ -0,0 +1,14 @@
1
+ import { type GraphQueryApi } from '@shrkcrft/graph';
2
+ import type { IArchViolation } from '../schema/violation.js';
3
+ /**
4
+ * Detect risky barrel files (`index.ts` re-exporters):
5
+ *
6
+ * - barrel-fat: > 40 re-exports without grouping commentary.
7
+ * - barrel-cycle: barrel re-exports a module that transitively imports
8
+ * the barrel itself (creates an import cycle through the barrel).
9
+ *
10
+ * Both checks are intentionally low-noise: only barrels named `index.ts`
11
+ * (or `.tsx` / `.js`) are considered.
12
+ */
13
+ export declare function detectBarrelRisks(api: GraphQueryApi): readonly IArchViolation[];
14
+ //# sourceMappingURL=barrel-risks.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"barrel-risks.d.ts","sourceRoot":"","sources":["../../src/checks/barrel-risks.ts"],"names":[],"mappings":"AAAA,OAAO,EAAY,KAAK,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAC/D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAI7D;;;;;;;;;GASG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,aAAa,GAAG,SAAS,cAAc,EAAE,CA+C/E"}
@@ -0,0 +1,60 @@
1
+ import { EdgeKind } from '@shrkcrft/graph';
2
+ const FAT_BARREL_THRESHOLD = 40;
3
+ /**
4
+ * Detect risky barrel files (`index.ts` re-exporters):
5
+ *
6
+ * - barrel-fat: > 40 re-exports without grouping commentary.
7
+ * - barrel-cycle: barrel re-exports a module that transitively imports
8
+ * the barrel itself (creates an import cycle through the barrel).
9
+ *
10
+ * Both checks are intentionally low-noise: only barrels named `index.ts`
11
+ * (or `.tsx` / `.js`) are considered.
12
+ */
13
+ export function detectBarrelRisks(api) {
14
+ const out = [];
15
+ for (const file of api.allFiles()) {
16
+ if (!file.path || !/(?:^|\/)index\.[mc]?[jt]sx?$/.test(file.path))
17
+ continue;
18
+ const data = file.data ?? {};
19
+ const reExportCount = data['reExportCount'] ?? 0;
20
+ const exportCount = data['exportCount'] ?? 0;
21
+ if (reExportCount + exportCount >= FAT_BARREL_THRESHOLD) {
22
+ out.push({
23
+ kind: 'barrel-fat',
24
+ severity: 'warning',
25
+ message: `fat barrel: ${reExportCount + exportCount} (re-)exports in a single index file`,
26
+ file: file.path,
27
+ suggestedFix: 'Split into themed sub-barrels (e.g. `./model`, `./service`) and re-export those.',
28
+ refs: [file.id],
29
+ });
30
+ }
31
+ // Cycle through this barrel: does any file the barrel imports
32
+ // (directly or transitively, capped 1-hop here) also import the
33
+ // barrel back?
34
+ const neighbours = api.neighbours(file.id);
35
+ if (!neighbours)
36
+ continue;
37
+ const importedTargets = neighbours.out
38
+ .filter((o) => o.edge.kind === EdgeKind.ImportsFile)
39
+ .map((o) => o.edge.to);
40
+ for (const tgt of importedTargets) {
41
+ const reverse = api.neighbours(tgt);
42
+ if (!reverse)
43
+ continue;
44
+ const reExportsBarrel = reverse.out.some((o) => o.edge.kind === EdgeKind.ImportsFile && o.edge.to === file.id);
45
+ if (reExportsBarrel) {
46
+ const tNode = api.neighbours(tgt)?.node;
47
+ out.push({
48
+ kind: 'barrel-cycle',
49
+ severity: 'error',
50
+ message: `barrel cycle: ${file.path} re-exports a module that imports the barrel back`,
51
+ file: file.path,
52
+ targetFile: tNode?.path,
53
+ suggestedFix: 'Move the cycle-creating module out of the barrel, or import directly from the leaf file.',
54
+ refs: [file.id, tgt],
55
+ });
56
+ }
57
+ }
58
+ }
59
+ return out;
60
+ }
@@ -0,0 +1,17 @@
1
+ import { type GraphQueryApi } from '@shrkcrft/graph';
2
+ import type { IArchContract } from '../schema/contract.js';
3
+ import type { IArchViolation } from '../schema/violation.js';
4
+ /**
5
+ * Evaluate a project-specific architecture contract against the graph.
6
+ *
7
+ * For each `imports-file` edge whose source file matches a layer:
8
+ * - If the source's rule has `mayNotImport` and the target layer is in
9
+ * that list → violation.
10
+ * - If the source's rule has `mayImport` (a positive whitelist) and
11
+ * the target layer is NOT in that list → violation.
12
+ *
13
+ * Files that match no layer are ignored (the contract says nothing
14
+ * about them). External / unresolved imports are skipped.
15
+ */
16
+ export declare function evaluateContract(api: GraphQueryApi, contract: IArchContract): readonly IArchViolation[];
17
+ //# sourceMappingURL=contract-checks.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"contract-checks.d.ts","sourceRoot":"","sources":["../../src/checks/contract-checks.ts"],"names":[],"mappings":"AACA,OAAO,EAAY,KAAK,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAC/D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAO7D;;;;;;;;;;;GAWG;AACH,wBAAgB,gBAAgB,CAC9B,GAAG,EAAE,aAAa,EAClB,QAAQ,EAAE,aAAa,GACtB,SAAS,cAAc,EAAE,CA6C3B"}
@@ -0,0 +1,80 @@
1
+ import { globToRegex } from '@shrkcrft/boundaries';
2
+ import { EdgeKind } from '@shrkcrft/graph';
3
+ /**
4
+ * Evaluate a project-specific architecture contract against the graph.
5
+ *
6
+ * For each `imports-file` edge whose source file matches a layer:
7
+ * - If the source's rule has `mayNotImport` and the target layer is in
8
+ * that list → violation.
9
+ * - If the source's rule has `mayImport` (a positive whitelist) and
10
+ * the target layer is NOT in that list → violation.
11
+ *
12
+ * Files that match no layer are ignored (the contract says nothing
13
+ * about them). External / unresolved imports are skipped.
14
+ */
15
+ export function evaluateContract(api, contract) {
16
+ const compiled = compile(contract);
17
+ const out = [];
18
+ for (const file of api.allFiles()) {
19
+ if (!file.path)
20
+ continue;
21
+ const srcLayer = matchLayer(file.path, compiled);
22
+ if (!srcLayer)
23
+ continue;
24
+ const rule = contract.rules.find((r) => r.from === srcLayer);
25
+ if (!rule)
26
+ continue;
27
+ const neighbours = api.neighbours(file.id);
28
+ if (!neighbours)
29
+ continue;
30
+ for (const o of neighbours.out) {
31
+ if (o.edge.kind !== EdgeKind.ImportsFile)
32
+ continue;
33
+ const target = api.neighbours(o.edge.to)?.node;
34
+ if (!target?.path)
35
+ continue;
36
+ const tgtLayer = matchLayer(target.path, compiled);
37
+ if (!tgtLayer)
38
+ continue;
39
+ if (rule.mayNotImport?.includes(tgtLayer)) {
40
+ out.push({
41
+ kind: 'contract-import',
42
+ severity: rule.severity ?? 'error',
43
+ message: `contract violation: ${srcLayer} → ${tgtLayer} (forbidden by ${contract.id ?? 'arch-contract'})`,
44
+ file: file.path,
45
+ line: o.edge.data?.['line'] ?? undefined,
46
+ targetFile: target.path,
47
+ suggestedFix: rule.reason ?? `Refactor so the dependency goes the other way (${tgtLayer} → ${srcLayer}).`,
48
+ refs: [file.id, target.id],
49
+ });
50
+ continue;
51
+ }
52
+ if (rule.mayImport && rule.mayImport.length > 0 && !rule.mayImport.includes(tgtLayer)) {
53
+ out.push({
54
+ kind: 'contract-layer-skip',
55
+ severity: rule.severity ?? 'error',
56
+ message: `contract violation: ${srcLayer} may only import {${rule.mayImport.join(', ')}}, found → ${tgtLayer}`,
57
+ file: file.path,
58
+ line: o.edge.data?.['line'] ?? undefined,
59
+ targetFile: target.path,
60
+ suggestedFix: rule.reason,
61
+ refs: [file.id, target.id],
62
+ });
63
+ }
64
+ }
65
+ }
66
+ return out;
67
+ }
68
+ function compile(c) {
69
+ return c.layers.map((l) => ({
70
+ name: l.name,
71
+ matchers: l.includes.map((p) => globToRegex(p)),
72
+ }));
73
+ }
74
+ function matchLayer(path, layers) {
75
+ for (const l of layers) {
76
+ if (l.matchers.some((re) => re.test(path)))
77
+ return l.name;
78
+ }
79
+ return undefined;
80
+ }
@@ -0,0 +1,10 @@
1
+ import { type GraphQueryApi, type IEdge } from '@shrkcrft/graph';
2
+ import type { IArchViolation } from '../schema/violation.js';
3
+ /**
4
+ * Find directed cycles in the `imports-file` graph (Tarjan SCC) and
5
+ * report each as a violation. Severity scales with cycle size: 2-node
6
+ * cycle = warning (often refactor-friendly), 3+ = error.
7
+ */
8
+ export declare function detectCycles(api: GraphQueryApi): readonly IArchViolation[];
9
+ export type { IEdge };
10
+ //# sourceMappingURL=cycle-severity.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cycle-severity.d.ts","sourceRoot":"","sources":["../../src/checks/cycle-severity.ts"],"names":[],"mappings":"AAAA,OAAO,EAAY,KAAK,aAAa,EAAE,KAAK,KAAK,EAAE,MAAM,iBAAiB,CAAC;AAC3E,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAE7D;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,aAAa,GAAG,SAAS,cAAc,EAAE,CAkC1E;AAyCD,YAAY,EAAE,KAAK,EAAE,CAAC"}
@@ -0,0 +1,85 @@
1
+ import { EdgeKind } from '@shrkcrft/graph';
2
+ /**
3
+ * Find directed cycles in the `imports-file` graph (Tarjan SCC) and
4
+ * report each as a violation. Severity scales with cycle size: 2-node
5
+ * cycle = warning (often refactor-friendly), 3+ = error.
6
+ */
7
+ export function detectCycles(api) {
8
+ // Build adjacency from imports-file edges among file nodes only.
9
+ const adj = new Map();
10
+ for (const f of api.allFiles()) {
11
+ const out = api.neighbours(f.id);
12
+ if (!out)
13
+ continue;
14
+ const list = [];
15
+ for (const e of out.out) {
16
+ if (e.edge.kind !== EdgeKind.ImportsFile)
17
+ continue;
18
+ if (e.edge.to.startsWith('file:'))
19
+ list.push(e.edge.to);
20
+ }
21
+ adj.set(f.id, list);
22
+ }
23
+ const sccs = stronglyConnectedComponents(adj);
24
+ const violations = [];
25
+ for (const scc of sccs) {
26
+ if (scc.length < 2)
27
+ continue;
28
+ const refs = [...scc];
29
+ const headId = scc[0];
30
+ const head = api.neighbours(headId)?.node;
31
+ if (!head?.path)
32
+ continue;
33
+ violations.push({
34
+ kind: 'cycle',
35
+ severity: scc.length >= 3 ? 'error' : 'warning',
36
+ message: `import cycle (${scc.length} files): ${scc
37
+ .map((id) => api.neighbours(id)?.node?.path ?? id)
38
+ .join(' → ')}`,
39
+ file: head.path,
40
+ suggestedFix: 'Identify the bidirectional edge and extract the shared types/utilities into a leaf module.',
41
+ refs,
42
+ });
43
+ }
44
+ return violations;
45
+ }
46
+ function stronglyConnectedComponents(adj) {
47
+ let index = 0;
48
+ const stack = [];
49
+ const onStack = new Set();
50
+ const indices = new Map();
51
+ const lowlinks = new Map();
52
+ const result = [];
53
+ const strongconnect = (v) => {
54
+ indices.set(v, index);
55
+ lowlinks.set(v, index);
56
+ index += 1;
57
+ stack.push(v);
58
+ onStack.add(v);
59
+ for (const w of adj.get(v) ?? []) {
60
+ if (!indices.has(w)) {
61
+ strongconnect(w);
62
+ lowlinks.set(v, Math.min(lowlinks.get(v), lowlinks.get(w)));
63
+ }
64
+ else if (onStack.has(w)) {
65
+ lowlinks.set(v, Math.min(lowlinks.get(v), indices.get(w)));
66
+ }
67
+ }
68
+ if (lowlinks.get(v) === indices.get(v)) {
69
+ const scc = [];
70
+ while (true) {
71
+ const w = stack.pop();
72
+ onStack.delete(w);
73
+ scc.push(w);
74
+ if (w === v)
75
+ break;
76
+ }
77
+ result.push(scc);
78
+ }
79
+ };
80
+ for (const v of adj.keys()) {
81
+ if (!indices.has(v))
82
+ strongconnect(v);
83
+ }
84
+ return result;
85
+ }
@@ -0,0 +1,17 @@
1
+ import { type GraphQueryApi, type IEdge } from '@shrkcrft/graph';
2
+ import type { IArchViolation } from '../schema/violation.js';
3
+ /**
4
+ * Detect cross-package imports that reach past the public entrypoint.
5
+ *
6
+ * Heuristic:
7
+ * - Allowed: importing from `<pkg>` (resolved to `<pkg>/src/index.ts`).
8
+ * - Allowed: importing a same-package sibling via relative path.
9
+ * - Violation: cross-package import whose resolved target is NOT an
10
+ * `index.ts` or `*.d.ts` (i.e. it reaches a private internal file).
11
+ *
12
+ * Detected via the `imports-file` edge in the code graph + the file
13
+ * node's owning package (via `belongs-to-package` edges).
14
+ */
15
+ export declare function detectPublicApiMisuse(api: GraphQueryApi): readonly IArchViolation[];
16
+ export type { IEdge };
17
+ //# sourceMappingURL=public-api-misuse.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"public-api-misuse.d.ts","sourceRoot":"","sources":["../../src/checks/public-api-misuse.ts"],"names":[],"mappings":"AAAA,OAAO,EAAY,KAAK,aAAa,EAAE,KAAK,KAAK,EAAc,MAAM,iBAAiB,CAAC;AACvF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAE7D;;;;;;;;;;;GAWG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,aAAa,GAAG,SAAS,cAAc,EAAE,CAsCnF;AAmBD,YAAY,EAAE,KAAK,EAAE,CAAC"}
@@ -0,0 +1,75 @@
1
+ import { EdgeKind } from '@shrkcrft/graph';
2
+ /**
3
+ * Detect cross-package imports that reach past the public entrypoint.
4
+ *
5
+ * Heuristic:
6
+ * - Allowed: importing from `<pkg>` (resolved to `<pkg>/src/index.ts`).
7
+ * - Allowed: importing a same-package sibling via relative path.
8
+ * - Violation: cross-package import whose resolved target is NOT an
9
+ * `index.ts` or `*.d.ts` (i.e. it reaches a private internal file).
10
+ *
11
+ * Detected via the `imports-file` edge in the code graph + the file
12
+ * node's owning package (via `belongs-to-package` edges).
13
+ */
14
+ export function detectPublicApiMisuse(api) {
15
+ const out = [];
16
+ const fileByIdPackage = buildFileToPackageMap(api);
17
+ for (const file of api.allFiles()) {
18
+ const neighbours = api.neighbours(file.id);
19
+ if (!neighbours)
20
+ continue;
21
+ const fromPkg = fileByIdPackage.get(file.id);
22
+ if (!fromPkg)
23
+ continue;
24
+ for (const o of neighbours.out) {
25
+ if (o.edge.kind !== EdgeKind.ImportsFile)
26
+ continue;
27
+ const targetId = o.edge.to;
28
+ if (!targetId.startsWith('file:'))
29
+ continue; // external / unresolved
30
+ const target = api.neighbours(targetId)?.node;
31
+ if (!target?.path)
32
+ continue;
33
+ const toPkg = fileByIdPackage.get(targetId);
34
+ if (!toPkg || toPkg === fromPkg)
35
+ continue;
36
+ // Cross-package import — confirm it lands on a public entry.
37
+ const path = target.path;
38
+ const isPublic = /\/index\.[mc]?[jt]sx?$/.test(path) ||
39
+ path.endsWith('.d.ts') ||
40
+ path.endsWith('.d.cts') ||
41
+ path.endsWith('.d.mts');
42
+ if (isPublic)
43
+ continue;
44
+ out.push({
45
+ kind: 'public-api-misuse',
46
+ severity: 'error',
47
+ message: `cross-package import of private file: ${fromPkg} → ${toPkg} (${target.path})`,
48
+ file: file.path,
49
+ line: o.edge.data?.['line'] ?? undefined,
50
+ targetFile: target.path,
51
+ suggestedFix: `import from the package entry point (${toPkg}) instead of the internal file.`,
52
+ refs: [file.id, target.id],
53
+ });
54
+ }
55
+ }
56
+ return out;
57
+ }
58
+ function buildFileToPackageMap(api) {
59
+ const out = new Map();
60
+ for (const file of api.allFiles()) {
61
+ const neighbours = api.neighbours(file.id);
62
+ if (!neighbours)
63
+ continue;
64
+ for (const o of neighbours.out) {
65
+ if (o.edge.kind !== EdgeKind.BelongsToPackage)
66
+ continue;
67
+ const target = o.target;
68
+ if ('resolved' in target)
69
+ continue;
70
+ out.set(file.id, target.label);
71
+ break;
72
+ }
73
+ }
74
+ return out;
75
+ }
@@ -0,0 +1,27 @@
1
+ import type { IArchContract } from '../schema/contract.js';
2
+ import { type IArchReport } from '../schema/violation.js';
3
+ export interface IRunArchCheckOptions {
4
+ projectRoot: string;
5
+ /** Optional project-specific contract. When absent only the generic checks run. */
6
+ contract?: IArchContract;
7
+ /**
8
+ * Which checks to run. Default: all generic checks; contract runs only
9
+ * when `contract` is provided.
10
+ */
11
+ enable?: Partial<{
12
+ publicApi: boolean;
13
+ barrels: boolean;
14
+ cycles: boolean;
15
+ contract: boolean;
16
+ adapterLeaks: boolean;
17
+ }>;
18
+ }
19
+ /**
20
+ * Run the architecture-guard checks against the on-disk code graph.
21
+ *
22
+ * Returns a structured report. Missing graph → diagnostic + zero
23
+ * violations (never throws on the read path; the CLI surfaces the
24
+ * `nextCommand` hint).
25
+ */
26
+ export declare function runArchCheck(options: IRunArchCheckOptions): IArchReport;
27
+ //# sourceMappingURL=run-arch-check.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"run-arch-check.d.ts","sourceRoot":"","sources":["../../src/checks/run-arch-check.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,EAIL,KAAK,WAAW,EAEjB,MAAM,wBAAwB,CAAC;AAOhC,MAAM,WAAW,oBAAoB;IACnC,WAAW,EAAE,MAAM,CAAC;IACpB,mFAAmF;IACnF,QAAQ,CAAC,EAAE,aAAa,CAAC;IACzB;;;OAGG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;QACf,SAAS,EAAE,OAAO,CAAC;QACnB,OAAO,EAAE,OAAO,CAAC;QACjB,MAAM,EAAE,OAAO,CAAC;QAChB,QAAQ,EAAE,OAAO,CAAC;QAClB,YAAY,EAAE,OAAO,CAAC;KACvB,CAAC,CAAC;CACJ;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,oBAAoB,GAAG,WAAW,CA+CvE"}
@@ -0,0 +1,84 @@
1
+ import { GraphQueryApi, GraphStore } from '@shrkcrft/graph';
2
+ import { ARCH_REPORT_SCHEMA, } from "../schema/violation.js";
3
+ import { detectAdapterLeaks } from "./adapter-leak.js";
4
+ import { detectBarrelRisks } from "./barrel-risks.js";
5
+ import { detectCycles } from "./cycle-severity.js";
6
+ import { detectPublicApiMisuse } from "./public-api-misuse.js";
7
+ import { evaluateContract } from "./contract-checks.js";
8
+ /**
9
+ * Run the architecture-guard checks against the on-disk code graph.
10
+ *
11
+ * Returns a structured report. Missing graph → diagnostic + zero
12
+ * violations (never throws on the read path; the CLI surfaces the
13
+ * `nextCommand` hint).
14
+ */
15
+ export function runArchCheck(options) {
16
+ const diagnostics = [];
17
+ const graphStore = new GraphStore(options.projectRoot);
18
+ if (!graphStore.exists()) {
19
+ diagnostics.push("code-graph store missing — run `shrk graph index`");
20
+ return emptyReport(diagnostics, 0);
21
+ }
22
+ const api = GraphQueryApi.fromStore(options.projectRoot);
23
+ const enable = {
24
+ publicApi: options.enable?.publicApi ?? true,
25
+ barrels: options.enable?.barrels ?? true,
26
+ cycles: options.enable?.cycles ?? true,
27
+ contract: options.enable?.contract ?? !!options.contract,
28
+ adapterLeaks: options.enable?.adapterLeaks ?? true,
29
+ };
30
+ const violations = [];
31
+ if (enable.publicApi)
32
+ violations.push(...detectPublicApiMisuse(api));
33
+ if (enable.barrels)
34
+ violations.push(...detectBarrelRisks(api));
35
+ if (enable.cycles)
36
+ violations.push(...detectCycles(api));
37
+ if (enable.adapterLeaks)
38
+ violations.push(...detectAdapterLeaks(api));
39
+ if (enable.contract && options.contract) {
40
+ violations.push(...evaluateContract(api, options.contract));
41
+ }
42
+ else if (enable.contract && !options.contract) {
43
+ diagnostics.push("contract check enabled but no contract provided — skipping");
44
+ }
45
+ const filesAnalyzed = [...api.allFiles()].length;
46
+ const countsBySeverity = { error: 0, warning: 0, info: 0 };
47
+ const countsByKind = {
48
+ 'public-api-misuse': 0,
49
+ 'barrel-cycle': 0,
50
+ 'barrel-fat': 0,
51
+ cycle: 0,
52
+ 'contract-import': 0,
53
+ 'contract-layer-skip': 0,
54
+ };
55
+ for (const v of violations) {
56
+ countsBySeverity[v.severity] += 1;
57
+ countsByKind[v.kind] += 1;
58
+ }
59
+ return {
60
+ schema: ARCH_REPORT_SCHEMA,
61
+ filesAnalyzed,
62
+ violations,
63
+ countsBySeverity,
64
+ countsByKind,
65
+ diagnostics,
66
+ };
67
+ }
68
+ function emptyReport(diagnostics, filesAnalyzed) {
69
+ return {
70
+ schema: ARCH_REPORT_SCHEMA,
71
+ filesAnalyzed,
72
+ violations: [],
73
+ countsBySeverity: { error: 0, warning: 0, info: 0 },
74
+ countsByKind: {
75
+ 'public-api-misuse': 0,
76
+ 'barrel-cycle': 0,
77
+ 'barrel-fat': 0,
78
+ cycle: 0,
79
+ 'contract-import': 0,
80
+ 'contract-layer-skip': 0,
81
+ },
82
+ diagnostics,
83
+ };
84
+ }
@@ -0,0 +1,8 @@
1
+ import { type IArchContract } from '../schema/contract.js';
2
+ /**
3
+ * Build an architecture contract. Validates the layer references in
4
+ * each rule; throws on undefined layer names so authoring mistakes are
5
+ * caught at load time, not check time.
6
+ */
7
+ export declare function defineArchContract(input: Omit<IArchContract, 'schema'>): IArchContract;
8
+ //# sourceMappingURL=define-contract.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"define-contract.d.ts","sourceRoot":"","sources":["../../src/contract/define-contract.ts"],"names":[],"mappings":"AAAA,OAAO,EAAwB,KAAK,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAEjF;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,IAAI,CAAC,aAAa,EAAE,QAAQ,CAAC,GAAG,aAAa,CActF"}
@@ -0,0 +1,23 @@
1
+ import { ARCH_CONTRACT_SCHEMA } from "../schema/contract.js";
2
+ /**
3
+ * Build an architecture contract. Validates the layer references in
4
+ * each rule; throws on undefined layer names so authoring mistakes are
5
+ * caught at load time, not check time.
6
+ */
7
+ export function defineArchContract(input) {
8
+ const layerNames = new Set(input.layers.map((l) => l.name));
9
+ for (const r of input.rules) {
10
+ if (!layerNames.has(r.from)) {
11
+ throw new Error(`arch-contract: rule.from refers to undefined layer "${r.from}"`);
12
+ }
13
+ for (const l of r.mayImport ?? []) {
14
+ if (!layerNames.has(l))
15
+ throw new Error(`arch-contract: rule.mayImport refers to undefined layer "${l}"`);
16
+ }
17
+ for (const l of r.mayNotImport ?? []) {
18
+ if (!layerNames.has(l))
19
+ throw new Error(`arch-contract: rule.mayNotImport refers to undefined layer "${l}"`);
20
+ }
21
+ }
22
+ return { schema: ARCH_CONTRACT_SCHEMA, ...input };
23
+ }
@@ -0,0 +1,11 @@
1
+ export * from './schema/violation.js';
2
+ export * from './schema/contract.js';
3
+ export * from './checks/public-api-misuse.js';
4
+ export * from './checks/barrel-risks.js';
5
+ export * from './checks/cycle-severity.js';
6
+ export * from './checks/contract-checks.js';
7
+ export * from './checks/adapter-leak.js';
8
+ export * from './contract/define-contract.js';
9
+ export * from './checks/run-arch-check.js';
10
+ export * from './store/arch-report-store.js';
11
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,uBAAuB,CAAC;AACtC,cAAc,sBAAsB,CAAC;AACrC,cAAc,+BAA+B,CAAC;AAC9C,cAAc,0BAA0B,CAAC;AACzC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,0BAA0B,CAAC;AACzC,cAAc,+BAA+B,CAAC;AAC9C,cAAc,4BAA4B,CAAC;AAC3C,cAAc,8BAA8B,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,10 @@
1
+ export * from "./schema/violation.js";
2
+ export * from "./schema/contract.js";
3
+ export * from "./checks/public-api-misuse.js";
4
+ export * from "./checks/barrel-risks.js";
5
+ export * from "./checks/cycle-severity.js";
6
+ export * from "./checks/contract-checks.js";
7
+ export * from "./checks/adapter-leak.js";
8
+ export * from "./contract/define-contract.js";
9
+ export * from "./checks/run-arch-check.js";
10
+ export * from "./store/arch-report-store.js";
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Project-specific architecture contract.
3
+ *
4
+ * Declarative DSL — no executable predicates. Authors call
5
+ * `defineArchContract(...)` (or hand-write the same shape) and pass
6
+ * the result to `runArchCheck({ contract })` or save it as
7
+ * `sharkcraft/arch.ts` for auto-discovery.
8
+ *
9
+ * Schema: sharkcraft.arch-contract/v1.
10
+ */
11
+ export declare const ARCH_CONTRACT_SCHEMA: "sharkcraft.arch-contract/v1";
12
+ export interface IArchLayer {
13
+ name: string;
14
+ /** Glob patterns (matched against project-relative file paths). */
15
+ includes: readonly string[];
16
+ }
17
+ export type ContractSeverity = 'error' | 'warning' | 'info';
18
+ export interface IArchRule {
19
+ /** Source layer name (from `layers`). */
20
+ from: string;
21
+ /** Layers the source MAY import from. */
22
+ mayImport?: readonly string[];
23
+ /** Layers the source MAY NOT import from. */
24
+ mayNotImport?: readonly string[];
25
+ /** Severity when a violation is detected. Default 'error'. */
26
+ severity?: ContractSeverity;
27
+ /** Optional explanation shown in the violation. */
28
+ reason?: string;
29
+ }
30
+ export interface IArchContract {
31
+ schema: typeof ARCH_CONTRACT_SCHEMA;
32
+ /** Optional contract id. */
33
+ id?: string;
34
+ /** Optional human title. */
35
+ title?: string;
36
+ /** Layer declarations. Order is irrelevant. */
37
+ layers: readonly IArchLayer[];
38
+ /** Rules among layers. */
39
+ rules: readonly IArchRule[];
40
+ }
41
+ //# sourceMappingURL=contract.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"contract.d.ts","sourceRoot":"","sources":["../../src/schema/contract.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,eAAO,MAAM,oBAAoB,EAAG,6BAAsC,CAAC;AAE3E,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,mEAAmE;IACnE,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;CAC7B;AAED,MAAM,MAAM,gBAAgB,GAAG,OAAO,GAAG,SAAS,GAAG,MAAM,CAAC;AAE5D,MAAM,WAAW,SAAS;IACxB,yCAAyC;IACzC,IAAI,EAAE,MAAM,CAAC;IACb,yCAAyC;IACzC,SAAS,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC9B,6CAA6C;IAC7C,YAAY,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACjC,8DAA8D;IAC9D,QAAQ,CAAC,EAAE,gBAAgB,CAAC;IAC5B,mDAAmD;IACnD,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,OAAO,oBAAoB,CAAC;IACpC,4BAA4B;IAC5B,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,4BAA4B;IAC5B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,+CAA+C;IAC/C,MAAM,EAAE,SAAS,UAAU,EAAE,CAAC;IAC9B,0BAA0B;IAC1B,KAAK,EAAE,SAAS,SAAS,EAAE,CAAC;CAC7B"}
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Project-specific architecture contract.
3
+ *
4
+ * Declarative DSL — no executable predicates. Authors call
5
+ * `defineArchContract(...)` (or hand-write the same shape) and pass
6
+ * the result to `runArchCheck({ contract })` or save it as
7
+ * `sharkcraft/arch.ts` for auto-discovery.
8
+ *
9
+ * Schema: sharkcraft.arch-contract/v1.
10
+ */
11
+ export const ARCH_CONTRACT_SCHEMA = 'sharkcraft.arch-contract/v1';
@@ -0,0 +1,29 @@
1
+ export declare const ARCH_REPORT_SCHEMA: "sharkcraft.architecture-report/v1";
2
+ export type ArchViolationSeverity = 'error' | 'warning' | 'info';
3
+ export type ArchViolationKind = 'public-api-misuse' | 'barrel-cycle' | 'barrel-fat' | 'cycle' | 'contract-import' | 'contract-layer-skip';
4
+ export interface IArchViolation {
5
+ kind: ArchViolationKind;
6
+ severity: ArchViolationSeverity;
7
+ /** Short headline. */
8
+ message: string;
9
+ /** Project-relative file path the violation originates from. */
10
+ file: string;
11
+ /** Optional line number. */
12
+ line?: number;
13
+ /** Optional file the violation targets (e.g. illegal import target). */
14
+ targetFile?: string;
15
+ /** Optional fix hint shown to the agent / human. */
16
+ suggestedFix?: string;
17
+ /** Graph node ids involved (for renderers). */
18
+ refs?: readonly string[];
19
+ }
20
+ export interface IArchReport {
21
+ schema: typeof ARCH_REPORT_SCHEMA;
22
+ /** Number of files considered by the report. */
23
+ filesAnalyzed: number;
24
+ violations: readonly IArchViolation[];
25
+ countsBySeverity: Readonly<Record<ArchViolationSeverity, number>>;
26
+ countsByKind: Readonly<Record<ArchViolationKind, number>>;
27
+ diagnostics: readonly string[];
28
+ }
29
+ //# sourceMappingURL=violation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"violation.d.ts","sourceRoot":"","sources":["../../src/schema/violation.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,kBAAkB,EAAG,mCAA4C,CAAC;AAE/E,MAAM,MAAM,qBAAqB,GAAG,OAAO,GAAG,SAAS,GAAG,MAAM,CAAC;AAEjE,MAAM,MAAM,iBAAiB,GACzB,mBAAmB,GACnB,cAAc,GACd,YAAY,GACZ,OAAO,GACP,iBAAiB,GACjB,qBAAqB,CAAC;AAE1B,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,iBAAiB,CAAC;IACxB,QAAQ,EAAE,qBAAqB,CAAC;IAChC,sBAAsB;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,gEAAgE;IAChE,IAAI,EAAE,MAAM,CAAC;IACb,4BAA4B;IAC5B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,wEAAwE;IACxE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,oDAAoD;IACpD,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,+CAA+C;IAC/C,IAAI,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC1B;AAED,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,OAAO,kBAAkB,CAAC;IAClC,gDAAgD;IAChD,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,SAAS,cAAc,EAAE,CAAC;IACtC,gBAAgB,EAAE,QAAQ,CAAC,MAAM,CAAC,qBAAqB,EAAE,MAAM,CAAC,CAAC,CAAC;IAClE,YAAY,EAAE,QAAQ,CAAC,MAAM,CAAC,iBAAiB,EAAE,MAAM,CAAC,CAAC,CAAC;IAC1D,WAAW,EAAE,SAAS,MAAM,EAAE,CAAC;CAChC"}
@@ -0,0 +1 @@
1
+ export const ARCH_REPORT_SCHEMA = 'sharkcraft.architecture-report/v1';
@@ -0,0 +1,51 @@
1
+ import type { ArchViolationSeverity, IArchReport, IArchViolation } from '../schema/violation.js';
2
+ export declare const ARCH_SNAPSHOT_SCHEMA: "sharkcraft.architecture-snapshot/v1";
3
+ /**
4
+ * Compact, comparison-friendly snapshot of an `IArchReport`. The full
5
+ * report can grow to hundreds of violations on a large repo — we don't
6
+ * need to persist every field for delta computation. The snapshot
7
+ * keeps counts + a deterministically-hashable violation id list so
8
+ * `shrk doctor` can answer "did anything new appear since the baseline?"
9
+ * without re-running the full check.
10
+ */
11
+ export interface IArchSnapshot {
12
+ schema: typeof ARCH_SNAPSHOT_SCHEMA;
13
+ generatedAt: string;
14
+ filesAnalyzed: number;
15
+ countsBySeverity: Readonly<Record<ArchViolationSeverity, number>>;
16
+ countsByKind: Readonly<Record<string, number>>;
17
+ /**
18
+ * Stable, sorted set of violation ids in the form
19
+ * `<kind>|<file>[:line]|<targetFile?>`. Used for delta computation.
20
+ * A violation appearing in `last` but not in `baseline` counts as
21
+ * "new"; a violation in `baseline` but not in `last` counts as "fixed".
22
+ */
23
+ violationIds: readonly string[];
24
+ }
25
+ export declare function snapshotFromReport(report: IArchReport): IArchSnapshot;
26
+ export declare function violationId(v: IArchViolation): string;
27
+ export declare class ArchReportStore {
28
+ private readonly projectRoot;
29
+ readonly lastPath: string;
30
+ readonly baselinePath: string;
31
+ constructor(projectRoot: string);
32
+ writeLast(report: IArchReport): IArchSnapshot;
33
+ writeBaseline(report: IArchReport): IArchSnapshot;
34
+ readLast(): IArchSnapshot | undefined;
35
+ readBaseline(): IArchSnapshot | undefined;
36
+ clearBaseline(): boolean;
37
+ private write;
38
+ private read;
39
+ }
40
+ export interface IArchSnapshotDelta {
41
+ /** Violation ids present in `last` but not `baseline`. */
42
+ newViolationIds: readonly string[];
43
+ /** Violation ids present in `baseline` but not `last`. */
44
+ fixedViolationIds: readonly string[];
45
+ /** Net change in error count (last − baseline). */
46
+ errorDelta: number;
47
+ /** Net change in warning count (last − baseline). */
48
+ warningDelta: number;
49
+ }
50
+ export declare function diffSnapshots(baseline: IArchSnapshot, last: IArchSnapshot): IArchSnapshotDelta;
51
+ //# sourceMappingURL=arch-report-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"arch-report-store.d.ts","sourceRoot":"","sources":["../../src/store/arch-report-store.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,qBAAqB,EACrB,WAAW,EACX,cAAc,EACf,MAAM,wBAAwB,CAAC;AAEhC,eAAO,MAAM,oBAAoB,EAAG,qCAA8C,CAAC;AAKnF;;;;;;;GAOG;AACH,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,OAAO,oBAAoB,CAAC;IACpC,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,gBAAgB,EAAE,QAAQ,CAAC,MAAM,CAAC,qBAAqB,EAAE,MAAM,CAAC,CAAC,CAAC;IAClE,YAAY,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAC/C;;;;;OAKG;IACH,YAAY,EAAE,SAAS,MAAM,EAAE,CAAC;CACjC;AAED,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,WAAW,GAAG,aAAa,CAWrE;AAED,wBAAgB,WAAW,CAAC,CAAC,EAAE,cAAc,GAAG,MAAM,CAIrD;AAED,qBAAa,eAAe;IAId,OAAO,CAAC,QAAQ,CAAC,WAAW;IAHxC,SAAgB,QAAQ,EAAE,MAAM,CAAC;IACjC,SAAgB,YAAY,EAAE,MAAM,CAAC;gBAER,WAAW,EAAE,MAAM;IAKhD,SAAS,CAAC,MAAM,EAAE,WAAW,GAAG,aAAa;IAM7C,aAAa,CAAC,MAAM,EAAE,WAAW,GAAG,aAAa;IAMjD,QAAQ,IAAI,aAAa,GAAG,SAAS;IAIrC,YAAY,IAAI,aAAa,GAAG,SAAS;IAIzC,aAAa,IAAI,OAAO;IAQxB,OAAO,CAAC,KAAK;IAKb,OAAO,CAAC,IAAI;CAUb;AAED,MAAM,WAAW,kBAAkB;IACjC,0DAA0D;IAC1D,eAAe,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,0DAA0D;IAC1D,iBAAiB,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC,mDAAmD;IACnD,UAAU,EAAE,MAAM,CAAC;IACnB,qDAAqD;IACrD,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,wBAAgB,aAAa,CAC3B,QAAQ,EAAE,aAAa,EACvB,IAAI,EAAE,aAAa,GAClB,kBAAkB,CAWpB"}
@@ -0,0 +1,86 @@
1
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
2
+ import * as nodePath from 'node:path';
3
+ export const ARCH_SNAPSHOT_SCHEMA = 'sharkcraft.architecture-snapshot/v1';
4
+ const DIR = '.sharkcraft/architecture';
5
+ const LAST_FILE = 'last.json';
6
+ const BASELINE_FILE = 'baseline.json';
7
+ export function snapshotFromReport(report) {
8
+ const ids = new Set();
9
+ for (const v of report.violations)
10
+ ids.add(violationId(v));
11
+ return {
12
+ schema: ARCH_SNAPSHOT_SCHEMA,
13
+ generatedAt: new Date().toISOString(),
14
+ filesAnalyzed: report.filesAnalyzed,
15
+ countsBySeverity: { ...report.countsBySeverity },
16
+ countsByKind: { ...report.countsByKind },
17
+ violationIds: [...ids].sort(),
18
+ };
19
+ }
20
+ export function violationId(v) {
21
+ const filePart = v.line ? `${v.file}:${v.line}` : v.file;
22
+ const target = v.targetFile ? `|${v.targetFile}` : '';
23
+ return `${v.kind}|${filePart}${target}`;
24
+ }
25
+ export class ArchReportStore {
26
+ projectRoot;
27
+ lastPath;
28
+ baselinePath;
29
+ constructor(projectRoot) {
30
+ this.projectRoot = projectRoot;
31
+ this.lastPath = nodePath.join(projectRoot, DIR, LAST_FILE);
32
+ this.baselinePath = nodePath.join(projectRoot, DIR, BASELINE_FILE);
33
+ }
34
+ writeLast(report) {
35
+ const snap = snapshotFromReport(report);
36
+ this.write(this.lastPath, snap);
37
+ return snap;
38
+ }
39
+ writeBaseline(report) {
40
+ const snap = snapshotFromReport(report);
41
+ this.write(this.baselinePath, snap);
42
+ return snap;
43
+ }
44
+ readLast() {
45
+ return this.read(this.lastPath);
46
+ }
47
+ readBaseline() {
48
+ return this.read(this.baselinePath);
49
+ }
50
+ clearBaseline() {
51
+ if (existsSync(this.baselinePath)) {
52
+ rmSync(this.baselinePath);
53
+ return true;
54
+ }
55
+ return false;
56
+ }
57
+ write(absPath, snap) {
58
+ mkdirSync(nodePath.dirname(absPath), { recursive: true });
59
+ writeFileSync(absPath, JSON.stringify(snap, null, 2), 'utf8');
60
+ }
61
+ read(absPath) {
62
+ if (!existsSync(absPath))
63
+ return undefined;
64
+ try {
65
+ const raw = JSON.parse(readFileSync(absPath, 'utf8'));
66
+ if (raw.schema !== ARCH_SNAPSHOT_SCHEMA)
67
+ return undefined;
68
+ return raw;
69
+ }
70
+ catch {
71
+ return undefined;
72
+ }
73
+ }
74
+ }
75
+ export function diffSnapshots(baseline, last) {
76
+ const baseSet = new Set(baseline.violationIds);
77
+ const lastSet = new Set(last.violationIds);
78
+ const newViolationIds = last.violationIds.filter((id) => !baseSet.has(id));
79
+ const fixedViolationIds = baseline.violationIds.filter((id) => !lastSet.has(id));
80
+ return {
81
+ newViolationIds,
82
+ fixedViolationIds,
83
+ errorDelta: last.countsBySeverity.error - baseline.countsBySeverity.error,
84
+ warningDelta: last.countsBySeverity.warning - baseline.countsBySeverity.warning,
85
+ };
86
+ }
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@shrkcrft/architecture-guard",
3
+ "version": "0.1.0-alpha.10",
4
+ "description": "SharkCraft architecture guard: semantic architecture checks on top of the code graph — public-API misuse, barrel risks, cycle severity, project-specific architecture contracts.",
5
+ "license": "MIT",
6
+ "author": "SharkCraft contributors",
7
+ "type": "module",
8
+ "main": "./dist/index.js",
9
+ "types": "./dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "import": "./dist/index.js",
14
+ "default": "./dist/index.js"
15
+ }
16
+ },
17
+ "files": [
18
+ "dist",
19
+ "README.md",
20
+ "LICENSE"
21
+ ],
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "git+https://github.com/sharkcraft/sharkcraft.git",
25
+ "directory": "packages/architecture-guard"
26
+ },
27
+ "homepage": "https://github.com/sharkcraft/sharkcraft",
28
+ "bugs": {
29
+ "url": "https://github.com/sharkcraft/sharkcraft/issues"
30
+ },
31
+ "keywords": [
32
+ "sharkcraft",
33
+ "architecture-guard",
34
+ "boundaries",
35
+ "code-intelligence"
36
+ ],
37
+ "engines": {
38
+ "bun": ">=1.1.0",
39
+ "node": ">=18"
40
+ },
41
+ "scripts": {
42
+ "typecheck": "tsc --noEmit -p tsconfig.json"
43
+ },
44
+ "dependencies": {
45
+ "@shrkcrft/core": "^0.1.0-alpha.10",
46
+ "@shrkcrft/boundaries": "^0.1.0-alpha.10",
47
+ "@shrkcrft/graph": "^0.1.0-alpha.10"
48
+ },
49
+ "publishConfig": {
50
+ "access": "public"
51
+ }
52
+ }