archcheck 0.2.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 (251) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +536 -0
  3. package/dist/archcheck.d.ts +99 -0
  4. package/dist/archcheck.d.ts.map +1 -0
  5. package/dist/archcheck.js +339 -0
  6. package/dist/archcheck.js.map +1 -0
  7. package/dist/baseline/index.d.ts +29 -0
  8. package/dist/baseline/index.d.ts.map +1 -0
  9. package/dist/baseline/index.js +116 -0
  10. package/dist/baseline/index.js.map +1 -0
  11. package/dist/cli/args.d.ts +36 -0
  12. package/dist/cli/args.d.ts.map +1 -0
  13. package/dist/cli/args.js +233 -0
  14. package/dist/cli/args.js.map +1 -0
  15. package/dist/cli/index.d.ts +4 -0
  16. package/dist/cli/index.d.ts.map +1 -0
  17. package/dist/cli/index.js +219 -0
  18. package/dist/cli/index.js.map +1 -0
  19. package/dist/cli/version.d.ts +2 -0
  20. package/dist/cli/version.d.ts.map +1 -0
  21. package/dist/cli/version.js +29 -0
  22. package/dist/cli/version.js.map +1 -0
  23. package/dist/contracts/banned-syntax.d.ts +48 -0
  24. package/dist/contracts/banned-syntax.d.ts.map +1 -0
  25. package/dist/contracts/banned-syntax.js +201 -0
  26. package/dist/contracts/banned-syntax.js.map +1 -0
  27. package/dist/contracts/declared-field.d.ts +39 -0
  28. package/dist/contracts/declared-field.d.ts.map +1 -0
  29. package/dist/contracts/declared-field.js +142 -0
  30. package/dist/contracts/declared-field.js.map +1 -0
  31. package/dist/contracts/encapsulation.d.ts +28 -0
  32. package/dist/contracts/encapsulation.d.ts.map +1 -0
  33. package/dist/contracts/encapsulation.js +130 -0
  34. package/dist/contracts/encapsulation.js.map +1 -0
  35. package/dist/contracts/index.d.ts +10 -0
  36. package/dist/contracts/index.d.ts.map +1 -0
  37. package/dist/contracts/index.js +9 -0
  38. package/dist/contracts/index.js.map +1 -0
  39. package/dist/contracts/judgement.d.ts +21 -0
  40. package/dist/contracts/judgement.d.ts.map +1 -0
  41. package/dist/contracts/judgement.js +109 -0
  42. package/dist/contracts/judgement.js.map +1 -0
  43. package/dist/contracts/layers.d.ts +75 -0
  44. package/dist/contracts/layers.d.ts.map +1 -0
  45. package/dist/contracts/layers.js +244 -0
  46. package/dist/contracts/layers.js.map +1 -0
  47. package/dist/contracts/naming.d.ts +21 -0
  48. package/dist/contracts/naming.d.ts.map +1 -0
  49. package/dist/contracts/naming.js +120 -0
  50. package/dist/contracts/naming.js.map +1 -0
  51. package/dist/contracts/registry.d.ts +23 -0
  52. package/dist/contracts/registry.d.ts.map +1 -0
  53. package/dist/contracts/registry.js +48 -0
  54. package/dist/contracts/registry.js.map +1 -0
  55. package/dist/contracts/required-file.d.ts +29 -0
  56. package/dist/contracts/required-file.d.ts.map +1 -0
  57. package/dist/contracts/required-file.js +131 -0
  58. package/dist/contracts/required-file.js.map +1 -0
  59. package/dist/contracts/types.d.ts +61 -0
  60. package/dist/contracts/types.d.ts.map +1 -0
  61. package/dist/contracts/types.js +2 -0
  62. package/dist/contracts/types.js.map +1 -0
  63. package/dist/coverage/index.d.ts +55 -0
  64. package/dist/coverage/index.d.ts.map +1 -0
  65. package/dist/coverage/index.js +68 -0
  66. package/dist/coverage/index.js.map +1 -0
  67. package/dist/emit/collect.d.ts +20 -0
  68. package/dist/emit/collect.d.ts.map +1 -0
  69. package/dist/emit/collect.js +17 -0
  70. package/dist/emit/collect.js.map +1 -0
  71. package/dist/emit/header.d.ts +21 -0
  72. package/dist/emit/header.d.ts.map +1 -0
  73. package/dist/emit/header.js +20 -0
  74. package/dist/emit/header.js.map +1 -0
  75. package/dist/emit/index.d.ts +57 -0
  76. package/dist/emit/index.d.ts.map +1 -0
  77. package/dist/emit/index.js +128 -0
  78. package/dist/emit/index.js.map +1 -0
  79. package/dist/emit/merge.d.ts +23 -0
  80. package/dist/emit/merge.d.ts.map +1 -0
  81. package/dist/emit/merge.js +71 -0
  82. package/dist/emit/merge.js.map +1 -0
  83. package/dist/emit/write.d.ts +20 -0
  84. package/dist/emit/write.d.ts.map +1 -0
  85. package/dist/emit/write.js +61 -0
  86. package/dist/emit/write.js.map +1 -0
  87. package/dist/exceptions/index.d.ts +46 -0
  88. package/dist/exceptions/index.d.ts.map +1 -0
  89. package/dist/exceptions/index.js +117 -0
  90. package/dist/exceptions/index.js.map +1 -0
  91. package/dist/fs/index.d.ts +12 -0
  92. package/dist/fs/index.d.ts.map +1 -0
  93. package/dist/fs/index.js +78 -0
  94. package/dist/fs/index.js.map +1 -0
  95. package/dist/graph/cache.d.ts +12 -0
  96. package/dist/graph/cache.d.ts.map +1 -0
  97. package/dist/graph/cache.js +76 -0
  98. package/dist/graph/cache.js.map +1 -0
  99. package/dist/graph/edges.d.ts +16 -0
  100. package/dist/graph/edges.d.ts.map +1 -0
  101. package/dist/graph/edges.js +161 -0
  102. package/dist/graph/edges.js.map +1 -0
  103. package/dist/graph/index.d.ts +15 -0
  104. package/dist/graph/index.d.ts.map +1 -0
  105. package/dist/graph/index.js +90 -0
  106. package/dist/graph/index.js.map +1 -0
  107. package/dist/graph/match.d.ts +12 -0
  108. package/dist/graph/match.d.ts.map +1 -0
  109. package/dist/graph/match.js +35 -0
  110. package/dist/graph/match.js.map +1 -0
  111. package/dist/graph/program.d.ts +12 -0
  112. package/dist/graph/program.d.ts.map +1 -0
  113. package/dist/graph/program.js +54 -0
  114. package/dist/graph/program.js.map +1 -0
  115. package/dist/graph/types.d.ts +55 -0
  116. package/dist/graph/types.d.ts.map +1 -0
  117. package/dist/graph/types.js +2 -0
  118. package/dist/graph/types.js.map +1 -0
  119. package/dist/index.d.ts +21 -0
  120. package/dist/index.d.ts.map +1 -0
  121. package/dist/index.js +20 -0
  122. package/dist/index.js.map +1 -0
  123. package/dist/init/index.d.ts +23 -0
  124. package/dist/init/index.d.ts.map +1 -0
  125. package/dist/init/index.js +218 -0
  126. package/dist/init/index.js.map +1 -0
  127. package/dist/normalize/identity.d.ts +4 -0
  128. package/dist/normalize/identity.d.ts.map +1 -0
  129. package/dist/normalize/identity.js +33 -0
  130. package/dist/normalize/identity.js.map +1 -0
  131. package/dist/normalize/index.d.ts +3 -0
  132. package/dist/normalize/index.d.ts.map +1 -0
  133. package/dist/normalize/index.js +3 -0
  134. package/dist/normalize/index.js.map +1 -0
  135. package/dist/normalize/normalize.d.ts +34 -0
  136. package/dist/normalize/normalize.d.ts.map +1 -0
  137. package/dist/normalize/normalize.js +91 -0
  138. package/dist/normalize/normalize.js.map +1 -0
  139. package/dist/probes/graph.d.ts +19 -0
  140. package/dist/probes/graph.d.ts.map +1 -0
  141. package/dist/probes/graph.js +181 -0
  142. package/dist/probes/graph.js.map +1 -0
  143. package/dist/probes/index.d.ts +5 -0
  144. package/dist/probes/index.d.ts.map +1 -0
  145. package/dist/probes/index.js +5 -0
  146. package/dist/probes/index.js.map +1 -0
  147. package/dist/probes/lint.d.ts +17 -0
  148. package/dist/probes/lint.d.ts.map +1 -0
  149. package/dist/probes/lint.js +121 -0
  150. package/dist/probes/lint.js.map +1 -0
  151. package/dist/probes/model/anthropic.d.ts +47 -0
  152. package/dist/probes/model/anthropic.d.ts.map +1 -0
  153. package/dist/probes/model/anthropic.js +100 -0
  154. package/dist/probes/model/anthropic.js.map +1 -0
  155. package/dist/probes/model/budget.d.ts +22 -0
  156. package/dist/probes/model/budget.d.ts.map +1 -0
  157. package/dist/probes/model/budget.js +27 -0
  158. package/dist/probes/model/budget.js.map +1 -0
  159. package/dist/probes/model/cache.d.ts +13 -0
  160. package/dist/probes/model/cache.d.ts.map +1 -0
  161. package/dist/probes/model/cache.js +74 -0
  162. package/dist/probes/model/cache.js.map +1 -0
  163. package/dist/probes/model/index.d.ts +20 -0
  164. package/dist/probes/model/index.d.ts.map +1 -0
  165. package/dist/probes/model/index.js +160 -0
  166. package/dist/probes/model/index.js.map +1 -0
  167. package/dist/probes/model/prompt.d.ts +16 -0
  168. package/dist/probes/model/prompt.d.ts.map +1 -0
  169. package/dist/probes/model/prompt.js +57 -0
  170. package/dist/probes/model/prompt.js.map +1 -0
  171. package/dist/probes/model/provider.d.ts +88 -0
  172. package/dist/probes/model/provider.d.ts.map +1 -0
  173. package/dist/probes/model/provider.js +48 -0
  174. package/dist/probes/model/provider.js.map +1 -0
  175. package/dist/probes/structure.d.ts +3 -0
  176. package/dist/probes/structure.d.ts.map +1 -0
  177. package/dist/probes/structure.js +95 -0
  178. package/dist/probes/structure.js.map +1 -0
  179. package/dist/probes/types.d.ts +46 -0
  180. package/dist/probes/types.d.ts.map +1 -0
  181. package/dist/probes/types.js +15 -0
  182. package/dist/probes/types.js.map +1 -0
  183. package/dist/registry/define.d.ts +40 -0
  184. package/dist/registry/define.d.ts.map +1 -0
  185. package/dist/registry/define.js +4 -0
  186. package/dist/registry/define.js.map +1 -0
  187. package/dist/registry/ids.d.ts +19 -0
  188. package/dist/registry/ids.d.ts.map +1 -0
  189. package/dist/registry/ids.js +86 -0
  190. package/dist/registry/ids.js.map +1 -0
  191. package/dist/registry/index.d.ts +45 -0
  192. package/dist/registry/index.d.ts.map +1 -0
  193. package/dist/registry/index.js +71 -0
  194. package/dist/registry/index.js.map +1 -0
  195. package/dist/registry/load.d.ts +14 -0
  196. package/dist/registry/load.d.ts.map +1 -0
  197. package/dist/registry/load.js +108 -0
  198. package/dist/registry/load.js.map +1 -0
  199. package/dist/registry/validate.d.ts +24 -0
  200. package/dist/registry/validate.d.ts.map +1 -0
  201. package/dist/registry/validate.js +160 -0
  202. package/dist/registry/validate.js.map +1 -0
  203. package/dist/report/index.d.ts +10 -0
  204. package/dist/report/index.d.ts.map +1 -0
  205. package/dist/report/index.js +30 -0
  206. package/dist/report/index.js.map +1 -0
  207. package/dist/report/json.d.ts +20 -0
  208. package/dist/report/json.d.ts.map +1 -0
  209. package/dist/report/json.js +34 -0
  210. package/dist/report/json.js.map +1 -0
  211. package/dist/report/sarif.d.ts +21 -0
  212. package/dist/report/sarif.d.ts.map +1 -0
  213. package/dist/report/sarif.js +138 -0
  214. package/dist/report/sarif.js.map +1 -0
  215. package/dist/report/terminal.d.ts +3 -0
  216. package/dist/report/terminal.d.ts.map +1 -0
  217. package/dist/report/terminal.js +117 -0
  218. package/dist/report/terminal.js.map +1 -0
  219. package/dist/report/types.d.ts +28 -0
  220. package/dist/report/types.d.ts.map +1 -0
  221. package/dist/report/types.js +11 -0
  222. package/dist/report/types.js.map +1 -0
  223. package/dist/report/verdict.d.ts +21 -0
  224. package/dist/report/verdict.d.ts.map +1 -0
  225. package/dist/report/verdict.js +31 -0
  226. package/dist/report/verdict.js.map +1 -0
  227. package/dist/result.d.ts +22 -0
  228. package/dist/result.d.ts.map +1 -0
  229. package/dist/result.js +19 -0
  230. package/dist/result.js.map +1 -0
  231. package/dist/types.d.ts +178 -0
  232. package/dist/types.d.ts.map +1 -0
  233. package/dist/types.js +11 -0
  234. package/dist/types.js.map +1 -0
  235. package/dist/util/compilerApi.d.ts +2 -0
  236. package/dist/util/compilerApi.d.ts.map +1 -0
  237. package/dist/util/compilerApi.js +28 -0
  238. package/dist/util/compilerApi.js.map +1 -0
  239. package/dist/util/glob.d.ts +43 -0
  240. package/dist/util/glob.d.ts.map +1 -0
  241. package/dist/util/glob.js +195 -0
  242. package/dist/util/glob.js.map +1 -0
  243. package/dist/util/paths.d.ts +6 -0
  244. package/dist/util/paths.d.ts.map +1 -0
  245. package/dist/util/paths.js +22 -0
  246. package/dist/util/paths.js.map +1 -0
  247. package/dist/version.d.ts +13 -0
  248. package/dist/version.d.ts.map +1 -0
  249. package/dist/version.js +12 -0
  250. package/dist/version.js.map +1 -0
  251. package/package.json +72 -0
@@ -0,0 +1,128 @@
1
+ /**
2
+ * The emit pipeline: collect → merge → write (or check).
3
+ *
4
+ * This is the half of the design that makes editor feedback work WITHOUT archcheck running. The
5
+ * emitted lint message carries the rule's own sentence, so the squiggle reads
6
+ * "domain may not import services — invert the call" rather than a rule name (LLD §1.1).
7
+ */
8
+ import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
9
+ import { dirname, join } from 'node:path';
10
+ import { implementedKinds } from '../contracts/index.js';
11
+ import { RULES_JSON, renderRulesTable, replaceFencedRegion, rulesDocument, } from '../coverage/index.js';
12
+ import { ID_LEDGER_PATH } from '../registry/ids.js';
13
+ import { repoRelative } from '../util/paths.js';
14
+ import { collectFragments } from './collect.js';
15
+ import { mergeBySink } from './merge.js';
16
+ import { checkSinks, writeSinks, } from './write.js';
17
+ export const DEFAULT_GENERATED_DIR = 'generated';
18
+ export function planEmit(options) {
19
+ const ctx = { root: options.root, rules: options.rules };
20
+ const { fragments, unimplemented } = collectFragments(options.rules, options.contractFor, ctx);
21
+ const source = repoRelative(options.root, options.configPath);
22
+ const files = mergeBySink(fragments, source);
23
+ // rules.json is DATA, so any document — or a dashboard later — can read it without archcheck
24
+ // knowing that document's format.
25
+ const document = rulesDocument(options.rules, options.coverage);
26
+ files.push({
27
+ path: RULES_JSON,
28
+ target: 'test',
29
+ content: `${JSON.stringify(document, null, 2)}\n`,
30
+ ruleIds: document.rules.map((rule) => rule.id),
31
+ });
32
+ files.sort((a, b) => a.path.localeCompare(b.path));
33
+ const table = renderRulesTable(document);
34
+ const docs = [];
35
+ const unreadableDocs = [];
36
+ for (const relative of options.docs ?? []) {
37
+ const existing = readDocument(options.root, relative);
38
+ if (existing === undefined) {
39
+ unreadableDocs.push(relative);
40
+ continue;
41
+ }
42
+ const replaced = replaceFencedRegion(existing, table);
43
+ // No fence means the document never asked for a generated region. Not an error, and not a
44
+ // reason to grow one by surprise.
45
+ if (replaced !== undefined)
46
+ docs.push({ path: relative, content: replaced });
47
+ }
48
+ return {
49
+ files,
50
+ unimplemented,
51
+ target: { root: options.root, generatedDir: options.generatedDir ?? DEFAULT_GENERATED_DIR },
52
+ docs,
53
+ unreadableDocs,
54
+ };
55
+ }
56
+ /**
57
+ * What `emit` says about a rule kind it generated no configuration for.
58
+ *
59
+ * `emit` is its own command: a CI lane that runs only `archcheck emit` sees neither
60
+ * `RunResult.skipped` nor `Coverage.unchecked`, so without this line a `forbidden` rule emits no
61
+ * fragment, prints nothing, and the editor configs the rule was declared for silently lack it.
62
+ * Same fact `collectFragments` records as `EmitPlan.unimplemented`, read from the rules alone
63
+ * because the CLI holds no plan. `src/emit/emit.test.ts` → 'describeUnimplemented' pins it.
64
+ */
65
+ export function describeUnimplemented(rules) {
66
+ const implemented = new Set(implementedKinds());
67
+ const kinds = [
68
+ ...new Set(rules.filter((rule) => rule.severity !== 'off').map((rule) => rule.kind)),
69
+ ]
70
+ .filter((kind) => !implemented.has(kind))
71
+ .sort();
72
+ if (kinds.length === 0)
73
+ return [];
74
+ const named = kinds.map((kind) => `'${kind}'`).join(', ');
75
+ return [
76
+ `warning: no configuration was generated for ${named} — no contract type implements ` +
77
+ `${kinds.length === 1 ? 'that kind' : 'those kinds'} yet`,
78
+ ' → those rules are declared, not enforced: `archcheck` reports them under skipped',
79
+ ];
80
+ }
81
+ function readDocument(root, relative) {
82
+ try {
83
+ return readFileSync(join(root, relative), 'utf8');
84
+ }
85
+ catch {
86
+ // Undefined means "could not read it", and whether that is news belongs to the caller:
87
+ // `planEmit` reports a document the manifest named, `applyEmit` and `checkEmit` treat an
88
+ // unreadable file as "differs from what we would write".
89
+ return undefined;
90
+ }
91
+ }
92
+ /** Writes the sinks, and the id ledger that makes rule ids immutable. */
93
+ export function applyEmit(plan, ledger) {
94
+ const written = writeSinks(plan.target, plan.files);
95
+ for (const doc of plan.docs) {
96
+ const path = join(plan.target.root, doc.path);
97
+ const current = readDocument(plan.target.root, doc.path);
98
+ if (current === doc.content) {
99
+ written.push({ path: doc.path, changed: false });
100
+ continue;
101
+ }
102
+ writeFileSync(path, doc.content);
103
+ written.push({ path: doc.path, changed: true });
104
+ }
105
+ const ledgerPath = join(plan.target.root, ID_LEDGER_PATH);
106
+ const content = `${JSON.stringify(ledger, null, 2)}\n`;
107
+ mkdirSync(dirname(ledgerPath), { recursive: true });
108
+ writeFileSync(ledgerPath, content);
109
+ return { written, ledgerPath: ID_LEDGER_PATH };
110
+ }
111
+ export function checkEmit(plan) {
112
+ const stale = checkSinks(plan.target, plan.files);
113
+ for (const path of plan.unreadableDocs)
114
+ stale.push({ path, reason: 'unreadable' });
115
+ for (const doc of plan.docs) {
116
+ // Hand-editing inside the fence is the failure this catches: the region is generated, so a
117
+ // human edit there is lost on the next emit and disagrees with the manifest until then.
118
+ if (readDocument(plan.target.root, doc.path) !== doc.content) {
119
+ stale.push({ path: doc.path, reason: 'hand-edited' });
120
+ }
121
+ }
122
+ return stale;
123
+ }
124
+ export { collectFragments } from './collect.js';
125
+ export { commentHeader, generatedMeta, REGENERATE_COMMAND } from './header.js';
126
+ export { mergeBySink, SINK_PATHS, sortDeep } from './merge.js';
127
+ export { checkSinks, describeStale, writeSinks, } from './write.js';
128
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/emit/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Merging fragments into one file per sink.
3
+ *
4
+ * What rule this enforces: deterministic output. Keys are sorted at every depth and entries are
5
+ * sorted by rule id, so the same manifest produces the same bytes on every machine and every run.
6
+ * Without that property `emit --check` fails at random, someone disables it, and the generated
7
+ * files start drifting from the manifest — which is the one failure this whole pipeline exists to
8
+ * prevent.
9
+ */
10
+ import type { Fragment, FragmentTarget } from '../types.js';
11
+ /** Where each sink lands. One file per sink, always at the same path. */
12
+ export declare const SINK_PATHS: Record<FragmentTarget, string>;
13
+ export type SinkFile = {
14
+ /** Relative to the generated directory. */
15
+ path: string;
16
+ target: FragmentTarget;
17
+ content: string;
18
+ ruleIds: string[];
19
+ };
20
+ /** Sort every object key at every depth. JSON.stringify preserves insertion order. */
21
+ export declare function sortDeep(value: unknown): unknown;
22
+ export declare function mergeBySink(fragments: readonly Fragment[], source: string): SinkFile[];
23
+ //# sourceMappingURL=merge.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"merge.d.ts","sourceRoot":"","sources":["../../src/emit/merge.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,KAAK,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAG5D,yEAAyE;AACzE,eAAO,MAAM,UAAU,EAAE,MAAM,CAAC,cAAc,EAAE,MAAM,CAMrD,CAAC;AAEF,MAAM,MAAM,QAAQ,GAAG;IACrB,2CAA2C;IAC3C,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,cAAc,CAAC;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB,CAAC;AAEF,sFAAsF;AACtF,wBAAgB,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAShD;AAiCD,wBAAgB,WAAW,CAAC,SAAS,EAAE,SAAS,QAAQ,EAAE,EAAE,MAAM,EAAE,MAAM,GAAG,QAAQ,EAAE,CAetF"}
@@ -0,0 +1,71 @@
1
+ import { commentHeader, generatedMeta } from './header.js';
2
+ /** Where each sink lands. One file per sink, always at the same path. */
3
+ export const SINK_PATHS = {
4
+ graph: 'graph.config.cjs',
5
+ lint: 'lint.overrides.json',
6
+ boundaries: 'boundaries.json',
7
+ test: 'required.json',
8
+ exports: 'exports.json',
9
+ };
10
+ /** Sort every object key at every depth. JSON.stringify preserves insertion order. */
11
+ export function sortDeep(value) {
12
+ if (Array.isArray(value))
13
+ return value.map(sortDeep);
14
+ if (value === null || typeof value !== 'object')
15
+ return value;
16
+ const out = {};
17
+ for (const key of Object.keys(value).sort()) {
18
+ const nested = value[key];
19
+ if (nested !== undefined)
20
+ out[key] = sortDeep(nested);
21
+ }
22
+ return out;
23
+ }
24
+ function jsonSink(target, fragments, source) {
25
+ const ruleIds = [...new Set(fragments.map((f) => f.ruleId))].sort();
26
+ const body = {
27
+ $generated: generatedMeta(source, ruleIds),
28
+ rules: fragments
29
+ .map((fragment) => ({ ruleId: fragment.ruleId, ...fragment.content }))
30
+ .sort((a, b) => a.ruleId.localeCompare(b.ruleId)),
31
+ };
32
+ return {
33
+ path: SINK_PATHS[target],
34
+ target,
35
+ content: `${JSON.stringify(sortDeep(body), null, 2)}\n`,
36
+ ruleIds,
37
+ };
38
+ }
39
+ function graphSink(fragments, source) {
40
+ const ruleIds = [...new Set(fragments.map((f) => f.ruleId))].sort();
41
+ const layers = fragments
42
+ .map((fragment) => ({ ruleId: fragment.ruleId, ...fragment.content }))
43
+ .sort((a, b) => a.ruleId.localeCompare(b.ruleId));
44
+ const body = JSON.stringify(sortDeep({ layers }), null, 2);
45
+ return {
46
+ path: SINK_PATHS.graph,
47
+ target: 'graph',
48
+ // CommonJS because this is the shape a module-graph tool's config expects to require().
49
+ content: `${commentHeader(source, ruleIds)}\n\nmodule.exports = ${body};\n`,
50
+ ruleIds,
51
+ };
52
+ }
53
+ export function mergeBySink(fragments, source) {
54
+ const byTarget = new Map();
55
+ for (const fragment of fragments) {
56
+ const existing = byTarget.get(fragment.target);
57
+ if (existing)
58
+ existing.push(fragment);
59
+ else
60
+ byTarget.set(fragment.target, [fragment]);
61
+ }
62
+ const files = [];
63
+ for (const target of Object.keys(SINK_PATHS).sort()) {
64
+ const group = byTarget.get(target);
65
+ if (!group || group.length === 0)
66
+ continue;
67
+ files.push(target === 'graph' ? graphSink(group, source) : jsonSink(target, group, source));
68
+ }
69
+ return files.sort((a, b) => a.path.localeCompare(b.path));
70
+ }
71
+ //# sourceMappingURL=merge.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"merge.js","sourceRoot":"","sources":["../../src/emit/merge.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,20 @@
1
+ import type { SinkFile } from './merge.js';
2
+ export type EmitTarget = {
3
+ /** Absolute path of the repository root. */
4
+ root: string;
5
+ /** Relative to root, e.g. `generated`. */
6
+ generatedDir: string;
7
+ };
8
+ export type WrittenFile = {
9
+ path: string;
10
+ changed: boolean;
11
+ };
12
+ export declare function writeSinks(target: EmitTarget, files: readonly SinkFile[]): WrittenFile[];
13
+ export type StaleFile = {
14
+ path: string;
15
+ /** `unreadable` is a document `manifest.docs` names that is not there — see `planEmit`. */
16
+ reason: 'missing' | 'hand-edited' | 'unreadable';
17
+ };
18
+ export declare function checkSinks(target: EmitTarget, files: readonly SinkFile[]): StaleFile[];
19
+ export declare function describeStale(stale: readonly StaleFile[]): string[];
20
+ //# sourceMappingURL=write.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"write.d.ts","sourceRoot":"","sources":["../../src/emit/write.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAE3C,MAAM,MAAM,UAAU,GAAG;IACvB,4CAA4C;IAC5C,IAAI,EAAE,MAAM,CAAC;IACb,0CAA0C;IAC1C,YAAY,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,CAAC;AAc7D,wBAAgB,UAAU,CAAC,MAAM,EAAE,UAAU,EAAE,KAAK,EAAE,SAAS,QAAQ,EAAE,GAAG,WAAW,EAAE,CAaxF;AAED,MAAM,MAAM,SAAS,GAAG;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,2FAA2F;IAC3F,MAAM,EAAE,SAAS,GAAG,aAAa,GAAG,YAAY,CAAC;CAClD,CAAC;AAEF,wBAAgB,UAAU,CAAC,MAAM,EAAE,UAAU,EAAE,KAAK,EAAE,SAAS,QAAQ,EAAE,GAAG,SAAS,EAAE,CAStF;AAWD,wBAAgB,aAAa,CAAC,KAAK,EAAE,SAAS,SAAS,EAAE,GAAG,MAAM,EAAE,CAEnE"}
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Writing the sinks, and the staleness check.
3
+ *
4
+ * What rule this enforces: `emit --check` exits 3 — a code of its own, distinct from 1 (the code is
5
+ * wrong) and 2 (the config is wrong). An agent reading exit 3 knows the fix is `archcheck emit`,
6
+ * not an edit to any source file (LLD §6).
7
+ *
8
+ * What a caller must know: `check` re-emits into MEMORY and compares. It never writes, so running
9
+ * it in CI cannot mask the drift it is meant to detect.
10
+ */
11
+ import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
12
+ import { dirname, join } from 'node:path';
13
+ import { toPosix } from '../util/paths.js';
14
+ function absolutePathOf(target, file) {
15
+ return join(target.root, target.generatedDir, file.path);
16
+ }
17
+ function readOrNull(path) {
18
+ try {
19
+ return readFileSync(path, 'utf8');
20
+ }
21
+ catch {
22
+ return null;
23
+ }
24
+ }
25
+ export function writeSinks(target, files) {
26
+ const written = [];
27
+ for (const file of files) {
28
+ const path = absolutePathOf(target, file);
29
+ const existing = readOrNull(path);
30
+ const changed = existing !== file.content;
31
+ if (changed) {
32
+ mkdirSync(dirname(path), { recursive: true });
33
+ writeFileSync(path, file.content);
34
+ }
35
+ written.push({ path: toPosix(join(target.generatedDir, file.path)), changed });
36
+ }
37
+ return written;
38
+ }
39
+ export function checkSinks(target, files) {
40
+ const stale = [];
41
+ for (const file of files) {
42
+ const existing = readOrNull(absolutePathOf(target, file));
43
+ const path = toPosix(join(target.generatedDir, file.path));
44
+ if (existing === null)
45
+ stale.push({ path, reason: 'missing' });
46
+ else if (existing !== file.content)
47
+ stale.push({ path, reason: 'hand-edited' });
48
+ }
49
+ return stale;
50
+ }
51
+ /** A Record over the reasons, so a new reason cannot be added without a sentence for it. */
52
+ const STALE_REASON = {
53
+ missing: 'has never been generated',
54
+ unreadable: 'is named in docs but could not be read — create it with an archcheck fence, or drop it ' +
55
+ 'from the manifest',
56
+ 'hand-edited': 'does not match the manifest — it was hand-edited, or the manifest changed',
57
+ };
58
+ export function describeStale(stale) {
59
+ return stale.map(({ path, reason }) => `${path} ${STALE_REASON[reason]}`);
60
+ }
61
+ //# sourceMappingURL=write.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"write.js","sourceRoot":"","sources":["../../src/emit/write.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACjE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAY3C,SAAS,cAAc,CAAC,MAAkB,EAAE,IAAc;IACxD,OAAO,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,YAAY,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC;AAC3D,CAAC;AAED,SAAS,UAAU,CAAC,IAAY;IAC9B,IAAI,CAAC;QACH,OAAO,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IACpC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,MAAkB,EAAE,KAA0B;IACvE,MAAM,OAAO,GAAkB,EAAE,CAAC;IAClC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,IAAI,GAAG,cAAc,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;QAC1C,MAAM,QAAQ,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC;QAClC,MAAM,OAAO,GAAG,QAAQ,KAAK,IAAI,CAAC,OAAO,CAAC;QAC1C,IAAI,OAAO,EAAE,CAAC;YACZ,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;YAC9C,aAAa,CAAC,IAAI,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC;QACpC,CAAC;QACD,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,YAAY,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC;IACjF,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC;AAQD,MAAM,UAAU,UAAU,CAAC,MAAkB,EAAE,KAA0B;IACvE,MAAM,KAAK,GAAgB,EAAE,CAAC;IAC9B,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,QAAQ,GAAG,UAAU,CAAC,cAAc,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC,CAAC;QAC1D,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,YAAY,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;QAC3D,IAAI,QAAQ,KAAK,IAAI;YAAE,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC,CAAC;aAC1D,IAAI,QAAQ,KAAK,IAAI,CAAC,OAAO;YAAE,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,aAAa,EAAE,CAAC,CAAC;IAClF,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,4FAA4F;AAC5F,MAAM,YAAY,GAAwC;IACxD,OAAO,EAAE,0BAA0B;IACnC,UAAU,EACR,yFAAyF;QACzF,mBAAmB;IACrB,aAAa,EAAE,2EAA2E;CAC3F,CAAC;AAEF,MAAM,UAAU,aAAa,CAAC,KAA2B;IACvD,OAAO,KAAK,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,GAAG,IAAI,IAAI,YAAY,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;AAC5E,CAAC"}
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Exceptions — a deliberate, owned, expiring allowance, and the only legitimate way past a rule.
3
+ *
4
+ * What rule this enforces: exceptions live in the MANIFEST, never inline. An inline suppression
5
+ * comment is itself a lint violation (plan 12), and this is the mechanism that makes that ban
6
+ * reasonable rather than merely strict — people do need exceptions, and denying them a designed
7
+ * route is how inline comments spread until nobody reviews any of them.
8
+ *
9
+ * Two properties make it work, and both are enforced rather than advised:
10
+ * 1. EXPIRY IS ENFORCED. Past the date the run fails and names the owner. An exception that
11
+ * needs extending is a conversation, which is the point — one nobody has to re-justify is
12
+ * just a rule that was quietly deleted.
13
+ * 2. ALWAYS VISIBLE. Every declared exception is reported every run, including the ones
14
+ * suppressing nothing. Silent exceptions decay into permanent ones.
15
+ */
16
+ import { type Result } from '../result.js';
17
+ import type { Diagnostic, ExceptionStatus, Rule } from '../types.js';
18
+ export type ArchitectureException = {
19
+ /** The rule id this allows past. */
20
+ rule: string;
21
+ /** A path glob the allowance covers. Either this or `identity` is required. */
22
+ path?: string | string[];
23
+ /** A single finding, pinned by its identity hash. */
24
+ identity?: string;
25
+ owner: string;
26
+ /** ISO date, YYYY-MM-DD. Enforced, not advisory. */
27
+ expires: string;
28
+ reason: string;
29
+ };
30
+ export declare function todayIso(now?: Date): string;
31
+ export declare function validateExceptions(exceptions: readonly ArchitectureException[], rules: readonly Rule[]): Result<void>;
32
+ export type AppliedExceptions = {
33
+ diagnostics: Diagnostic[];
34
+ statuses: ExceptionStatus[];
35
+ /** Expired exceptions. The run fails on these, naming the owner. */
36
+ expired: ExceptionStatus[];
37
+ };
38
+ /**
39
+ * Mark covered diagnostics as exempted, and report the state of every declared exception.
40
+ *
41
+ * An EXPIRED exception suppresses nothing: the violations it used to cover come back, and the
42
+ * expiry itself is reported so the failure names a person rather than only a file.
43
+ */
44
+ export declare function applyExceptions(exceptions: readonly ArchitectureException[], diagnostics: readonly Diagnostic[], today?: string): AppliedExceptions;
45
+ export declare function describeExpired(expired: readonly ExceptionStatus[]): string[];
46
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/exceptions/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,EAAW,KAAK,MAAM,EAAE,MAAM,cAAc,CAAC;AACpD,OAAO,KAAK,EAAE,UAAU,EAAE,eAAe,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AAGrE,MAAM,MAAM,qBAAqB,GAAG;IAClC,oCAAoC;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,+EAA+E;IAC/E,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IACzB,qDAAqD;IACrD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,oDAAoD;IACpD,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AAgBF,wBAAgB,QAAQ,CAAC,GAAG,GAAE,IAAiB,GAAG,MAAM,CAEvD;AAOD,wBAAgB,kBAAkB,CAChC,UAAU,EAAE,SAAS,qBAAqB,EAAE,EAC5C,KAAK,EAAE,SAAS,IAAI,EAAE,GACrB,MAAM,CAAC,IAAI,CAAC,CA8Bd;AAUD,MAAM,MAAM,iBAAiB,GAAG;IAC9B,WAAW,EAAE,UAAU,EAAE,CAAC;IAC1B,QAAQ,EAAE,eAAe,EAAE,CAAC;IAC5B,oEAAoE;IACpE,OAAO,EAAE,eAAe,EAAE,CAAC;CAC5B,CAAC;AAEF;;;;;GAKG;AACH,wBAAgB,eAAe,CAC7B,UAAU,EAAE,SAAS,qBAAqB,EAAE,EAC5C,WAAW,EAAE,SAAS,UAAU,EAAE,EAClC,KAAK,GAAE,MAAmB,GACzB,iBAAiB,CAmCnB;AAED,wBAAgB,eAAe,CAAC,OAAO,EAAE,SAAS,eAAe,EAAE,GAAG,MAAM,EAAE,CAM7E"}
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Exceptions — a deliberate, owned, expiring allowance, and the only legitimate way past a rule.
3
+ *
4
+ * What rule this enforces: exceptions live in the MANIFEST, never inline. An inline suppression
5
+ * comment is itself a lint violation (plan 12), and this is the mechanism that makes that ban
6
+ * reasonable rather than merely strict — people do need exceptions, and denying them a designed
7
+ * route is how inline comments spread until nobody reviews any of them.
8
+ *
9
+ * Two properties make it work, and both are enforced rather than advised:
10
+ * 1. EXPIRY IS ENFORCED. Past the date the run fails and names the owner. An exception that
11
+ * needs extending is a conversation, which is the point — one nobody has to re-justify is
12
+ * just a rule that was quietly deleted.
13
+ * 2. ALWAYS VISIBLE. Every declared exception is reported every run, including the ones
14
+ * suppressing nothing. Silent exceptions decay into permanent ones.
15
+ */
16
+ import { err, ok } from '../result.js';
17
+ import { matchesAny } from '../util/glob.js';
18
+ const ISO_DATE = /^\d{4}-\d{2}-\d{2}$/;
19
+ const DAY_MS = 86_400_000;
20
+ /**
21
+ * A REAL calendar date, not merely a regex-shaped one. `9999-99-99` and `2030-02-30` pass the shape
22
+ * check, and an unreal date is the one way an exception never expires. The round trip catches both
23
+ * kinds of engine: one that refuses the out-of-range day, and one that rolls it over to a different
24
+ * date. Pinned by 'rejects an expiry that is regex-shaped but not a real calendar date'.
25
+ */
26
+ function isCalendarDate(value) {
27
+ const parsed = new Date(`${value}T00:00:00Z`);
28
+ return !Number.isNaN(parsed.getTime()) && parsed.toISOString().slice(0, 10) === value;
29
+ }
30
+ export function todayIso(now = new Date()) {
31
+ return now.toISOString().slice(0, 10);
32
+ }
33
+ /** Both dates are real calendar dates — `validateExceptions` refuses anything else. */
34
+ function daysBetween(fromIso, toIso) {
35
+ return Math.round((Date.parse(toIso) - Date.parse(fromIso)) / DAY_MS);
36
+ }
37
+ export function validateExceptions(exceptions, rules) {
38
+ const known = new Set(rules.map((rule) => rule.id));
39
+ const errors = [];
40
+ for (const [index, exception] of exceptions.entries()) {
41
+ const where = `exceptions[${index}]`;
42
+ if (typeof exception?.rule !== 'string' || !known.has(exception.rule)) {
43
+ errors.push(`${where}: rule '${String(exception?.rule)}' is not defined in the manifest`);
44
+ }
45
+ if (typeof exception?.owner !== 'string' || exception.owner.trim().length === 0) {
46
+ errors.push(`${where}: owner is required — an unowned allowance is never re-justified`);
47
+ }
48
+ if (typeof exception?.expires !== 'string' || !ISO_DATE.test(exception.expires)) {
49
+ errors.push(`${where}: expires is required and must be YYYY-MM-DD — an allowance with no end is a rule quietly deleted`);
50
+ }
51
+ else if (!isCalendarDate(exception.expires)) {
52
+ errors.push(`${where}: expires '${exception.expires}' is not a real calendar date — a date that never arrives is an allowance that never expires`);
53
+ }
54
+ if (typeof exception?.reason !== 'string' || exception.reason.trim().length === 0) {
55
+ errors.push(`${where}: reason is required — the next reader has to be able to judge it`);
56
+ }
57
+ if (exception?.path === undefined && exception?.identity === undefined) {
58
+ errors.push(`${where}: needs a path or an identity — an exception cannot cover everything`);
59
+ }
60
+ }
61
+ return errors.length > 0 ? err(errors) : ok(undefined);
62
+ }
63
+ function covers(exception, diagnostic) {
64
+ if (exception.rule !== diagnostic.ruleId)
65
+ return false;
66
+ if (exception.identity !== undefined)
67
+ return exception.identity === diagnostic.identity;
68
+ if (exception.path === undefined)
69
+ return false;
70
+ const globs = typeof exception.path === 'string' ? [exception.path] : exception.path;
71
+ return matchesAny(globs, diagnostic.file);
72
+ }
73
+ /**
74
+ * Mark covered diagnostics as exempted, and report the state of every declared exception.
75
+ *
76
+ * An EXPIRED exception suppresses nothing: the violations it used to cover come back, and the
77
+ * expiry itself is reported so the failure names a person rather than only a file.
78
+ */
79
+ export function applyExceptions(exceptions, diagnostics, today = todayIso()) {
80
+ // ONE mechanism for both halves. Suppression used to compare the ISO strings while expiry parsed
81
+ // them, so `expires: '9999-99-99'` suppressed forever and reported NaN days left — property 1 of
82
+ // this module, silently untrue. Both now ask this predicate, so they cannot disagree. Written as
83
+ // `!(… >= 0)` so an unparseable date reads as EXPIRED rather than as a permanent allowance; only
84
+ // a caller bypassing `validateExceptions` can produce one. Pinned by 'suppression and expiry are
85
+ // one mechanism, so they never disagree'.
86
+ const isExpired = (exception) => !(daysBetween(today, exception.expires) >= 0);
87
+ const counts = new Map();
88
+ const marked = diagnostics.map((diagnostic) => {
89
+ const index = exceptions.findIndex((exception) => !isExpired(exception) && covers(exception, diagnostic));
90
+ if (index === -1)
91
+ return diagnostic;
92
+ counts.set(index, (counts.get(index) ?? 0) + 1);
93
+ return { ...diagnostic, exempted: true };
94
+ });
95
+ const statuses = exceptions.map((exception, index) => {
96
+ const daysLeft = daysBetween(today, exception.expires);
97
+ return {
98
+ ruleId: exception.rule,
99
+ owner: exception.owner,
100
+ expires: exception.expires,
101
+ // The expiry date itself is the last day it holds, so "expired" means strictly past it.
102
+ expired: isExpired(exception),
103
+ daysLeft,
104
+ reason: exception.reason,
105
+ suppressed: counts.get(index) ?? 0,
106
+ };
107
+ });
108
+ return { diagnostics: marked, statuses, expired: statuses.filter((s) => s.expired) };
109
+ }
110
+ export function describeExpired(expired) {
111
+ return expired.flatMap((status) => [
112
+ `exception expired ${status.expires} (${-status.daysLeft} day(s) ago): ${status.ruleId}`,
113
+ ` owner: ${status.owner} — ${status.reason}`,
114
+ ' → extend it with a new date and a re-stated reason, or fix the violations and remove it',
115
+ ]);
116
+ }
117
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/exceptions/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ export type FileSystem = {
2
+ /** Repo-relative POSIX path. */
3
+ exists(path: string): boolean;
4
+ /** Returns undefined rather than throwing — a missing file is an answer, not a failure. */
5
+ read(path: string): string | undefined;
6
+ /** Repo-relative POSIX paths matching the globs, sorted. */
7
+ glob(patterns: string | string[]): string[];
8
+ };
9
+ export declare function createNodeFileSystem(root: string): FileSystem;
10
+ /** The twin. Same contract, no disk. */
11
+ export declare function createMemoryFileSystem(files: Record<string, string>): FileSystem;
12
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/fs/index.ts"],"names":[],"mappings":"AAgBA,MAAM,MAAM,UAAU,GAAG;IACvB,gCAAgC;IAChC,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;IAC9B,2FAA2F;IAC3F,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;IACvC,4DAA4D;IAC5D,IAAI,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,MAAM,EAAE,CAAC;CAC7C,CAAC;AAIF,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,CA2C7D;AAED,wCAAwC;AACxC,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,UAAU,CAUhF"}
@@ -0,0 +1,78 @@
1
+ /**
2
+ * The filesystem, behind one interface.
3
+ *
4
+ * What rule this enforces: the same twin discipline archcheck asks of its consumers. The memory
5
+ * implementation is what lets the structure tests run without touching disk, and the conformance
6
+ * test runs BOTH implementations against one fixture and asserts identical findings — which is
7
+ * the only thing that proves the twin is a twin rather than a second, subtly different tool.
8
+ *
9
+ * What a caller must know: every path is repo-relative POSIX, in and out. The Node implementation
10
+ * holds the one absolute root; nothing above this layer ever sees one.
11
+ */
12
+ import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
13
+ import { join } from 'node:path';
14
+ import { matchesAny } from '../util/glob.js';
15
+ import { toPosix } from '../util/paths.js';
16
+ const IGNORED = new Set(['node_modules', '.git', 'dist', '.archcheck']);
17
+ export function createNodeFileSystem(root) {
18
+ let listing;
19
+ const walk = () => {
20
+ if (listing)
21
+ return listing;
22
+ const out = [];
23
+ const visit = (relative) => {
24
+ let entries;
25
+ try {
26
+ entries = readdirSync(join(root, relative));
27
+ }
28
+ catch {
29
+ return; // an unreadable directory is empty as far as a rule is concerned
30
+ }
31
+ for (const entry of entries) {
32
+ if (IGNORED.has(entry))
33
+ continue;
34
+ const next = relative === '' ? entry : `${relative}/${entry}`;
35
+ try {
36
+ if (statSync(join(root, next)).isDirectory())
37
+ visit(next);
38
+ else
39
+ out.push(next);
40
+ }
41
+ catch {
42
+ // a vanished entry between readdir and stat is simply not there
43
+ }
44
+ }
45
+ };
46
+ visit('');
47
+ listing = out.sort();
48
+ return listing;
49
+ };
50
+ return {
51
+ exists: (path) => existsSync(join(root, toPosix(path))),
52
+ read: (path) => {
53
+ try {
54
+ return readFileSync(join(root, toPosix(path)), 'utf8');
55
+ }
56
+ catch {
57
+ return undefined;
58
+ }
59
+ },
60
+ glob: (patterns) => {
61
+ const globs = typeof patterns === 'string' ? [patterns] : patterns;
62
+ return walk().filter((path) => matchesAny(globs, path));
63
+ },
64
+ };
65
+ }
66
+ /** The twin. Same contract, no disk. */
67
+ export function createMemoryFileSystem(files) {
68
+ const paths = Object.keys(files).sort();
69
+ return {
70
+ exists: (path) => Object.hasOwn(files, toPosix(path)),
71
+ read: (path) => files[toPosix(path)],
72
+ glob: (patterns) => {
73
+ const globs = typeof patterns === 'string' ? [patterns] : patterns;
74
+ return paths.filter((path) => matchesAny(globs, path));
75
+ },
76
+ };
77
+ }
78
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/fs/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ import type { FileFacts } from './types.js';
2
+ export declare const CACHE_DIR = ".archcheck/cache";
3
+ export declare function hashContent(content: string): string;
4
+ export type FactsCache = {
5
+ get(file: string, contentHash: string): FileFacts | undefined;
6
+ set(file: string, contentHash: string, facts: FileFacts): void;
7
+ flush(): void;
8
+ };
9
+ /** A cache that remembers nothing — the default when a run has nowhere to write. */
10
+ export declare function createNullCache(): FactsCache;
11
+ export declare function createFactsCache(root: string, fingerprint: string): FactsCache;
12
+ //# sourceMappingURL=cache.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../../src/graph/cache.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAQ,SAAS,EAAE,MAAM,YAAY,CAAC;AAElD,eAAO,MAAM,SAAS,qBAAqB,CAAC;AAE5C,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAEnD;AAMD,MAAM,MAAM,UAAU,GAAG;IACvB,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS,CAAC;IAC9D,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,GAAG,IAAI,CAAC;IAC/D,KAAK,IAAI,IAAI,CAAC;CACf,CAAC;AAUF,oFAAoF;AACpF,wBAAgB,eAAe,IAAI,UAAU,CAE5C;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,UAAU,CAyC9E"}