@goodbones/core 0.1.0-beta.1

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 (147) hide show
  1. package/LICENSE +21 -0
  2. package/build/dts/core/baseline.d.ts +16 -0
  3. package/build/dts/core/baseline.d.ts.map +1 -0
  4. package/build/dts/core/coverage.d.ts +47 -0
  5. package/build/dts/core/coverage.d.ts.map +1 -0
  6. package/build/dts/core/exports.d.ts +38 -0
  7. package/build/dts/core/exports.d.ts.map +1 -0
  8. package/build/dts/core/graph.d.ts +40 -0
  9. package/build/dts/core/graph.d.ts.map +1 -0
  10. package/build/dts/core/imports.d.ts +29 -0
  11. package/build/dts/core/imports.d.ts.map +1 -0
  12. package/build/dts/core/members.d.ts +26 -0
  13. package/build/dts/core/members.d.ts.map +1 -0
  14. package/build/dts/core/patterns.d.ts +17 -0
  15. package/build/dts/core/patterns.d.ts.map +1 -0
  16. package/build/dts/core/structure.d.ts +48 -0
  17. package/build/dts/core/structure.d.ts.map +1 -0
  18. package/build/dts/core/surface.d.ts +39 -0
  19. package/build/dts/core/surface.d.ts.map +1 -0
  20. package/build/dts/domain/architecture-config.d.ts +322 -0
  21. package/build/dts/domain/architecture-config.d.ts.map +1 -0
  22. package/build/dts/domain/architecture-error.d.ts +35 -0
  23. package/build/dts/domain/architecture-error.d.ts.map +1 -0
  24. package/build/dts/domain/facts.d.ts +26 -0
  25. package/build/dts/domain/facts.d.ts.map +1 -0
  26. package/build/dts/domain/violation.d.ts +11 -0
  27. package/build/dts/domain/violation.d.ts.map +1 -0
  28. package/build/dts/index.d.ts +23 -0
  29. package/build/dts/index.d.ts.map +1 -0
  30. package/build/dts/infrastructure/fact-extractor-fake.d.ts +4 -0
  31. package/build/dts/infrastructure/fact-extractor-fake.d.ts.map +1 -0
  32. package/build/dts/infrastructure/file-system-fake.d.ts +3 -0
  33. package/build/dts/infrastructure/file-system-fake.d.ts.map +1 -0
  34. package/build/dts/infrastructure/file-system-live.d.ts +3 -0
  35. package/build/dts/infrastructure/file-system-live.d.ts.map +1 -0
  36. package/build/dts/infrastructure/manifest-file.d.ts +3 -0
  37. package/build/dts/infrastructure/manifest-file.d.ts.map +1 -0
  38. package/build/dts/infrastructure/module-resolver-fake.d.ts +3 -0
  39. package/build/dts/infrastructure/module-resolver-fake.d.ts.map +1 -0
  40. package/build/dts/infrastructure/walk.d.ts +4 -0
  41. package/build/dts/infrastructure/walk.d.ts.map +1 -0
  42. package/build/dts/load/policy.d.ts +42 -0
  43. package/build/dts/load/policy.d.ts.map +1 -0
  44. package/build/dts/manifest/compile.d.ts +26 -0
  45. package/build/dts/manifest/compile.d.ts.map +1 -0
  46. package/build/dts/manifest/glob.d.ts +14 -0
  47. package/build/dts/manifest/glob.d.ts.map +1 -0
  48. package/build/dts/manifest/manifest.d.ts +208 -0
  49. package/build/dts/manifest/manifest.d.ts.map +1 -0
  50. package/build/dts/ports/fact-extractor.d.ts +5 -0
  51. package/build/dts/ports/fact-extractor.d.ts.map +1 -0
  52. package/build/dts/ports/file-system.d.ts +5 -0
  53. package/build/dts/ports/file-system.d.ts.map +1 -0
  54. package/build/dts/ports/language.d.ts +14 -0
  55. package/build/dts/ports/language.d.ts.map +1 -0
  56. package/build/dts/ports/module-resolver.d.ts +12 -0
  57. package/build/dts/ports/module-resolver.d.ts.map +1 -0
  58. package/build/dts/testing.d.ts +4 -0
  59. package/build/dts/testing.d.ts.map +1 -0
  60. package/build/esm/core/baseline.js +30 -0
  61. package/build/esm/core/baseline.js.map +1 -0
  62. package/build/esm/core/coverage.js +75 -0
  63. package/build/esm/core/coverage.js.map +1 -0
  64. package/build/esm/core/exports.js +102 -0
  65. package/build/esm/core/exports.js.map +1 -0
  66. package/build/esm/core/graph.js +259 -0
  67. package/build/esm/core/graph.js.map +1 -0
  68. package/build/esm/core/imports.js +103 -0
  69. package/build/esm/core/imports.js.map +1 -0
  70. package/build/esm/core/members.js +106 -0
  71. package/build/esm/core/members.js.map +1 -0
  72. package/build/esm/core/patterns.js +75 -0
  73. package/build/esm/core/patterns.js.map +1 -0
  74. package/build/esm/core/structure.js +257 -0
  75. package/build/esm/core/structure.js.map +1 -0
  76. package/build/esm/core/surface.js +137 -0
  77. package/build/esm/core/surface.js.map +1 -0
  78. package/build/esm/domain/architecture-config.js +312 -0
  79. package/build/esm/domain/architecture-config.js.map +1 -0
  80. package/build/esm/domain/architecture-error.js +41 -0
  81. package/build/esm/domain/architecture-error.js.map +1 -0
  82. package/build/esm/domain/facts.js +2 -0
  83. package/build/esm/domain/facts.js.map +1 -0
  84. package/build/esm/domain/violation.js +8 -0
  85. package/build/esm/domain/violation.js.map +1 -0
  86. package/build/esm/index.js +22 -0
  87. package/build/esm/index.js.map +1 -0
  88. package/build/esm/infrastructure/fact-extractor-fake.js +24 -0
  89. package/build/esm/infrastructure/fact-extractor-fake.js.map +1 -0
  90. package/build/esm/infrastructure/file-system-fake.js +10 -0
  91. package/build/esm/infrastructure/file-system-fake.js.map +1 -0
  92. package/build/esm/infrastructure/file-system-live.js +27 -0
  93. package/build/esm/infrastructure/file-system-live.js.map +1 -0
  94. package/build/esm/infrastructure/manifest-file.js +15 -0
  95. package/build/esm/infrastructure/manifest-file.js.map +1 -0
  96. package/build/esm/infrastructure/module-resolver-fake.js +33 -0
  97. package/build/esm/infrastructure/module-resolver-fake.js.map +1 -0
  98. package/build/esm/infrastructure/walk.js +45 -0
  99. package/build/esm/infrastructure/walk.js.map +1 -0
  100. package/build/esm/load/policy.js +207 -0
  101. package/build/esm/load/policy.js.map +1 -0
  102. package/build/esm/manifest/compile.js +706 -0
  103. package/build/esm/manifest/compile.js.map +1 -0
  104. package/build/esm/manifest/glob.js +77 -0
  105. package/build/esm/manifest/glob.js.map +1 -0
  106. package/build/esm/manifest/manifest.js +303 -0
  107. package/build/esm/manifest/manifest.js.map +1 -0
  108. package/build/esm/ports/fact-extractor.js +2 -0
  109. package/build/esm/ports/fact-extractor.js.map +1 -0
  110. package/build/esm/ports/file-system.js +2 -0
  111. package/build/esm/ports/file-system.js.map +1 -0
  112. package/build/esm/ports/language.js +2 -0
  113. package/build/esm/ports/language.js.map +1 -0
  114. package/build/esm/ports/module-resolver.js +2 -0
  115. package/build/esm/ports/module-resolver.js.map +1 -0
  116. package/build/esm/testing.js +7 -0
  117. package/build/esm/testing.js.map +1 -0
  118. package/package.json +61 -0
  119. package/src/core/baseline.ts +62 -0
  120. package/src/core/coverage.ts +154 -0
  121. package/src/core/exports.ts +171 -0
  122. package/src/core/graph.ts +331 -0
  123. package/src/core/imports.ts +160 -0
  124. package/src/core/members.ts +161 -0
  125. package/src/core/patterns.ts +116 -0
  126. package/src/core/structure.ts +349 -0
  127. package/src/core/surface.ts +181 -0
  128. package/src/domain/architecture-config.ts +370 -0
  129. package/src/domain/architecture-error.ts +59 -0
  130. package/src/domain/facts.ts +47 -0
  131. package/src/domain/violation.ts +24 -0
  132. package/src/index.ts +136 -0
  133. package/src/infrastructure/fact-extractor-fake.ts +28 -0
  134. package/src/infrastructure/file-system-fake.ts +14 -0
  135. package/src/infrastructure/file-system-live.ts +27 -0
  136. package/src/infrastructure/manifest-file.ts +17 -0
  137. package/src/infrastructure/module-resolver-fake.ts +39 -0
  138. package/src/infrastructure/walk.ts +54 -0
  139. package/src/load/policy.ts +344 -0
  140. package/src/manifest/compile.ts +1011 -0
  141. package/src/manifest/glob.ts +108 -0
  142. package/src/manifest/manifest.ts +408 -0
  143. package/src/ports/fact-extractor.ts +12 -0
  144. package/src/ports/file-system.ts +8 -0
  145. package/src/ports/language.ts +39 -0
  146. package/src/ports/module-resolver.ts +28 -0
  147. package/src/testing.ts +6 -0
@@ -0,0 +1,62 @@
1
+ import type { Violation } from "../domain/violation.js";
2
+ import { fingerprintOf } from "../domain/violation.js";
3
+
4
+ // A baseline is the set of violations a repository is choosing to carry while it
5
+ // adopts a policy. It exists so a rule can be turned on before the code is
6
+ // clean — the alternative is not turning it on.
7
+ //
8
+ // Two properties make it a ratchet rather than a suppression list:
9
+ //
10
+ // - Entries are line-independent fingerprints, so an entry survives edits to
11
+ // the file it names. One keyed on a position would go stale on the first
12
+ // reformat and silently re-admit the violation it was meant to record.
13
+ // - An entry that no longer fires is an error, not a shrug. Fixing a violation
14
+ // must remove its entry, or the floor never rises.
15
+ export type Baseline = {
16
+ readonly version: 1;
17
+ readonly entries: ReadonlyArray<string>;
18
+ };
19
+
20
+ export const EMPTY_BASELINE: Baseline = { version: 1, entries: [] };
21
+
22
+ export const baselineOf = (violations: Iterable<Violation>): Baseline => ({
23
+ version: 1,
24
+ entries: [...new Set([...violations].map(fingerprintOf))].sort(),
25
+ });
26
+
27
+ export const decodeBaseline = (raw: unknown): Baseline => {
28
+ if (typeof raw !== "object" || raw === null) return EMPTY_BASELINE;
29
+ const entries = (raw as { entries?: unknown }).entries;
30
+ if (!Array.isArray(entries)) return EMPTY_BASELINE;
31
+ return { version: 1, entries: entries.filter((one): one is string => typeof one === "string") };
32
+ };
33
+
34
+ export const serializeBaseline = (baseline: Baseline): string =>
35
+ `${JSON.stringify(baseline, null, 2)}\n`;
36
+
37
+ export type BaselineFilter = {
38
+ readonly isBaselined: (violation: Violation) => boolean;
39
+ };
40
+
41
+ export const makeBaselineFilter = (baseline: Baseline): BaselineFilter => {
42
+ const entries = new Set(baseline.entries);
43
+ return { isBaselined: (violation) => entries.has(fingerprintOf(violation)) };
44
+ };
45
+
46
+ // The ratchet's teeth: entries the code no longer produces. A baseline that is
47
+ // allowed to keep them stops being a record of debt and becomes a place to hide.
48
+ export const staleEntriesOf = (
49
+ baseline: Baseline,
50
+ violations: Iterable<Violation>,
51
+ ): ReadonlyArray<string> => {
52
+ const current = new Set([...violations].map(fingerprintOf));
53
+ return baseline.entries.filter((entry) => !current.has(entry));
54
+ };
55
+
56
+ export const unbaselined = (
57
+ baseline: Baseline,
58
+ violations: Iterable<Violation>,
59
+ ): ReadonlyArray<Violation> => {
60
+ const { isBaselined } = makeBaselineFilter(baseline);
61
+ return [...violations].filter((violation) => !isBaselined(violation));
62
+ };
@@ -0,0 +1,154 @@
1
+ import { OPEN_LAYOUT } from "../domain/architecture-config.js";
2
+ import type { CompiledGraph } from "./graph.js";
3
+ import type { CompiledImportRule } from "./imports.js";
4
+ import type { CompiledMemberRule } from "./members.js";
5
+ import { firstFromMatch } from "./patterns.js";
6
+ import type { CompiledStructure } from "./structure.js";
7
+ import type { CompiledSurfaceRule } from "./surface.js";
8
+
9
+ // A probe proves a rule can fire. This is the other question: does the tree
10
+ // actually reach the files? A file no allowlist selects, in a folder no layout
11
+ // enumerates, is a file the policy has nothing to say about — and a policy that
12
+ // is 40% silence looks exactly like one that is 100% enforced, until counted.
13
+
14
+ export type FamilyCoverage = {
15
+ readonly covered: number;
16
+ readonly total: number;
17
+ };
18
+
19
+ export type StructureCoverage = {
20
+ // Its folder's files are listed by name.
21
+ readonly enumerated: number;
22
+ // Its folder is claimed, and admits any name — `layout: "open"`.
23
+ readonly open: number;
24
+ readonly total: number;
25
+ };
26
+
27
+ export type Coverage = {
28
+ readonly files: number;
29
+ // Under an import allowlist. An `unrestricted` tier emits none, so this is
30
+ // the honest count of files whose imports are actually bounded.
31
+ readonly imports: FamilyCoverage;
32
+ readonly structure: StructureCoverage;
33
+ readonly members: FamilyCoverage;
34
+ readonly surface: FamilyCoverage;
35
+ // In the scope of a cycles or orphans rule.
36
+ readonly graph: FamilyCoverage;
37
+ };
38
+
39
+ export type CoverageInputs = {
40
+ readonly importRules: ReadonlyArray<CompiledImportRule>;
41
+ readonly structure: CompiledStructure;
42
+ readonly memberRules: ReadonlyArray<CompiledMemberRule>;
43
+ readonly surfaceRules: ReadonlyArray<CompiledSurfaceRule>;
44
+ readonly graph: CompiledGraph;
45
+ };
46
+
47
+ // An allowlist names no `to`: it fires when the target matches none of its
48
+ // `toNot`. A prohibition names one. Only the former bounds a file.
49
+ const isAllowlist = (rule: CompiledImportRule): boolean =>
50
+ rule.to.length === 0 && rule.toNot.length > 0;
51
+
52
+ const dirnameOf = (file: string): string => {
53
+ const at = file.lastIndexOf("/");
54
+ return at === -1 ? "" : file.slice(0, at);
55
+ };
56
+
57
+ const selects = (
58
+ rule: { from: ReadonlyArray<RegExp>; fromNot: ReadonlyArray<RegExp> },
59
+ file: string,
60
+ ) => firstFromMatch(rule, file) !== null;
61
+
62
+ export const coverageOf = (policy: CoverageInputs, files: ReadonlyArray<string>): Coverage => {
63
+ const allowlists = policy.importRules.filter(isAllowlist);
64
+ let imports = 0;
65
+ let enumerated = 0;
66
+ let open = 0;
67
+ let members = 0;
68
+ let surface = 0;
69
+ let graph = 0;
70
+
71
+ for (const file of files) {
72
+ if (allowlists.some((rule) => selects(rule, file))) imports += 1;
73
+
74
+ const folder = dirnameOf(file);
75
+ const governing = policy.structure.folders.filter((rule) =>
76
+ rule.folder.some((pattern) => pattern.test(folder)),
77
+ );
78
+ if (governing.length > 0) {
79
+ if (governing.every((rule) => rule.files.some((pattern) => pattern.source === OPEN_LAYOUT))) {
80
+ open += 1;
81
+ } else {
82
+ enumerated += 1;
83
+ }
84
+ }
85
+
86
+ if (policy.memberRules.some((rule) => selects(rule, file))) members += 1;
87
+ if (policy.surfaceRules.some((rule) => selects(rule, file))) surface += 1;
88
+
89
+ const scoped = [...policy.graph.cycles, ...policy.graph.orphans].some(
90
+ (rule) =>
91
+ rule.within.some((pattern) => pattern.test(file)) &&
92
+ !rule.withinNot.some((pattern) => pattern.test(file)),
93
+ );
94
+ if (scoped) graph += 1;
95
+ }
96
+
97
+ const total = files.length;
98
+ return {
99
+ files: total,
100
+ imports: { covered: imports, total },
101
+ structure: { enumerated, open, total },
102
+ members: { covered: members, total },
103
+ surface: { covered: surface, total },
104
+ graph: { covered: graph, total },
105
+ };
106
+ };
107
+
108
+ // A floor the policy states for itself, per family, as a fraction. Structure
109
+ // counts enumerated folders only: an open one is claimed, not policed by name.
110
+ export type CoverageFloors = {
111
+ readonly imports?: number;
112
+ readonly structure?: number;
113
+ readonly members?: number;
114
+ readonly surface?: number;
115
+ readonly graph?: number;
116
+ };
117
+
118
+ export type CoverageFamily = keyof CoverageFloors;
119
+
120
+ export const fractionOf = (covered: number, total: number): number =>
121
+ total === 0 ? 1 : covered / total;
122
+
123
+ export const fractionsOf = (coverage: Coverage): Readonly<Record<CoverageFamily, number>> => ({
124
+ imports: fractionOf(coverage.imports.covered, coverage.imports.total),
125
+ structure: fractionOf(coverage.structure.enumerated, coverage.structure.total),
126
+ members: fractionOf(coverage.members.covered, coverage.members.total),
127
+ surface: fractionOf(coverage.surface.covered, coverage.surface.total),
128
+ graph: fractionOf(coverage.graph.covered, coverage.graph.total),
129
+ });
130
+
131
+ export type Shortfall = {
132
+ readonly family: CoverageFamily;
133
+ readonly actual: number;
134
+ readonly floor: number;
135
+ };
136
+
137
+ export const coverageShortfalls = (
138
+ coverage: Coverage,
139
+ floors: CoverageFloors,
140
+ ): ReadonlyArray<Shortfall> => {
141
+ const actual = fractionsOf(coverage);
142
+ const families: ReadonlyArray<CoverageFamily> = [
143
+ "imports",
144
+ "structure",
145
+ "members",
146
+ "surface",
147
+ "graph",
148
+ ];
149
+ return families.flatMap((family) => {
150
+ const floor = floors[family];
151
+ if (floor === undefined || actual[family] >= floor) return [];
152
+ return [{ family, actual: actual[family], floor }];
153
+ });
154
+ };
@@ -0,0 +1,171 @@
1
+ import * as Result from "effect/Result";
2
+
3
+ import type { BindingKind, ExportProbe, ExportRule } from "../domain/architecture-config.js";
4
+ import type { ImportUnresolved, PatternInvalid } from "../domain/architecture-error.js";
5
+ import type { Binding } from "../domain/facts.js";
6
+ import type { Violation } from "../domain/violation.js";
7
+ import type { FactExtractor } from "../ports/fact-extractor.js";
8
+ import type { ModuleResolver } from "../ports/module-resolver.js";
9
+ import {
10
+ compilePatterns,
11
+ firstFromMatch,
12
+ sourcesOf,
13
+ targetAllowed,
14
+ validateTargetPatterns,
15
+ } from "./patterns.js";
16
+
17
+ export type { Binding } from "../domain/facts.js";
18
+
19
+ const DEFAULT_KINDS: ReadonlyArray<BindingKind> = ["named"];
20
+
21
+ export type CompiledExportRule = {
22
+ readonly name: string;
23
+ readonly message: string;
24
+ readonly from: ReadonlyArray<RegExp>;
25
+ readonly fromNot: ReadonlyArray<RegExp>;
26
+ readonly to: ReadonlyArray<string>;
27
+ readonly toNot: ReadonlyArray<string>;
28
+ readonly symbols: ReadonlySet<string> | null;
29
+ readonly kinds: ReadonlyArray<BindingKind>;
30
+ readonly fix: "subpath-namespace-import" | null;
31
+ readonly probe: ExportProbe;
32
+ };
33
+
34
+ export const compileExportRule = (
35
+ rule: ExportRule,
36
+ ): Result.Result<CompiledExportRule, PatternInvalid> => {
37
+ const from = compilePatterns(rule.name, "from", rule.from);
38
+ if (Result.isFailure(from)) return Result.fail(from.failure);
39
+ const fromNot = compilePatterns(rule.name, "fromNot", rule.fromNot);
40
+ if (Result.isFailure(fromNot)) return Result.fail(fromNot.failure);
41
+
42
+ const targets = validateTargetPatterns(rule.name, [
43
+ ["to", rule.to],
44
+ ["toNot", rule.toNot],
45
+ ]);
46
+ if (Result.isFailure(targets)) return Result.fail(targets.failure);
47
+
48
+ return Result.succeed({
49
+ name: rule.name,
50
+ message: rule.message,
51
+ probe: rule.probe,
52
+ from: from.success,
53
+ fromNot: fromNot.success,
54
+ to: sourcesOf(rule.to),
55
+ toNot: sourcesOf(rule.toNot),
56
+ symbols: rule.symbols === undefined ? null : new Set(rule.symbols),
57
+ kinds: rule.kinds ?? DEFAULT_KINDS,
58
+ fix: rule.fix ?? null,
59
+ });
60
+ };
61
+
62
+ export const compileExportRules = (
63
+ rules: ReadonlyArray<ExportRule>,
64
+ ): Result.Result<ReadonlyArray<CompiledExportRule>, PatternInvalid> => {
65
+ const compiled: Array<CompiledExportRule> = [];
66
+ for (const rule of rules) {
67
+ const one = compileExportRule(rule);
68
+ if (Result.isFailure(one)) return Result.fail(one.failure);
69
+ compiled.push(one.success);
70
+ }
71
+ return Result.succeed(compiled);
72
+ };
73
+
74
+ export type BindingEdge = {
75
+ readonly importer: string;
76
+ readonly specifier: string;
77
+ readonly bindings: ReadonlyArray<Binding>;
78
+ };
79
+
80
+ export type SelectedExportRule = readonly [CompiledExportRule, RegExpExecArray];
81
+
82
+ export const exportRulesSelecting = (
83
+ rules: ReadonlyArray<CompiledExportRule>,
84
+ importer: string,
85
+ ): ReadonlyArray<SelectedExportRule> => {
86
+ const selected: Array<SelectedExportRule> = [];
87
+ for (const rule of rules) {
88
+ const captures = firstFromMatch(rule, importer);
89
+ if (captures !== null) selected.push([rule, captures]);
90
+ }
91
+ return selected;
92
+ };
93
+
94
+ const covers = (rule: CompiledExportRule, binding: Binding): boolean =>
95
+ rule.kinds.includes(binding.kind) && (rule.symbols === null || rule.symbols.has(binding.symbol));
96
+
97
+ export type ExportViolation = {
98
+ readonly violation: Violation;
99
+ readonly rule: CompiledExportRule;
100
+ readonly bindings: ReadonlyArray<Binding>;
101
+ };
102
+
103
+ export const evaluateSelectedBindings = (
104
+ selected: ReadonlyArray<SelectedExportRule>,
105
+ resolver: ModuleResolver,
106
+ edge: BindingEdge,
107
+ ): Result.Result<ReadonlyArray<ExportViolation>, ImportUnresolved> => {
108
+ if (selected.length === 0 || edge.bindings.length === 0) return Result.succeed([]);
109
+
110
+ const resolved = resolver.resolve(edge.importer, edge.specifier);
111
+ if (Result.isFailure(resolved)) return Result.fail(resolved.failure);
112
+ const target = resolved.success;
113
+
114
+ const violations: Array<ExportViolation> = [];
115
+ for (const [rule, captures] of selected) {
116
+ if (!targetAllowed(rule, captures, target.path)) continue;
117
+
118
+ const offending = edge.bindings.filter((binding) => covers(rule, binding));
119
+ if (offending.length === 0) continue;
120
+
121
+ // A rule carrying a fix reports once for the whole declaration, because the
122
+ // fix rewrites the declaration as a unit; one without reports per symbol, so
123
+ // the diagnostic sits on the name that is actually restricted.
124
+ const groups = rule.fix === null ? offending.map((binding) => [binding]) : [offending];
125
+ for (const bindings of groups) {
126
+ violations.push({
127
+ rule,
128
+ bindings,
129
+ violation: {
130
+ kind: "export",
131
+ ruleName: rule.name,
132
+ message: rule.message,
133
+ file: edge.importer,
134
+ subject: `${target.path}#${bindings.map((binding) => binding.symbol).join(",")}`,
135
+ },
136
+ });
137
+ }
138
+ }
139
+
140
+ return Result.succeed(violations);
141
+ };
142
+
143
+ export const evaluateBindingEdge = (
144
+ rules: ReadonlyArray<CompiledExportRule>,
145
+ resolver: ModuleResolver,
146
+ edge: BindingEdge,
147
+ ): Result.Result<ReadonlyArray<ExportViolation>, ImportUnresolved> =>
148
+ evaluateSelectedBindings(exportRulesSelecting(rules, edge.importer), resolver, edge);
149
+
150
+ // A probe with a `source` is checked through the parser: every edge in the
151
+ // snippet is taken to resolve to `probe.to`, and a binding named `probe.symbol`
152
+ // must come out of the extractor covered by the rule. A probe without one is
153
+ // checked against the rule's patterns alone.
154
+ export const exportRulesFailingTheirProbe = (
155
+ rules: ReadonlyArray<CompiledExportRule>,
156
+ extractor: FactExtractor,
157
+ ): ReadonlyArray<CompiledExportRule> =>
158
+ rules.filter((rule) => {
159
+ const captures = firstFromMatch(rule, rule.probe.from);
160
+ if (captures === null) return true;
161
+ if (!targetAllowed(rule, captures, rule.probe.to)) return true;
162
+
163
+ if (rule.probe.source === undefined) {
164
+ return !covers(rule, { symbol: rule.probe.symbol, kind: rule.probe.kind ?? "named" });
165
+ }
166
+ const facts = extractor.factsOf(rule.probe.from, rule.probe.source);
167
+ const bindings = [...facts.bindings.values()].flat();
168
+ return !bindings.some(
169
+ (binding) => binding.symbol === rule.probe.symbol && covers(rule, binding),
170
+ );
171
+ });