@intentset/architecture 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.
Files changed (91) hide show
  1. package/dist/baseline.d.ts +39 -0
  2. package/dist/baseline.d.ts.map +1 -0
  3. package/dist/baseline.js +104 -0
  4. package/dist/baseline.js.map +1 -0
  5. package/dist/boundaries.d.ts +63 -0
  6. package/dist/boundaries.d.ts.map +1 -0
  7. package/dist/boundaries.js +436 -0
  8. package/dist/boundaries.js.map +1 -0
  9. package/dist/check.d.ts +73 -0
  10. package/dist/check.d.ts.map +1 -0
  11. package/dist/check.js +130 -0
  12. package/dist/check.js.map +1 -0
  13. package/dist/claims.d.ts +24 -0
  14. package/dist/claims.d.ts.map +1 -0
  15. package/dist/claims.js +201 -0
  16. package/dist/claims.js.map +1 -0
  17. package/dist/config.d.ts +53 -0
  18. package/dist/config.d.ts.map +1 -0
  19. package/dist/config.js +159 -0
  20. package/dist/config.js.map +1 -0
  21. package/dist/dependencies.d.ts +30 -0
  22. package/dist/dependencies.d.ts.map +1 -0
  23. package/dist/dependencies.js +179 -0
  24. package/dist/dependencies.js.map +1 -0
  25. package/dist/exceptions.d.ts +43 -0
  26. package/dist/exceptions.d.ts.map +1 -0
  27. package/dist/exceptions.js +233 -0
  28. package/dist/exceptions.js.map +1 -0
  29. package/dist/extract.d.ts +24 -0
  30. package/dist/extract.d.ts.map +1 -0
  31. package/dist/extract.js +316 -0
  32. package/dist/extract.js.map +1 -0
  33. package/dist/finding.d.ts +39 -0
  34. package/dist/finding.d.ts.map +1 -0
  35. package/dist/finding.js +29 -0
  36. package/dist/finding.js.map +1 -0
  37. package/dist/imports.d.ts +30 -0
  38. package/dist/imports.d.ts.map +1 -0
  39. package/dist/imports.js +48 -0
  40. package/dist/imports.js.map +1 -0
  41. package/dist/index.d.ts +23 -0
  42. package/dist/index.d.ts.map +1 -0
  43. package/dist/index.js +22 -0
  44. package/dist/index.js.map +1 -0
  45. package/dist/layers.d.ts +38 -0
  46. package/dist/layers.d.ts.map +1 -0
  47. package/dist/layers.js +41 -0
  48. package/dist/layers.js.map +1 -0
  49. package/dist/ownership.d.ts +25 -0
  50. package/dist/ownership.d.ts.map +1 -0
  51. package/dist/ownership.js +97 -0
  52. package/dist/ownership.js.map +1 -0
  53. package/dist/paths.d.ts +18 -0
  54. package/dist/paths.d.ts.map +1 -0
  55. package/dist/paths.js +54 -0
  56. package/dist/paths.js.map +1 -0
  57. package/dist/patterns.d.ts +25 -0
  58. package/dist/patterns.d.ts.map +1 -0
  59. package/dist/patterns.js +101 -0
  60. package/dist/patterns.js.map +1 -0
  61. package/dist/regions.d.ts +85 -0
  62. package/dist/regions.d.ts.map +1 -0
  63. package/dist/regions.js +144 -0
  64. package/dist/regions.js.map +1 -0
  65. package/dist/resolve.d.ts +53 -0
  66. package/dist/resolve.d.ts.map +1 -0
  67. package/dist/resolve.js +268 -0
  68. package/dist/resolve.js.map +1 -0
  69. package/dist/tsconfig.d.ts +42 -0
  70. package/dist/tsconfig.d.ts.map +1 -0
  71. package/dist/tsconfig.js +186 -0
  72. package/dist/tsconfig.js.map +1 -0
  73. package/package.json +36 -0
  74. package/src/baseline.ts +126 -0
  75. package/src/boundaries.ts +499 -0
  76. package/src/check.ts +197 -0
  77. package/src/claims.ts +229 -0
  78. package/src/config.ts +212 -0
  79. package/src/dependencies.ts +211 -0
  80. package/src/exceptions.ts +269 -0
  81. package/src/extract.ts +344 -0
  82. package/src/finding.ts +64 -0
  83. package/src/imports.ts +77 -0
  84. package/src/index.ts +58 -0
  85. package/src/layers.ts +67 -0
  86. package/src/ownership.ts +135 -0
  87. package/src/paths.ts +53 -0
  88. package/src/patterns.ts +95 -0
  89. package/src/regions.ts +243 -0
  90. package/src/resolve.ts +283 -0
  91. package/src/tsconfig.ts +198 -0
@@ -0,0 +1,39 @@
1
+ /**
2
+ * A baseline of known violations for adoption by declared scope (VSA §9):
3
+ * baseline the existing violations, block new ones, retire the baseline
4
+ * progressively. In migration mode a diagnostic the baseline knows becomes a
5
+ * warning marked "(baselined)" and is counted apart; in strict mode the
6
+ * baseline is ignored, so the two modes are always distinguishable.
7
+ *
8
+ * An entry fingerprints the code, path, artifact, field and message, and the
9
+ * messages carry no line numbers, so moving an import does not unbaseline it
10
+ * while a new file, a new target or a larger cycle is a new fingerprint and
11
+ * stays an error. Matching is one entry per diagnostic: a baseline of one
12
+ * violation does not absorb a second copy of it.
13
+ */
14
+ import { type Diagnostic } from "@intentset/core";
15
+ export type Mode = "migration" | "strict";
16
+ export interface BaselineEntry {
17
+ code: string;
18
+ path: string | null;
19
+ artifact?: string;
20
+ fingerprint: string;
21
+ }
22
+ /** The message without the notes exceptions and baselines add. */
23
+ export declare function plainMessage(message: string): string;
24
+ export declare function fingerprint(diagnostic: Diagnostic): string;
25
+ /** Record every error, and every diagnostic already baselined, as the baseline to adopt. Sorted. */
26
+ export declare function writeBaseline(diagnostics: readonly Diagnostic[]): BaselineEntry[];
27
+ /** Parse a baseline file. Throws on a shape that is not an array of entries, since a misread baseline must not pass silently. */
28
+ export declare function parseBaseline(text: string): BaselineEntry[];
29
+ /**
30
+ * Apply a baseline. In migration mode each entry turns one matching error
31
+ * into a "(baselined)" warning; entries nothing matched are returned as
32
+ * stale. Strict mode returns the diagnostics unchanged.
33
+ */
34
+ export declare function applyBaseline(diagnostics: readonly Diagnostic[], baseline: readonly BaselineEntry[], mode?: Mode): {
35
+ diagnostics: Diagnostic[];
36
+ baselined: number;
37
+ stale: BaselineEntry[];
38
+ };
39
+ //# sourceMappingURL=baseline.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"baseline.d.ts","sourceRoot":"","sources":["../src/baseline.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,EAAiB,KAAK,UAAU,EAAa,MAAM,iBAAiB,CAAC;AAI5E,MAAM,MAAM,IAAI,GAAG,WAAW,GAAG,QAAQ,CAAC;AAE1C,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;CACrB;AAID,kEAAkE;AAClE,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAOpD;AAED,wBAAgB,WAAW,CAAC,UAAU,EAAE,UAAU,GAAG,MAAM,CAU1D;AAMD,oGAAoG;AACpG,wBAAgB,aAAa,CAAC,WAAW,EAAE,SAAS,UAAU,EAAE,GAAG,aAAa,EAAE,CAejF;AAED,iIAAiI;AACjI,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,aAAa,EAAE,CAsB3D;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAC3B,WAAW,EAAE,SAAS,UAAU,EAAE,EAClC,QAAQ,EAAE,SAAS,aAAa,EAAE,EAClC,IAAI,GAAE,IAAkB,GACvB;IAAE,WAAW,EAAE,UAAU,EAAE,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,aAAa,EAAE,CAAA;CAAE,CAmB1E"}
@@ -0,0 +1,104 @@
1
+ /**
2
+ * A baseline of known violations for adoption by declared scope (VSA §9):
3
+ * baseline the existing violations, block new ones, retire the baseline
4
+ * progressively. In migration mode a diagnostic the baseline knows becomes a
5
+ * warning marked "(baselined)" and is counted apart; in strict mode the
6
+ * baseline is ignored, so the two modes are always distinguishable.
7
+ *
8
+ * An entry fingerprints the code, path, artifact, field and message, and the
9
+ * messages carry no line numbers, so moving an import does not unbaseline it
10
+ * while a new file, a new target or a larger cycle is a new fingerprint and
11
+ * stays an error. Matching is one entry per diagnostic: a baseline of one
12
+ * violation does not absorb a second copy of it.
13
+ */
14
+ import { canonicalJson, sha256Hex } from "@intentset/core";
15
+ import { annotate } from "./exceptions.js";
16
+ import { compareStrings } from "./patterns.js";
17
+ const NOTES = / \((?:baselined|excepted by [^()]*|exception [^()]* expired [^()]*)\)(?=\.?$)/;
18
+ /** The message without the notes exceptions and baselines add. */
19
+ export function plainMessage(message) {
20
+ let out = message;
21
+ for (let previous = ""; previous !== out;) {
22
+ previous = out;
23
+ out = out.replace(NOTES, "");
24
+ }
25
+ return out;
26
+ }
27
+ export function fingerprint(diagnostic) {
28
+ return sha256Hex(canonicalJson({
29
+ code: diagnostic.code,
30
+ path: diagnostic.path,
31
+ artifact: diagnostic.artifact,
32
+ field: diagnostic.field ?? null,
33
+ message: plainMessage(diagnostic.message),
34
+ }));
35
+ }
36
+ function isBaselined(diagnostic) {
37
+ return diagnostic.severity === "warning" && / \(baselined\)\.?$/.test(diagnostic.message);
38
+ }
39
+ /** Record every error, and every diagnostic already baselined, as the baseline to adopt. Sorted. */
40
+ export function writeBaseline(diagnostics) {
41
+ return diagnostics
42
+ .filter((diagnostic) => diagnostic.severity === "error" || isBaselined(diagnostic))
43
+ .map((diagnostic) => ({
44
+ code: diagnostic.code,
45
+ path: diagnostic.path,
46
+ ...(diagnostic.artifact !== null ? { artifact: diagnostic.artifact } : {}),
47
+ fingerprint: fingerprint(diagnostic),
48
+ }))
49
+ .sort((a, b) => compareStrings(a.code, b.code) ||
50
+ compareStrings(a.path ?? "", b.path ?? "") ||
51
+ compareStrings(a.fingerprint, b.fingerprint));
52
+ }
53
+ /** Parse a baseline file. Throws on a shape that is not an array of entries, since a misread baseline must not pass silently. */
54
+ export function parseBaseline(text) {
55
+ const value = JSON.parse(text);
56
+ if (!Array.isArray(value))
57
+ throw new Error("a baseline is a JSON array of entries");
58
+ return value.map((item, i) => {
59
+ const entry = item;
60
+ if (typeof entry !== "object" ||
61
+ entry === null ||
62
+ typeof entry.code !== "string" ||
63
+ typeof entry.fingerprint !== "string" ||
64
+ !(typeof entry.path === "string" || entry.path === null) ||
65
+ !(entry.artifact === undefined || typeof entry.artifact === "string")) {
66
+ throw new Error(`baseline entry ${i} is not { code, path, artifact?, fingerprint }`);
67
+ }
68
+ return {
69
+ code: entry.code,
70
+ path: entry.path,
71
+ ...(typeof entry.artifact === "string" ? { artifact: entry.artifact } : {}),
72
+ fingerprint: entry.fingerprint,
73
+ };
74
+ });
75
+ }
76
+ /**
77
+ * Apply a baseline. In migration mode each entry turns one matching error
78
+ * into a "(baselined)" warning; entries nothing matched are returned as
79
+ * stale. Strict mode returns the diagnostics unchanged.
80
+ */
81
+ export function applyBaseline(diagnostics, baseline, mode = "migration") {
82
+ if (mode === "strict")
83
+ return { diagnostics: [...diagnostics], baselined: 0, stale: [] };
84
+ const remaining = new Map();
85
+ for (const entry of baseline) {
86
+ const list = remaining.get(entry.fingerprint) ?? [];
87
+ list.push(entry);
88
+ remaining.set(entry.fingerprint, list);
89
+ }
90
+ let baselined = 0;
91
+ const out = diagnostics.map((diagnostic) => {
92
+ if (diagnostic.severity !== "error")
93
+ return diagnostic;
94
+ const list = remaining.get(fingerprint(diagnostic));
95
+ if (list === undefined || list.length === 0)
96
+ return diagnostic;
97
+ list.pop();
98
+ baselined++;
99
+ return { ...diagnostic, severity: "warning", message: annotate(diagnostic.message, "baselined") };
100
+ });
101
+ const stale = [...remaining.values()].flat().sort((a, b) => compareStrings(a.fingerprint, b.fingerprint));
102
+ return { diagnostics: out, baselined, stale };
103
+ }
104
+ //# sourceMappingURL=baseline.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"baseline.js","sourceRoot":"","sources":["../src/baseline.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,63 @@
1
+ /**
2
+ * The rules one import edge can break (VSA003, VSA006, region boundaries;
3
+ * profile TS001–TS003, TS006, AMP001, AMP002, AMP006). Each edge gets at
4
+ * most one boundary finding, the most specific that applies, in this order:
5
+ *
6
+ * 1. AMP001 frontend code importing anything under a backend root, unless
7
+ * a client-seam file imports a named response-parser module.
8
+ * 2. REG001 region boundary: shared importing infrastructure, composition
9
+ * or a slice; infrastructure importing composition or a slice;
10
+ * a slice importing composition (VSA §5). VSA007 is a review
11
+ * assertion about business ownership, so a structural region
12
+ * violation has its own code.
13
+ * 3. TS003 a screen of another slice imported by anything but a named
14
+ * router file; or one of a slice's entrypoints importing its own
15
+ * screen.
16
+ * 4. VSA003 another slice's file that is none of its entrypoints, by
17
+ * relative path, alias or re-export; with TS006 beside it when a
18
+ * wildcard alias opened the path. Every entrypoint is a public
19
+ * surface, and the message names the one in the importer's own
20
+ * package first, since that is the surface it should use.
21
+ * 5. AMP006 a policy or model file importing its slice's client seam or
22
+ * infrastructure, or a backend SDK.
23
+ * 6. VSA006 an edge inside one slice that the layer matrix does not allow.
24
+ *
25
+ * Beside those: TS002 (warning) when one slice reaches another's entrypoint
26
+ * by relative path although an alias for it is configured; AMP002 when a
27
+ * slice file outside its client seam imports a backend SDK; TS001 (warning)
28
+ * when an entrypoint re-exports with `export *`.
29
+ *
30
+ * Test files get the separate pass profile §3 asks for: they may import
31
+ * their own slice's internals, and VSA003 and TS003 still apply to anyone
32
+ * else's.
33
+ *
34
+ * Edges from files inside the declared scope are checked, including those to
35
+ * a file outside it that a slice claims. Edges from files outside the scope
36
+ * are checked for one thing only: reaching past a slice's public surfaces to
37
+ * a file inside the scope, which is VSA003 (TS003 for a screen) as a warning,
38
+ * so a narrow scope cannot hide its consumers and they do not fail it
39
+ * (VSA §9). They count toward no edge total and no slice dependency.
40
+ */
41
+ import { type Finding } from "./finding.ts";
42
+ import type { ImportEdge } from "./imports.ts";
43
+ import { type Model } from "./regions.ts";
44
+ import type { Resolver } from "./resolve.ts";
45
+ export interface SliceEdge {
46
+ from: string;
47
+ to: string;
48
+ /** The import edges behind it, in file and line order. */
49
+ imports: ImportEdge[];
50
+ }
51
+ export interface BoundaryResult {
52
+ findings: Finding[];
53
+ /** Observed slice-to-slice dependencies from production files in scope, keyed "FROM->TO". */
54
+ sliceEdges: Map<string, SliceEdge>;
55
+ /** Resolved file-to-file edges from production files. */
56
+ fileEdges: number;
57
+ /** Those between two different slices. */
58
+ crossSliceEdges: number;
59
+ }
60
+ export declare function checkBoundaries(model: Model, edges: readonly ImportEdge[], resolver: Resolver): BoundaryResult;
61
+ /** One warning per slice whose code files are in no declared layer, or in a layer the matrix does not know (VSA006). */
62
+ export declare function checkUnclassified(model: Model): Finding[];
63
+ //# sourceMappingURL=boundaries.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"boundaries.d.ts","sourceRoot":"","sources":["../src/boundaries.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AACH,OAAO,EAAE,KAAK,OAAO,EAAqB,MAAM,cAAc,CAAC;AAC/D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAG/C,OAAO,EAAmB,KAAK,KAAK,EAA+B,MAAM,cAAc,CAAC;AACxF,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAE7C,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,0DAA0D;IAC1D,OAAO,EAAE,UAAU,EAAE,CAAC;CACvB;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB,6FAA6F;IAC7F,UAAU,EAAE,GAAG,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;IACnC,yDAAyD;IACzD,SAAS,EAAE,MAAM,CAAC;IAClB,0CAA0C;IAC1C,eAAe,EAAE,MAAM,CAAC;CACzB;AAyDD,wBAAgB,eAAe,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,SAAS,UAAU,EAAE,EAAE,QAAQ,EAAE,QAAQ,GAAG,cAAc,CAqV9G;AAED,wHAAwH;AACxH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,KAAK,GAAG,OAAO,EAAE,CAmCzD"}
@@ -0,0 +1,436 @@
1
+ /**
2
+ * The rules one import edge can break (VSA003, VSA006, region boundaries;
3
+ * profile TS001–TS003, TS006, AMP001, AMP002, AMP006). Each edge gets at
4
+ * most one boundary finding, the most specific that applies, in this order:
5
+ *
6
+ * 1. AMP001 frontend code importing anything under a backend root, unless
7
+ * a client-seam file imports a named response-parser module.
8
+ * 2. REG001 region boundary: shared importing infrastructure, composition
9
+ * or a slice; infrastructure importing composition or a slice;
10
+ * a slice importing composition (VSA §5). VSA007 is a review
11
+ * assertion about business ownership, so a structural region
12
+ * violation has its own code.
13
+ * 3. TS003 a screen of another slice imported by anything but a named
14
+ * router file; or one of a slice's entrypoints importing its own
15
+ * screen.
16
+ * 4. VSA003 another slice's file that is none of its entrypoints, by
17
+ * relative path, alias or re-export; with TS006 beside it when a
18
+ * wildcard alias opened the path. Every entrypoint is a public
19
+ * surface, and the message names the one in the importer's own
20
+ * package first, since that is the surface it should use.
21
+ * 5. AMP006 a policy or model file importing its slice's client seam or
22
+ * infrastructure, or a backend SDK.
23
+ * 6. VSA006 an edge inside one slice that the layer matrix does not allow.
24
+ *
25
+ * Beside those: TS002 (warning) when one slice reaches another's entrypoint
26
+ * by relative path although an alias for it is configured; AMP002 when a
27
+ * slice file outside its client seam imports a backend SDK; TS001 (warning)
28
+ * when an entrypoint re-exports with `export *`.
29
+ *
30
+ * Test files get the separate pass profile §3 asks for: they may import
31
+ * their own slice's internals, and VSA003 and TS003 still apply to anyone
32
+ * else's.
33
+ *
34
+ * Edges from files inside the declared scope are checked, including those to
35
+ * a file outside it that a slice claims. Edges from files outside the scope
36
+ * are checked for one thing only: reaching past a slice's public surfaces to
37
+ * a file inside the scope, which is VSA003 (TS003 for a screen) as a warning,
38
+ * so a narrow scope cannot hide its consumers and they do not fail it
39
+ * (VSA §9). They count toward no edge total and no slice dependency.
40
+ */
41
+ import { finding, listSome } from "./finding.js";
42
+ import { inSeam, isPure, knownLayer, layerAllows, layerOf } from "./layers.js";
43
+ import { matchAny, matchPattern } from "./patterns.js";
44
+ import { describePackage } from "./regions.js";
45
+ function how(edge) {
46
+ const kind = edge.reexport
47
+ ? "re-exports"
48
+ : edge.dynamic
49
+ ? "dynamically imports"
50
+ : edge.typeOnly
51
+ ? "type-imports"
52
+ : "imports";
53
+ return `${kind} "${edge.specifier}"`;
54
+ }
55
+ function through(edge) {
56
+ return edge.via === "relative"
57
+ ? "a relative path"
58
+ : edge.via === "workspace"
59
+ ? "a package path"
60
+ : `the ${edge.via === "baseUrl" ? "baseUrl" : "alias"} "${edge.specifier}"`;
61
+ }
62
+ /** A slice's entrypoints with those in the importer's own package first, since that is the surface it should use. */
63
+ function surfacesFor(model, target, from) {
64
+ const pkg = model.packageOf(from);
65
+ const own = target.meta.entrypoints.filter((entry) => model.packageOf(entry) === pkg);
66
+ return { own, others: target.meta.entrypoints.filter((entry) => !own.includes(entry)) };
67
+ }
68
+ /** "its public surface is X", or with several, the one in the importer's package first. */
69
+ function describeSurfaces(model, target, from) {
70
+ const entries = target.meta.entrypoints;
71
+ if (entries.length === 1)
72
+ return `its public surface is ${entries[0]}`;
73
+ const { own, others } = surfacesFor(model, target, from);
74
+ if (own.length === 0)
75
+ return `its public surfaces are ${listSome(others, others.length)}`;
76
+ const elsewhere = others.length === 0 ? "" : `, and elsewhere ${listSome(others, others.length)}`;
77
+ return `its public surface in ${describePackage(model.packageOf(from))} is ${listSome(own, own.length)}${elsewhere}`;
78
+ }
79
+ /** The file to import instead: the entrypoint in the importer's package, or the only one. */
80
+ function importInstead(model, target, from) {
81
+ const { own } = surfacesFor(model, target, from);
82
+ if (own.length > 0)
83
+ return `Import ${own[0]}`;
84
+ const entries = target.meta.entrypoints;
85
+ return entries.length === 1 ? `Import ${entries[0]}` : `Import one of ${target.id}'s entrypoints`;
86
+ }
87
+ function describeRegion(region) {
88
+ switch (region.kind) {
89
+ case "slice":
90
+ return `slice ${region.owner}`;
91
+ case "resource":
92
+ return `resource ${region.owner}`;
93
+ default:
94
+ return region.kind;
95
+ }
96
+ }
97
+ export function checkBoundaries(model, edges, resolver) {
98
+ const { config } = model;
99
+ const findings = [];
100
+ const sliceEdges = new Map();
101
+ let fileEdges = 0;
102
+ let crossSliceEdges = 0;
103
+ const sliceOf = (region) => region.kind === "slice" ? model.sliceById.get(region.owner) : undefined;
104
+ const isScreen = (path) => matchPattern(config.screensGlob, path);
105
+ // VSA §9: from outside the declared scope, only an import past a slice's public surfaces into the scope is
106
+ // reported, and as a warning. The exemptions are those that hold inside the scope.
107
+ const outOfScope = (edge) => {
108
+ const { from, to } = edge;
109
+ if (to === null || !model.inScope(to))
110
+ return null;
111
+ const target = sliceOf(model.regionOf(to));
112
+ if (target === undefined || target.meta.entrypoints.includes(to))
113
+ return null;
114
+ const test = model.isTest(from);
115
+ const source = test ? undefined : sliceOf(model.regionOf(from));
116
+ const owner = test ? model.testOwner(from) : (source?.id ?? null);
117
+ if (owner === target.id)
118
+ return null;
119
+ if (!test && model.underBackend(to)) {
120
+ if (source === undefined && model.underBackend(from))
121
+ return null; // the unified backend composition (VSA §7)
122
+ if (source !== undefined && inSeam(source, from) && matchAny(config.responseParserModules, to))
123
+ return null;
124
+ }
125
+ const who = test ? `Test file ${from}` : from;
126
+ const subject = { edges: [{ from, to }, ...(owner !== null ? [{ from: owner, to: target.id }] : [])] };
127
+ const why = "It is a warning because the importer lies outside the declared scope (VSA §9).";
128
+ if (isScreen(to)) {
129
+ if (!test && matchAny(config.routerFiles, from))
130
+ return null;
131
+ return finding({
132
+ code: "TS003",
133
+ severity: "warning",
134
+ artifact: owner,
135
+ path: from,
136
+ location: { line: edge.line },
137
+ message: `${who} lies outside the declared scope and ${how(edge)}, a screen of ${target.id}; only a named router file may import screens directly.`,
138
+ remediation: `Let the router compose ${target.id}'s screen, or consume a component API that ${target.id} exposes through an entrypoint (TS003). ${why}`,
139
+ ...subject,
140
+ });
141
+ }
142
+ return finding({
143
+ code: "VSA003",
144
+ severity: "warning",
145
+ artifact: owner,
146
+ path: from,
147
+ location: { line: edge.line },
148
+ message: `${who} lies outside the declared scope and ${how(edge)}, a private file of ${target.id} reached through ${through(edge)}; ${describeSurfaces(model, target, from)}.`,
149
+ remediation: `${importInstead(model, target, from)} and have it export what is needed (VSA003). ${why}`,
150
+ ...subject,
151
+ });
152
+ };
153
+ for (const edge of edges) {
154
+ const from = edge.from;
155
+ if (!model.inScope(from)) {
156
+ const leak = outOfScope(edge);
157
+ if (leak !== null)
158
+ findings.push(leak);
159
+ continue;
160
+ }
161
+ const location = { line: edge.line };
162
+ if (model.isTest(from)) {
163
+ if (edge.to === null)
164
+ continue;
165
+ const owner = model.testOwner(from);
166
+ const target = sliceOf(model.regionOf(edge.to));
167
+ if (target === undefined || target.id === owner)
168
+ continue;
169
+ if (isScreen(edge.to)) {
170
+ findings.push(finding({
171
+ code: "TS003",
172
+ artifact: owner,
173
+ path: from,
174
+ location,
175
+ message: `Test file ${from} ${how(edge)}, a screen of ${target.id}, and tests must not bypass another slice's contract.`,
176
+ remediation: `Test ${target.id}'s screens from ${target.id}'s own tests, or drive them through the router (profile §3).`,
177
+ edges: [{ from, to: edge.to }],
178
+ }));
179
+ }
180
+ else if (!target.meta.entrypoints.includes(edge.to)) {
181
+ findings.push(finding({
182
+ code: "VSA003",
183
+ artifact: owner,
184
+ path: from,
185
+ location,
186
+ message: `Test file ${from} ${how(edge)}, a private file of ${target.id}; ${describeSurfaces(model, target, from)}.`,
187
+ remediation: `${importInstead(model, target, from)} instead; tests may reach only their own slice's internals (profile §3).`,
188
+ edges: [{ from, to: edge.to }],
189
+ }));
190
+ }
191
+ continue;
192
+ }
193
+ const regionFrom = model.regionOf(from);
194
+ const source = sliceOf(regionFrom);
195
+ const artifact = source?.id ?? null;
196
+ if (source?.meta.entrypoints.includes(from) && edge.wildcardExport) {
197
+ findings.push(finding({
198
+ code: "TS001",
199
+ severity: "warning",
200
+ artifact,
201
+ path: from,
202
+ location,
203
+ message: `The entrypoint of ${source.id} re-exports everything from "${edge.specifier}" with a wildcard, which can publish private modules as contract.`,
204
+ remediation: "Export the reviewed names explicitly (VSA §4, TS001).",
205
+ edges: [{ from, to: edge.to ?? edge.specifier }],
206
+ }));
207
+ }
208
+ if (edge.to === null) {
209
+ if (source !== undefined &&
210
+ !model.underBackend(from) &&
211
+ matchAny(config.backendSdks, edge.specifier.replace(/^node:/, ""))) {
212
+ if (isPure(source, from)) {
213
+ findings.push(finding({
214
+ code: "AMP006",
215
+ artifact,
216
+ path: from,
217
+ location,
218
+ message: `${from} is a policy or model file of ${source.id} and ${how(edge)}, a backend SDK.`,
219
+ remediation: "Keep SDK and generated client types out of policies and models; adapt them in the client seam (AMP006).",
220
+ edges: [{ from, to: edge.specifier }],
221
+ }));
222
+ }
223
+ else if (!inSeam(source, from)) {
224
+ findings.push(finding({
225
+ code: "AMP002",
226
+ artifact,
227
+ path: from,
228
+ location,
229
+ message: `${from} ${how(edge)}, a backend SDK, outside the client seam of ${source.id}.`,
230
+ remediation: `Move the backend call into ${source.id}'s client seam and call that from here (AMP002, VSA010).`,
231
+ edges: [{ from, to: edge.specifier }],
232
+ }));
233
+ }
234
+ }
235
+ continue;
236
+ }
237
+ const to = edge.to;
238
+ const regionTo = model.regionOf(to);
239
+ const target = sliceOf(regionTo);
240
+ fileEdges++;
241
+ if (source !== undefined && target !== undefined && source.id !== target.id) {
242
+ crossSliceEdges++;
243
+ const key = `${source.id}->${target.id}`;
244
+ const entry = sliceEdges.get(key) ?? { from: source.id, to: target.id, imports: [] };
245
+ entry.imports.push(edge);
246
+ sliceEdges.set(key, entry);
247
+ }
248
+ if (!model.inScope(to) && target === undefined)
249
+ continue;
250
+ const subject = { edges: [{ from, to }, ...(source && target ? [{ from: source.id, to: target.id }] : [])] };
251
+ // 1. AMP001
252
+ if (model.underBackend(to) && !model.underBackend(from)) {
253
+ const seam = source !== undefined && inSeam(source, from);
254
+ if (!(seam && matchAny(config.responseParserModules, to))) {
255
+ findings.push(finding({
256
+ code: "AMP001",
257
+ artifact,
258
+ path: from,
259
+ location,
260
+ message: `${from} ${how(edge)}, the backend file ${to}${edge.typeOnly ? " (type-only imports count)" : ""}, and frontend code must not import backend definitions.`,
261
+ remediation: seam
262
+ ? "Only the named response-parser modules may be imported by a client seam; adapt everything else to the slice's own types (profile §4)."
263
+ : "Reach the backend through the slice's client seam with its own transport types, or record an approved type-bridge ADR as an exception (profile §4).",
264
+ ...subject,
265
+ }));
266
+ }
267
+ continue;
268
+ }
269
+ // 2. Region boundaries.
270
+ const regionViolation = (regionFrom.kind === "shared" && ["infrastructure", "composition", "slice"].includes(regionTo.kind)) ||
271
+ (regionFrom.kind === "infrastructure" && ["composition", "slice"].includes(regionTo.kind)) ||
272
+ (regionFrom.kind === "slice" && regionTo.kind === "composition");
273
+ if (regionViolation) {
274
+ findings.push(finding({
275
+ code: "REG001",
276
+ artifact,
277
+ path: from,
278
+ location,
279
+ message: `Region boundary: ${describeRegion(regionFrom)} file ${from} ${how(edge)}, which is ${describeRegion(regionTo)}.`,
280
+ remediation: regionFrom.kind === "shared"
281
+ ? "Shared code depends only on shared neutral abstractions; invert the dependency so infrastructure imports shared (VSA §5)."
282
+ : regionFrom.kind === "infrastructure"
283
+ ? "Infrastructure provides mechanisms and must not know slices or composition; have composition wire them together (VSA §5)."
284
+ : "Composition depends on slices, never the reverse; pass what the slice needs in through its contract (VSA §5, §6).",
285
+ ...subject,
286
+ }));
287
+ continue;
288
+ }
289
+ // A unified backend root is compatible with VSA (§7): the backend composition (amplify/backend.ts, a recorded
290
+ // resource) wires every slice's resource definitions. Between two slices' backend files the rules below still hold.
291
+ if (source === undefined && model.underBackend(from) && model.underBackend(to))
292
+ continue;
293
+ // 3 and 4. Another slice's screens and private files.
294
+ if (target !== undefined && target.id !== source?.id) {
295
+ if (isScreen(to)) {
296
+ if (!matchAny(config.routerFiles, from)) {
297
+ findings.push(finding({
298
+ code: "TS003",
299
+ artifact,
300
+ path: from,
301
+ location,
302
+ message: `${from} ${how(edge)}, a screen of ${target.id}, and only a named router file may import screens directly.`,
303
+ remediation: source !== undefined
304
+ ? `Let the router compose ${target.id}'s screen, or consume a component API that ${target.id} exposes through an entrypoint (TS003).`
305
+ : "Import the screen from the router file named in routerFiles, or name this file there if it is the router (TS003).",
306
+ ...subject,
307
+ }));
308
+ }
309
+ continue;
310
+ }
311
+ if (!target.meta.entrypoints.includes(to)) {
312
+ findings.push(finding({
313
+ code: "VSA003",
314
+ artifact,
315
+ path: from,
316
+ location,
317
+ message: `${source ? source.id : describeRegion(regionFrom)} ${how(edge)}, a private file of ${target.id} reached through ${through(edge)}; ${describeSurfaces(model, target, from)}.`,
318
+ remediation: `${importInstead(model, target, from)} and have it export what is needed, or move the shared rule into a contract (VSA003).`,
319
+ ...subject,
320
+ }));
321
+ if (edge.wildcardAlias && (edge.via === "alias" || edge.via === "workspace")) {
322
+ findings.push(finding({
323
+ code: "TS006",
324
+ artifact,
325
+ path: from,
326
+ location,
327
+ message: `The wildcard in "${edge.specifier}" opens ${to}, a private file of ${target.id}, to code outside the slice.`,
328
+ remediation: `Map the alias for ${target.id} to its entrypoints only, so the alias cannot reach internals (TS006).`,
329
+ ...subject,
330
+ }));
331
+ }
332
+ continue;
333
+ }
334
+ if (source !== undefined && (edge.via === "relative" || edge.via === "baseUrl")) {
335
+ const alias = resolver.aliasFor(from, to);
336
+ if (alias !== null) {
337
+ findings.push(finding({
338
+ code: "TS002",
339
+ severity: "warning",
340
+ artifact,
341
+ path: from,
342
+ location,
343
+ message: `${source.id} reaches the entrypoint of ${target.id} through "${edge.specifier}" rather than its configured alias "${alias}".`,
344
+ remediation: `Import "${alias}" (TS002).`,
345
+ ...subject,
346
+ }));
347
+ }
348
+ }
349
+ continue;
350
+ }
351
+ if (source === undefined)
352
+ continue;
353
+ // Inside one slice, or out to shared and infrastructure.
354
+ if (target !== undefined && source.meta.entrypoints.includes(from) && isScreen(to)) {
355
+ findings.push(finding({
356
+ code: "TS003",
357
+ artifact,
358
+ path: from,
359
+ location,
360
+ message: `The entrypoint of ${source.id} ${how(edge)}, one of its screens, and screens must not be exported from a slice's contract.`,
361
+ remediation: "Remove the screen from the entrypoint and let the named router import it (TS003).",
362
+ ...subject,
363
+ }));
364
+ continue;
365
+ }
366
+ // 5. AMP006
367
+ if (isPure(source, from) && ((target !== undefined && inSeam(source, to)) || regionTo.kind === "infrastructure")) {
368
+ findings.push(finding({
369
+ code: "AMP006",
370
+ artifact,
371
+ path: from,
372
+ location,
373
+ message: `${from} is a policy or model file of ${source.id} and ${how(edge)}, which is ${regionTo.kind === "infrastructure" ? "infrastructure" : "its client seam"}.`,
374
+ remediation: "Policies and models import models only; have a use-case call the client and pass plain values in (AMP006).",
375
+ ...subject,
376
+ }));
377
+ continue;
378
+ }
379
+ // 6. VSA006
380
+ if (target !== undefined) {
381
+ const a = layerOf(source, from);
382
+ const b = layerOf(source, to);
383
+ if (a.layer === null || b.layer === null || !knownLayer(config, a.layer) || !knownLayer(config, b.layer))
384
+ continue;
385
+ if (!layerAllows(config, a.layer, b.layer)) {
386
+ const allowed = [a.layer, ...(config.layerMatrix[a.layer] ?? [])].filter((value, i, all) => all.indexOf(value) === i);
387
+ findings.push(finding({
388
+ code: "VSA006",
389
+ artifact,
390
+ path: from,
391
+ location,
392
+ message: `${from} is in the ${a.layer} layer of ${source.id} and ${how(edge)}, which is in the ${b.layer} layer.`,
393
+ remediation: `The ${a.layer} layer may import ${allowed.join(", ")}; move the dependency so it flows downward (profile §3).`,
394
+ ...subject,
395
+ }));
396
+ }
397
+ }
398
+ }
399
+ return { findings, sliceEdges, fileEdges, crossSliceEdges };
400
+ }
401
+ /** One warning per slice whose code files are in no declared layer, or in a layer the matrix does not know (VSA006). */
402
+ export function checkUnclassified(model) {
403
+ const findings = [];
404
+ for (const slice of model.slices) {
405
+ const unclassified = [];
406
+ const files = new Set(slice.claimFiles.flat());
407
+ for (const file of [...files].sort()) {
408
+ if (!/\.[cm]?[jt]sx?$/.test(file) ||
409
+ matchAny(model.config.tests, file) ||
410
+ slice.meta.entrypoints.includes(file) ||
411
+ model.underBackend(file) ||
412
+ !model.inScope(file))
413
+ continue;
414
+ // A file another slice also claims is VSA009 already, and its region is that slice's.
415
+ if (model.regionOf(file).owner !== slice.id)
416
+ continue;
417
+ const placed = layerOf(slice, file);
418
+ if (placed.layer === null || !knownLayer(model.config, placed.layer))
419
+ unclassified.push(file);
420
+ }
421
+ if (unclassified.length === 0)
422
+ continue;
423
+ findings.push(finding({
424
+ code: "VSA006",
425
+ severity: "warning",
426
+ artifact: slice.id,
427
+ path: slice.path,
428
+ field: "/intentset/slice/layers",
429
+ message: `${unclassified.length === 1 ? "1 file" : `${unclassified.length} files`} of ${slice.id} ${unclassified.length === 1 ? "is" : "are"} in no single layer the matrix knows, so ${unclassified.length === 1 ? "its" : "their"} imports were not layer-checked: ${unclassified.slice(0, 3).join(", ")}${unclassified.length > 3 ? ` and ${unclassified.length - 3} more` : ""}.`,
430
+ remediation: "Cover each file with exactly one of the presentation, application, policy, model and external layer patterns (VSA006).",
431
+ paths: unclassified,
432
+ }));
433
+ }
434
+ return findings;
435
+ }
436
+ //# sourceMappingURL=boundaries.js.map