@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,370 @@
1
+ import * as Schema from "effect/Schema";
2
+
3
+ // Every pattern in this config is a JavaScript regular-expression source string
4
+ // matched against a repo-relative, forward-slash path — the same vocabulary
5
+ // dependency-cruiser rules are written in, so a rule ported from there keeps its
6
+ // pattern character-for-character. A list matches when ANY member matches.
7
+ const PatternList = Schema.Union([Schema.String, Schema.Array(Schema.String)]);
8
+
9
+ // An import edge is a violation when the importer matches `from` (and not
10
+ // `fromNot`) AND the resolved target matches `to` (and not `toNot`). Omitting
11
+ // `to` means "any target", which is how a rule expresses "this folder may not
12
+ // import anything outside its allowlist" as a single `toNot`.
13
+ // A synthetic edge this rule must report. Every rule carries one: a configured
14
+ // rule that reports nothing is indistinguishable from a clean codebase, and that
15
+ // is the failure this package exists to make impossible. `from` is a repo-relative
16
+ // importer path. `to` is the target in one of three forms, and the form is its
17
+ // dependency kind: a repo-relative resolved path is `local`, `{ external }`
18
+ // names a third-party package, `{ builtin }` names a runtime module.
19
+ const ImportProbeTarget = Schema.Union([
20
+ Schema.String,
21
+ Schema.Struct({ external: Schema.String }),
22
+ Schema.Struct({ builtin: Schema.String }),
23
+ ]);
24
+
25
+ const ImportProbe = Schema.Struct({
26
+ from: Schema.String,
27
+ to: ImportProbeTarget,
28
+ });
29
+
30
+ export const ImportRule = Schema.Struct({
31
+ name: Schema.String,
32
+ message: Schema.String,
33
+ probe: ImportProbe,
34
+ from: PatternList,
35
+ fromNot: Schema.optionalKey(PatternList),
36
+ to: Schema.optionalKey(PatternList),
37
+ toNot: Schema.optionalKey(PatternList),
38
+ // Third-party packages this rule permits, by package name. An external
39
+ // target is judged by its package, never by where the language's resolver
40
+ // happened to find it on disk — `to`/`toNot` patterns are for the
41
+ // repository's own files.
42
+ externals: Schema.optionalKey(Schema.Array(Schema.String)),
43
+ // `external` is a third-party package, `builtin` a runtime module, `local`
44
+ // anything in the repository. Compared against what the resolver reports,
45
+ // never read off a path. Replaces dependency-cruiser's `dependencyTypes`,
46
+ // whose finer npm grades no rule in this repo distinguishes.
47
+ dependencyKind: Schema.optionalKey(Schema.Literals(["external", "local", "builtin"])),
48
+ });
49
+
50
+ // Which language pack resolves and parses the files a scope covers. A monorepo
51
+ // needs several scopes even in one language — the web pass resolves `@/*` the
52
+ // server pass does not — and a second language is one more scope. `options`
53
+ // belong to the language: the policy carries them opaquely and the pack
54
+ // validates them at load, so nothing here knows what a tsconfig is.
55
+ const ResolveScope = Schema.Struct({
56
+ // A regular-expression source matched against the importing file's path.
57
+ files: Schema.String,
58
+ language: Schema.String,
59
+ options: Schema.optionalKey(Schema.Unknown),
60
+ });
61
+
62
+ export const ResolveConfig = Schema.Struct({
63
+ scopes: Schema.Array(ResolveScope),
64
+ // An edge nobody can resolve is an edge no rule can police, which is the
65
+ // silent-vacuity failure this whole package exists to prevent. Default loud.
66
+ unresolved: Schema.optionalKey(Schema.Literals(["error", "off"])),
67
+ ignoreUnresolved: Schema.optionalKey(Schema.Array(Schema.String)),
68
+ });
69
+
70
+ // The autofix strategies a rule may name. Each is a rewrite in one language's
71
+ // module syntax, so a language pack lists the ones it implements.
72
+ export const ExportFix = Schema.Literals(["subpath-namespace-import"]);
73
+
74
+ // Which binding form an import site used. A rule that fences off a factory
75
+ // function cares about `named`; one steering people to namespace subpath imports
76
+ // cares that `named` was used at all.
77
+ const BindingKind = Schema.Literals(["named", "default", "namespace"]);
78
+
79
+ // `source`, when present, is a snippet the loading adapter parses: the probe
80
+ // then holds only if a binding named `symbol` comes out of the parser and the
81
+ // rule covers it, with every edge in the snippet taken to resolve to `to`.
82
+ // Without it the probe is a synthetic binding of `symbol` and `kind`.
83
+ const ExportProbe = Schema.Struct({
84
+ from: Schema.String,
85
+ to: Schema.String,
86
+ symbol: Schema.String,
87
+ kind: Schema.optionalKey(BindingKind),
88
+ source: Schema.optionalKey(Schema.String),
89
+ });
90
+
91
+ // Where a given *exported symbol* may be imported. `imports` asks whether one
92
+ // file may reach another at all; this asks which names it may pull across when
93
+ // it does — the distinction a path rule cannot make, because every importer of a
94
+ // barrel resolves to the same file.
95
+ export const ExportRule = Schema.Struct({
96
+ name: Schema.String,
97
+ message: Schema.String,
98
+ probe: ExportProbe,
99
+ from: PatternList,
100
+ fromNot: Schema.optionalKey(PatternList),
101
+ to: PatternList,
102
+ toNot: Schema.optionalKey(PatternList),
103
+ // Exact exported names. Omit to mean "any name", which is how a rule bans a
104
+ // whole binding form (every named import from a package barrel, say).
105
+ symbols: Schema.optionalKey(Schema.Array(Schema.String)),
106
+ // Defaults to ["named"] — the discriminating form for every rule of this shape
107
+ // written so far.
108
+ kinds: Schema.optionalKey(Schema.Array(BindingKind)),
109
+ // A named autofix strategy, which a language pack may or may not implement:
110
+ // the loader refuses a rule naming one no loaded language does.
111
+ // `subpath-namespace-import` is an ES-module rewrite — `import { A, B as C }
112
+ // from "pkg"` into `import * as A from "pkg/A"` / `import * as C from "pkg/B"`,
113
+ // for packages that publish each module as its own subpath. A rule carrying a
114
+ // fix reports once per declaration rather than once per symbol, because the
115
+ // fix rewrites the whole declaration.
116
+ fix: Schema.optionalKey(ExportFix),
117
+ });
118
+
119
+ // What a name was declared as. For an export site, the declaration that
120
+ // introduced it; for a member site, the declaration it is written in.
121
+ // `expression` is `export default <expr>`; `other` covers a namespace, an
122
+ // `export =`, and a re-export, whose declaration is somewhere else. A second
123
+ // language will want `struct`, `record`, `constant`, `module` here; they are
124
+ // added with the pack that reads them, not before.
125
+ export const DeclarationKind = Schema.Literals([
126
+ "function",
127
+ "class",
128
+ "variable",
129
+ "type",
130
+ "interface",
131
+ "enum",
132
+ "expression",
133
+ "other",
134
+ ]);
135
+
136
+ // What kind of name a rule is about. `members` are the names written in a
137
+ // named declaration — a type alias, an interface, a class body — under that
138
+ // declaration's name (a port's method vocabulary); `calls` are called
139
+ // identifiers (the hooks a tier may reach for). Which declarations a `members`
140
+ // rule speaks to is `declares`' to say, so the vocabulary carries no language's
141
+ // split between types and values.
142
+ const MemberSubject = Schema.Literals(["members", "calls"]);
143
+
144
+ // `source`, when present, is a snippet the loading adapter parses: the probe
145
+ // then holds only if a site named `name` comes out of the parser and the rule
146
+ // reports it — the declaration shape is the parser's to judge, not `in`'s.
147
+ // Without it the probe is a synthetic site of `name` inside `in`, declared as
148
+ // `declares`.
149
+ const MemberProbe = Schema.Struct({
150
+ from: Schema.String,
151
+ name: Schema.String,
152
+ in: Schema.optionalKey(Schema.String),
153
+ declares: Schema.optionalKey(DeclarationKind),
154
+ source: Schema.optionalKey(Schema.String),
155
+ });
156
+
157
+ // Which names a file is allowed to declare or call. This is the one family that
158
+ // needs no module resolution: it is about the vocabulary inside a file, not the
159
+ // edges leaving it.
160
+ export const MemberRule = Schema.Struct({
161
+ name: Schema.String,
162
+ message: Schema.String,
163
+ probe: MemberProbe,
164
+ from: PatternList,
165
+ fromNot: Schema.optionalKey(PatternList),
166
+ subject: MemberSubject,
167
+ // `members` only: which declaration's members are governed, by its name.
168
+ in: Schema.optionalKey(PatternList),
169
+ // `members` only: which kinds of declaration are governed. Omit for every
170
+ // kind — a type alias, an interface and a class body alike.
171
+ declares: Schema.optionalKey(Schema.Array(DeclarationKind)),
172
+ // Which names the rule speaks to at all. Omit for "every one".
173
+ match: Schema.optionalKey(PatternList),
174
+ matchNot: Schema.optionalKey(PatternList),
175
+ // Names that are fine. A name the rule speaks to and this does not admit is
176
+ // the violation.
177
+ allow: Schema.optionalKey(PatternList),
178
+ });
179
+
180
+ // One export site, as a probe states it: the name (`default` for a default
181
+ // export, `*` for `export *`), its binding kind, and optionally what it was
182
+ // declared as and whether it is a re-export.
183
+ const SurfaceSite = Schema.Struct({
184
+ name: Schema.String,
185
+ kind: BindingKind,
186
+ declares: Schema.optionalKey(DeclarationKind),
187
+ reexport: Schema.optionalKey(Schema.Boolean),
188
+ });
189
+
190
+ // A whole surface, because `count` is about the file rather than a site.
191
+ // `source`, when present, is parsed by the loading adapter instead.
192
+ const SurfaceProbe = Schema.Struct({
193
+ from: Schema.String,
194
+ sites: Schema.optionalKey(Schema.Array(SurfaceSite)),
195
+ source: Schema.optionalKey(Schema.String),
196
+ });
197
+
198
+ // What a file may export. The selectors (`kinds`, `declares`, `reexport`,
199
+ // `match`) say which sites the rule speaks to; exactly one demand says what is
200
+ // required of them. No demand means `forbid`: a selected site is the violation.
201
+ export const SurfaceRule = Schema.Struct({
202
+ name: Schema.String,
203
+ message: Schema.String,
204
+ probe: SurfaceProbe,
205
+ from: PatternList,
206
+ fromNot: Schema.optionalKey(PatternList),
207
+ kinds: Schema.optionalKey(Schema.Array(BindingKind)),
208
+ declares: Schema.optionalKey(Schema.Array(DeclarationKind)),
209
+ reexport: Schema.optionalKey(Schema.Boolean),
210
+ match: Schema.optionalKey(PatternList),
211
+ matchNot: Schema.optionalKey(PatternList),
212
+ forbid: Schema.optionalKey(Schema.Boolean),
213
+ // Names that are fine; a selected site named otherwise is the violation.
214
+ allow: Schema.optionalKey(PatternList),
215
+ // A regular-expression source every selected name must match.
216
+ convention: Schema.optionalKey(Schema.String),
217
+ // How many selected sites the file may have.
218
+ count: Schema.optionalKey(
219
+ Schema.Struct({
220
+ min: Schema.optionalKey(Schema.Finite),
221
+ max: Schema.optionalKey(Schema.Finite),
222
+ }),
223
+ ),
224
+ });
225
+
226
+ // A small synthetic graph the rule must report on: the edges, and any files
227
+ // that take part without an edge (an orphan has none).
228
+ const GraphProbe = Schema.Struct({
229
+ edges: Schema.Array(Schema.Tuple([Schema.String, Schema.String])),
230
+ files: Schema.optionalKey(Schema.Array(Schema.String)),
231
+ });
232
+
233
+ // Rules about the shape of the whole import graph, which no single file can
234
+ // answer. The CLI evaluates them; the plugin, which sees one file at a time,
235
+ // compiles and probes them so a vacuous one still fails to load.
236
+ export const GraphCycleRule = Schema.Struct({
237
+ name: Schema.String,
238
+ message: Schema.String,
239
+ probe: GraphProbe,
240
+ within: PatternList,
241
+ withinNot: Schema.optionalKey(PatternList),
242
+ });
243
+
244
+ export const GraphOrphanRule = Schema.Struct({
245
+ name: Schema.String,
246
+ message: Schema.String,
247
+ probe: GraphProbe,
248
+ within: PatternList,
249
+ withinNot: Schema.optionalKey(PatternList),
250
+ // Files that are imported by nothing by design — the program's entry points.
251
+ entry: PatternList,
252
+ });
253
+
254
+ export const GraphReachRule = Schema.Struct({
255
+ name: Schema.String,
256
+ message: Schema.String,
257
+ probe: GraphProbe,
258
+ from: PatternList,
259
+ fromNot: Schema.optionalKey(PatternList),
260
+ to: PatternList,
261
+ toNot: Schema.optionalKey(PatternList),
262
+ // The tier that was supposed to mediate: a path stepping onto a `via` file is
263
+ // allowed, so only a path that avoids every `via` is the violation.
264
+ via: Schema.optionalKey(PatternList),
265
+ });
266
+
267
+ export const GraphConfig = Schema.Struct({
268
+ cycles: Schema.optionalKey(Schema.Array(GraphCycleRule)),
269
+ orphans: Schema.optionalKey(Schema.Array(GraphOrphanRule)),
270
+ reach: Schema.optionalKey(Schema.Array(GraphReachRule)),
271
+ });
272
+
273
+ const PathProbe = Schema.Struct({ path: Schema.String });
274
+
275
+ // The file taxonomy, as three questions rather than one nested tree.
276
+ //
277
+ // `roots` marks the regions where layout is deny-by-default. `folders` says
278
+ // which basenames each folder admits. `parity` says which siblings a file owes.
279
+ // Keeping them apart is what removes the nested config's most fragile rule —
280
+ // that a specific pattern must beat a `*` catch-all — because an exemption is
281
+ // now a `fileNot` on the parity rule that would otherwise fire.
282
+ const StructureRoot = Schema.Struct({
283
+ name: Schema.String,
284
+ message: Schema.String,
285
+ probe: PathProbe,
286
+ // A file under this path whose folder no `folders` rule governs is a file in a
287
+ // folder the taxonomy does not know about.
288
+ path: PatternList,
289
+ });
290
+
291
+ const StructureFolder = Schema.Struct({
292
+ name: Schema.String,
293
+ message: Schema.String,
294
+ probe: PathProbe,
295
+ // Matched against the file's folder, repo-relative, with no trailing slash.
296
+ folder: PatternList,
297
+ // Basenames this folder admits. Anything else is the violation.
298
+ files: PatternList,
299
+ });
300
+
301
+ const StructureParity = Schema.Struct({
302
+ name: Schema.String,
303
+ message: Schema.String,
304
+ probe: PathProbe,
305
+ file: PatternList,
306
+ fileNot: Schema.optionalKey(PatternList),
307
+ // Paths that must exist, relative to the file's own folder. `{base}` is the
308
+ // filename minus its final extension — `create-todo.handler` for
309
+ // `create-todo.handler.ts`, `handler` for `handler.go` — so `{base}.test.ts`
310
+ // or `{base}_test.go` names the sibling test in either language.
311
+ requires: Schema.Array(Schema.String),
312
+ });
313
+
314
+ // What shape the variable part of a name may take. `folders` says which
315
+ // stereotypes a folder admits; this says what the concept name in front of the
316
+ // stereotype may look like — the degree of freedom a taxonomy alone leaves open.
317
+ const StructureNaming = Schema.Struct({
318
+ name: Schema.String,
319
+ message: Schema.String,
320
+ probe: PathProbe,
321
+ // Matched against the whole repo-relative path, and carrying capture groups:
322
+ // `subject` says which of them holds the name being judged.
323
+ file: PatternList,
324
+ fileNot: Schema.optionalKey(PatternList),
325
+ subject: Schema.Finite,
326
+ // The shape the subject must have. Exactly one of these.
327
+ convention: Schema.optionalKey(Schema.String),
328
+ // A capture group the subject must equal, for "named after its folder".
329
+ sameAs: Schema.optionalKey(Schema.Finite),
330
+ });
331
+
332
+ const StructureConfig = Schema.Struct({
333
+ roots: Schema.optionalKey(Schema.Array(StructureRoot)),
334
+ folders: Schema.optionalKey(Schema.Array(StructureFolder)),
335
+ parity: Schema.optionalKey(Schema.Array(StructureParity)),
336
+ naming: Schema.optionalKey(Schema.Array(StructureNaming)),
337
+ });
338
+
339
+ export type ImportRule = (typeof ImportRule)["Type"];
340
+ export type ResolveConfig = (typeof ResolveConfig)["Type"];
341
+ export type ResolveScope = (typeof ResolveScope)["Type"];
342
+ export type ImportProbe = (typeof ImportProbe)["Type"];
343
+ export type ImportProbeTarget = (typeof ImportProbeTarget)["Type"];
344
+ export type ExportRule = (typeof ExportRule)["Type"];
345
+ export type ExportProbe = (typeof ExportProbe)["Type"];
346
+ export type BindingKind = (typeof BindingKind)["Type"];
347
+ export type ExportFix = (typeof ExportFix)["Type"];
348
+ export type MemberRule = (typeof MemberRule)["Type"];
349
+ export type MemberProbe = (typeof MemberProbe)["Type"];
350
+ export type MemberSubject = (typeof MemberSubject)["Type"];
351
+ export type GraphProbe = (typeof GraphProbe)["Type"];
352
+ export type GraphCycleRule = (typeof GraphCycleRule)["Type"];
353
+ export type GraphOrphanRule = (typeof GraphOrphanRule)["Type"];
354
+ export type GraphReachRule = (typeof GraphReachRule)["Type"];
355
+ export type GraphConfig = (typeof GraphConfig)["Type"];
356
+ export type DeclarationKind = (typeof DeclarationKind)["Type"];
357
+ export type SurfaceRule = (typeof SurfaceRule)["Type"];
358
+ export type SurfaceProbe = (typeof SurfaceProbe)["Type"];
359
+ export type StructureConfig = (typeof StructureConfig)["Type"];
360
+ export type StructureRoot = (typeof StructureRoot)["Type"];
361
+ export type StructureFolder = (typeof StructureFolder)["Type"];
362
+ export type StructureParity = (typeof StructureParity)["Type"];
363
+ export type StructureNaming = (typeof StructureNaming)["Type"];
364
+
365
+ // The file pattern an open folder's layout rule carries: it admits any name,
366
+ // so it claims the folder without policing it. Coverage counts it apart.
367
+ export const OPEN_LAYOUT = "^.*$";
368
+
369
+ export const patternsOf = (patterns: string | ReadonlyArray<string>): ReadonlyArray<string> =>
370
+ typeof patterns === "string" ? [patterns] : patterns;
@@ -0,0 +1,59 @@
1
+ import * as Schema from "effect/Schema";
2
+
3
+ // These errors surface through oxlint's plugin loader, which prints a stack and
4
+ // nothing else. The `message` override is what turns "ConfigInvalid" into an
5
+ // instruction the reader can act on.
6
+
7
+ export class ConfigInvalid extends Schema.TaggedErrorClass<ConfigInvalid>("ConfigInvalid")(
8
+ "ConfigInvalid",
9
+ { configPath: Schema.String, detail: Schema.String },
10
+ ) {
11
+ override get message(): string {
12
+ return `${this.configPath}: ${this.detail}`;
13
+ }
14
+ }
15
+
16
+ export class PatternInvalid extends Schema.TaggedErrorClass<PatternInvalid>("PatternInvalid")(
17
+ "PatternInvalid",
18
+ {
19
+ ruleName: Schema.String,
20
+ field: Schema.String,
21
+ pattern: Schema.String,
22
+ detail: Schema.String,
23
+ },
24
+ ) {
25
+ override get message(): string {
26
+ return `rule "${this.ruleName}" has an uncompilable ${this.field} pattern ${JSON.stringify(
27
+ this.pattern,
28
+ )}: ${this.detail}`;
29
+ }
30
+ }
31
+
32
+ // A `resolve.scopes` entry a language pack cannot build a resolver from: options
33
+ // it does not understand, or a language no pack answers to. Raised by the pack,
34
+ // without the config path, which the loader adds when it reports it.
35
+ export class ScopeInvalid extends Schema.TaggedErrorClass<ScopeInvalid>("ScopeInvalid")(
36
+ "ScopeInvalid",
37
+ {
38
+ files: Schema.String,
39
+ language: Schema.String,
40
+ detail: Schema.String,
41
+ },
42
+ ) {
43
+ override get message(): string {
44
+ return `resolve scope ${JSON.stringify(this.files)} (${this.language}): ${this.detail}`;
45
+ }
46
+ }
47
+
48
+ export class ImportUnresolved extends Schema.TaggedErrorClass<ImportUnresolved>("ImportUnresolved")(
49
+ "ImportUnresolved",
50
+ {
51
+ fromFile: Schema.String,
52
+ specifier: Schema.String,
53
+ detail: Schema.String,
54
+ },
55
+ ) {
56
+ override get message(): string {
57
+ return `${this.fromFile} imports "${this.specifier}", which does not resolve: ${this.detail}`;
58
+ }
59
+ }
@@ -0,0 +1,47 @@
1
+ import type { BindingKind, DeclarationKind, MemberSubject } from "./architecture-config.js";
2
+
3
+ // What a policy can know about one source file, read once. Both adapters
4
+ // produce this vocabulary — the plugin from oxlint's syntax tree, the CLI from
5
+ // TypeScript's — which is what keeps them answerable to the same core rather
6
+ // than to each other. A rule is evaluated against these and nothing else.
7
+
8
+ // One name pulled across one import edge: `import { makeCommandBus } from "…"`
9
+ // is a single binding, and so is the `Effect` in `import { Effect } from "effect"`.
10
+ export type Binding = {
11
+ readonly symbol: string;
12
+ readonly kind: BindingKind;
13
+ };
14
+
15
+ // One declared or called name. For `members`, the declaration it is written in
16
+ // — its name and what it was declared as (`type`, `interface`, `class`); both
17
+ // absent for `calls`.
18
+ export type MemberSite = {
19
+ readonly file: string;
20
+ readonly subject: MemberSubject;
21
+ readonly name: string;
22
+ readonly in?: string;
23
+ readonly declares?: DeclarationKind;
24
+ };
25
+
26
+ // One name a file offers, at its top level: `default` for a default export,
27
+ // `*` for `export *`, the exported (not the local) name otherwise. `declares`
28
+ // is what it was declared as when the declaration is in this file, and `other`
29
+ // for a re-export, whose declaration is elsewhere.
30
+ export type ExportSite = {
31
+ readonly file: string;
32
+ readonly name: string;
33
+ readonly kind: BindingKind;
34
+ readonly declares: DeclarationKind;
35
+ readonly reexport: boolean;
36
+ };
37
+
38
+ export type SourceFacts = {
39
+ // One entry per import edge, in source order. An edge appears once even if it
40
+ // carries several bindings.
41
+ readonly specifiers: ReadonlyArray<string>;
42
+ // The names pulled across each edge, keyed by specifier.
43
+ readonly bindings: ReadonlyMap<string, ReadonlyArray<Binding>>;
44
+ readonly memberSites: ReadonlyArray<MemberSite>;
45
+ // In source order; a file's whole surface, top-level statements only.
46
+ readonly exportSites: ReadonlyArray<ExportSite>;
47
+ };
@@ -0,0 +1,24 @@
1
+ export type ViolationKind = "import" | "export" | "structure" | "member" | "surface" | "graph";
2
+
3
+ export type Violation = {
4
+ readonly kind: ViolationKind;
5
+ readonly ruleName: string;
6
+ readonly message: string;
7
+ // Repo-relative, forward slashes.
8
+ readonly file: string;
9
+ // The other end of the violated relationship: the resolved import target, the
10
+ // restricted symbol, the missing sibling, the disallowed member. `null` when
11
+ // the file alone is the violation.
12
+ readonly subject: string | null;
13
+ };
14
+
15
+ // Deliberately excludes line and column so an entry survives ordinary edits to
16
+ // the file it names — a baseline keyed on positions would go stale on the first
17
+ // reformat and silently re-admit a violation it was never meant to cover.
18
+ export const fingerprintOf = (violation: Violation): string =>
19
+ [violation.kind, violation.ruleName, violation.file, violation.subject ?? ""].join("|");
20
+
21
+ // `{name}` in a message is replaced with the subject, so a rule can quote the
22
+ // offending symbol without the config needing a callback.
23
+ export const formatMessage = (violation: Violation): string =>
24
+ `[${violation.ruleName}] ${violation.message.replaceAll("{name}", violation.subject ?? "")}`;
package/src/index.ts ADDED
@@ -0,0 +1,136 @@
1
+ // The core's public surface: the manifest vocabulary, the evaluators per
2
+ // family, the ports a language pack implements, and the loader that turns a
3
+ // manifest into a policy. A host composes these with a language pack and the
4
+ // live file system; a language pack implements the ports. Nothing here names a
5
+ // language. The fakes are under `@goodbones/core/testing`.
6
+ export {
7
+ type Baseline,
8
+ type BaselineFilter,
9
+ baselineOf,
10
+ decodeBaseline,
11
+ EMPTY_BASELINE,
12
+ makeBaselineFilter,
13
+ serializeBaseline,
14
+ staleEntriesOf,
15
+ unbaselined,
16
+ } from "./core/baseline.js";
17
+ export {
18
+ type Coverage,
19
+ type CoverageFamily,
20
+ type CoverageFloors,
21
+ coverageOf,
22
+ coverageShortfalls,
23
+ fractionsOf,
24
+ } from "./core/coverage.js";
25
+ export {
26
+ type BindingEdge,
27
+ type CompiledExportRule,
28
+ compileExportRule,
29
+ compileExportRules,
30
+ evaluateBindingEdge,
31
+ evaluateSelectedBindings,
32
+ exportRulesFailingTheirProbe,
33
+ exportRulesSelecting,
34
+ type ExportViolation,
35
+ type SelectedExportRule,
36
+ } from "./core/exports.js";
37
+ export {
38
+ type CompiledGraph,
39
+ compileGraphRules,
40
+ EMPTY_GRAPH_RULES,
41
+ evaluateGraph,
42
+ type Graph,
43
+ graphRulesFailingTheirProbe,
44
+ hasGraphRules,
45
+ } from "./core/graph.js";
46
+ export {
47
+ type CompiledImportRule,
48
+ compileImportRule,
49
+ compileImportRules,
50
+ evaluateImportEdge,
51
+ evaluateSelectedEdge,
52
+ type ImportEdge,
53
+ probeTargetOf,
54
+ rulesFailingTheirProbe,
55
+ rulesSelecting,
56
+ type SelectedRule,
57
+ } from "./core/imports.js";
58
+ export {
59
+ type CompiledMemberRule,
60
+ compileMemberRules,
61
+ evaluateMemberSite,
62
+ memberRulesFailingTheirProbe,
63
+ memberRulesSelecting,
64
+ } from "./core/members.js";
65
+ export {
66
+ type CompiledStructure,
67
+ compileStructure,
68
+ EMPTY_STRUCTURE,
69
+ evaluateStructure,
70
+ requiredSiblingsOf,
71
+ structureRulesFailingTheirProbe,
72
+ } from "./core/structure.js";
73
+ export {
74
+ type CompiledSurfaceRule,
75
+ compileSurfaceRules,
76
+ evaluateSurface,
77
+ surfaceRulesFailingTheirProbe,
78
+ surfaceRulesSelecting,
79
+ } from "./core/surface.js";
80
+ export {
81
+ type BindingKind,
82
+ type DeclarationKind,
83
+ type ExportFix,
84
+ type ExportRule,
85
+ type GraphConfig,
86
+ type GraphCycleRule,
87
+ type GraphOrphanRule,
88
+ type GraphReachRule,
89
+ type ImportProbe,
90
+ type ImportProbeTarget,
91
+ type ImportRule,
92
+ type MemberRule,
93
+ type MemberSubject,
94
+ type ResolveConfig,
95
+ type ResolveScope,
96
+ type StructureConfig,
97
+ type SurfaceRule,
98
+ } from "./domain/architecture-config.js";
99
+ export {
100
+ ConfigInvalid,
101
+ ImportUnresolved,
102
+ PatternInvalid,
103
+ ScopeInvalid,
104
+ } from "./domain/architecture-error.js";
105
+ export {
106
+ type Binding,
107
+ type ExportSite,
108
+ type MemberSite,
109
+ type SourceFacts,
110
+ } from "./domain/facts.js";
111
+ export {
112
+ fingerprintOf,
113
+ formatMessage,
114
+ type Violation,
115
+ type ViolationKind,
116
+ } from "./domain/violation.js";
117
+ export { makeFileSystemLive } from "./infrastructure/file-system-live.js";
118
+ export { DEFAULT_CONFIG_FILENAME, readManifestFile } from "./infrastructure/manifest-file.js";
119
+ export { listSourceFiles, type WalkedLanguage } from "./infrastructure/walk.js";
120
+ export { type LoadedPolicy, loadPolicy, type LoadPolicyInput } from "./load/policy.js";
121
+ export { type LoweredRules, lowerManifest, type ProbeLanguage } from "./manifest/compile.js";
122
+ export {
123
+ type DecodedManifest,
124
+ decodeManifest,
125
+ type Manifest,
126
+ type ManifestNode,
127
+ Manifest as ManifestSchema,
128
+ } from "./manifest/manifest.js";
129
+ export { type FactExtractor } from "./ports/fact-extractor.js";
130
+ export { type FileSystem } from "./ports/file-system.js";
131
+ export { type Language } from "./ports/language.js";
132
+ export {
133
+ type DependencyKind,
134
+ type ModuleResolver,
135
+ type ResolvedTarget,
136
+ } from "./ports/module-resolver.js";
@@ -0,0 +1,28 @@
1
+ import type { SourceFacts } from "../domain/facts.js";
2
+ import type { FactExtractor } from "../ports/fact-extractor.js";
3
+
4
+ const NOTHING: SourceFacts = {
5
+ specifiers: [],
6
+ bindings: new Map(),
7
+ memberSites: [],
8
+ exportSites: [],
9
+ };
10
+
11
+ // Keyed by the source text itself: a core test states what a snippet reads as
12
+ // and never the parse, which has its own tests against the real extractor. A
13
+ // snippet not staged reads as nothing — the shape of a form the extractor is
14
+ // blind to, which is the case a source probe exists to catch.
15
+ export const makeFactExtractorFake = (
16
+ facts: Readonly<Record<string, Partial<SourceFacts>>>,
17
+ ): FactExtractor => ({
18
+ factsOf: (file, text) => {
19
+ const found = facts[text];
20
+ if (found === undefined) return NOTHING;
21
+ return {
22
+ ...NOTHING,
23
+ ...found,
24
+ memberSites: (found.memberSites ?? []).map((site) => ({ ...site, file })),
25
+ exportSites: (found.exportSites ?? []).map((site) => ({ ...site, file })),
26
+ };
27
+ },
28
+ });
@@ -0,0 +1,14 @@
1
+ import type { FileSystem } from "../ports/file-system.js";
2
+
3
+ // The files that exist, and optionally what some of them say. A file staged
4
+ // with contents exists; one staged by name alone exists and reads as nothing.
5
+ export const makeFileSystemFake = (
6
+ present: Iterable<string>,
7
+ contents: Readonly<Record<string, string>> = {},
8
+ ): FileSystem => {
9
+ const paths = new Set([...present, ...Object.keys(contents)]);
10
+ return {
11
+ exists: (repoRelativePath) => paths.has(repoRelativePath),
12
+ readText: (repoRelativePath) => contents[repoRelativePath] ?? null,
13
+ };
14
+ };