@goodbones/core 0.1.0-beta.10 → 0.1.0-beta.12

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 (119) hide show
  1. package/build/dts/core/structure.d.ts.map +1 -1
  2. package/build/dts/domain/architecture-config.d.ts +3 -310
  3. package/build/dts/domain/architecture-config.d.ts.map +1 -1
  4. package/build/dts/domain/snapshot.d.ts +150 -18
  5. package/build/dts/domain/snapshot.d.ts.map +1 -1
  6. package/build/dts/index.d.ts +11 -14
  7. package/build/dts/index.d.ts.map +1 -1
  8. package/build/dts/infrastructure/file-system-fake.d.ts.map +1 -1
  9. package/build/dts/infrastructure/file-system-live.d.ts.map +1 -1
  10. package/build/dts/infrastructure/walk.d.ts +6 -1
  11. package/build/dts/infrastructure/walk.d.ts.map +1 -1
  12. package/build/dts/load/extension.d.ts +33 -0
  13. package/build/dts/load/extension.d.ts.map +1 -0
  14. package/build/dts/load/policy.d.ts +4 -10
  15. package/build/dts/load/policy.d.ts.map +1 -1
  16. package/build/dts/manifest/compile.d.ts +7 -2
  17. package/build/dts/manifest/compile.d.ts.map +1 -1
  18. package/build/dts/manifest/extension.d.ts +8 -0
  19. package/build/dts/manifest/extension.d.ts.map +1 -0
  20. package/build/dts/manifest/glob.d.ts +1 -0
  21. package/build/dts/manifest/glob.d.ts.map +1 -1
  22. package/build/dts/manifest/json-schema.d.ts +6 -2
  23. package/build/dts/manifest/json-schema.d.ts.map +1 -1
  24. package/build/dts/manifest/manifest.d.ts +7 -362
  25. package/build/dts/manifest/manifest.d.ts.map +1 -1
  26. package/build/dts/ports/file-system.d.ts +1 -0
  27. package/build/dts/ports/file-system.d.ts.map +1 -1
  28. package/build/dts/testing.d.ts +0 -1
  29. package/build/dts/testing.d.ts.map +1 -1
  30. package/build/esm/core/structure.js +1 -1
  31. package/build/esm/core/structure.js.map +1 -1
  32. package/build/esm/domain/architecture-config.js +3 -136
  33. package/build/esm/domain/architecture-config.js.map +1 -1
  34. package/build/esm/domain/snapshot.js +55 -15
  35. package/build/esm/domain/snapshot.js.map +1 -1
  36. package/build/esm/index.js +16 -10
  37. package/build/esm/index.js.map +1 -1
  38. package/build/esm/infrastructure/file-system-fake.js +10 -0
  39. package/build/esm/infrastructure/file-system-fake.js.map +1 -1
  40. package/build/esm/infrastructure/file-system-live.js +9 -1
  41. package/build/esm/infrastructure/file-system-live.js.map +1 -1
  42. package/build/esm/infrastructure/walk.js +50 -4
  43. package/build/esm/infrastructure/walk.js.map +1 -1
  44. package/build/esm/load/extension.js +2 -0
  45. package/build/esm/load/extension.js.map +1 -0
  46. package/build/esm/load/policy.js +43 -116
  47. package/build/esm/load/policy.js.map +1 -1
  48. package/build/esm/manifest/compile.js +3 -182
  49. package/build/esm/manifest/compile.js.map +1 -1
  50. package/build/esm/manifest/extension.js +2 -0
  51. package/build/esm/manifest/extension.js.map +1 -0
  52. package/build/esm/manifest/glob.js +3 -0
  53. package/build/esm/manifest/glob.js.map +1 -1
  54. package/build/esm/manifest/json-schema.js +45 -11
  55. package/build/esm/manifest/json-schema.js.map +1 -1
  56. package/build/esm/manifest/manifest.js +56 -172
  57. package/build/esm/manifest/manifest.js.map +1 -1
  58. package/build/esm/testing.js +0 -1
  59. package/build/esm/testing.js.map +1 -1
  60. package/package.json +1 -1
  61. package/schema/architecture-node.schema.json +0 -887
  62. package/schema/architecture.schema.json +3264 -1591
  63. package/schema/conformance.schema.json +291 -47
  64. package/src/core/structure.ts +5 -3
  65. package/src/domain/architecture-config.ts +3 -180
  66. package/src/domain/snapshot.ts +125 -25
  67. package/src/index.ts +51 -78
  68. package/src/infrastructure/file-system-fake.ts +12 -0
  69. package/src/infrastructure/file-system-live.ts +8 -1
  70. package/src/infrastructure/walk.ts +58 -3
  71. package/src/load/extension.ts +66 -0
  72. package/src/load/policy.ts +60 -175
  73. package/src/manifest/compile.ts +2 -213
  74. package/src/manifest/extension.ts +41 -0
  75. package/src/manifest/glob.ts +5 -0
  76. package/src/manifest/json-schema.ts +53 -11
  77. package/src/manifest/manifest.ts +82 -214
  78. package/src/ports/file-system.ts +3 -0
  79. package/src/testing.ts +0 -1
  80. package/build/dts/core/campaigns.d.ts +0 -141
  81. package/build/dts/core/campaigns.d.ts.map +0 -1
  82. package/build/dts/core/ledger.d.ts +0 -61
  83. package/build/dts/core/ledger.d.ts.map +0 -1
  84. package/build/dts/domain/report.d.ts +0 -16
  85. package/build/dts/domain/report.d.ts.map +0 -1
  86. package/build/dts/infrastructure/campaign-functions.d.ts +0 -7
  87. package/build/dts/infrastructure/campaign-functions.d.ts.map +0 -1
  88. package/build/dts/infrastructure/report-source-fake.d.ts +0 -4
  89. package/build/dts/infrastructure/report-source-fake.d.ts.map +0 -1
  90. package/build/dts/infrastructure/report-source-live.d.ts +0 -3
  91. package/build/dts/infrastructure/report-source-live.d.ts.map +0 -1
  92. package/build/dts/ports/campaign-predicate.d.ts +0 -24
  93. package/build/dts/ports/campaign-predicate.d.ts.map +0 -1
  94. package/build/dts/ports/report-source.d.ts +0 -14
  95. package/build/dts/ports/report-source.d.ts.map +0 -1
  96. package/build/esm/core/campaigns.js +0 -778
  97. package/build/esm/core/campaigns.js.map +0 -1
  98. package/build/esm/core/ledger.js +0 -171
  99. package/build/esm/core/ledger.js.map +0 -1
  100. package/build/esm/domain/report.js +0 -168
  101. package/build/esm/domain/report.js.map +0 -1
  102. package/build/esm/infrastructure/campaign-functions.js +0 -67
  103. package/build/esm/infrastructure/campaign-functions.js.map +0 -1
  104. package/build/esm/infrastructure/report-source-fake.js +0 -6
  105. package/build/esm/infrastructure/report-source-fake.js.map +0 -1
  106. package/build/esm/infrastructure/report-source-live.js +0 -165
  107. package/build/esm/infrastructure/report-source-live.js.map +0 -1
  108. package/build/esm/ports/campaign-predicate.js +0 -2
  109. package/build/esm/ports/campaign-predicate.js.map +0 -1
  110. package/build/esm/ports/report-source.js +0 -7
  111. package/build/esm/ports/report-source.js.map +0 -1
  112. package/src/core/campaigns.ts +0 -1056
  113. package/src/core/ledger.ts +0 -242
  114. package/src/domain/report.ts +0 -203
  115. package/src/infrastructure/campaign-functions.ts +0 -98
  116. package/src/infrastructure/report-source-fake.ts +0 -10
  117. package/src/infrastructure/report-source-live.ts +0 -192
  118. package/src/ports/campaign-predicate.ts +0 -30
  119. package/src/ports/report-source.ts +0 -35
@@ -1,242 +0,0 @@
1
- import * as Result from "effect/Result";
2
- import * as Schema from "effect/Schema";
3
-
4
- import type { CampaignUnit } from "../domain/architecture-config.js";
5
- import type { Violation } from "../domain/violation.js";
6
-
7
- // A campaign's ledger: every place the pattern still occurs, and the record of
8
- // every time the count was allowed to go up. It is the baseline's cousin and
9
- // not the baseline — same fingerprint, different file, different semantics.
10
- //
11
- // - One file per campaign, so two pull requests migrating different
12
- // components never conflict, and finishing a campaign is deleting its file.
13
- // - Growth is only ever recorded, never silent. `prune` only removes;
14
- // `allow` is the one way an entry is added, and it always appends a
15
- // regression with a reason, a timestamp and an author.
16
- // - The arithmetic is checked: `entries.length === initial + Σ delta − fixed`.
17
- // A hand-added entry with no regression record fails the build, with no
18
- // git history in the loop.
19
- // - A fixed entry is stale, and `check` fails on it as it fails on a stale
20
- // baseline entry, so progress lands as a visible diff.
21
-
22
- const Regression = Schema.Struct({
23
- at: Schema.String,
24
- by: Schema.String,
25
- delta: Schema.Finite,
26
- reason: Schema.String,
27
- entries: Schema.Array(Schema.String),
28
- });
29
-
30
- export const Ledger = Schema.Struct({
31
- version: Schema.Literal(1),
32
- id: Schema.String,
33
- created: Schema.String,
34
- // The count on the day the ledger was written.
35
- initial: Schema.Finite,
36
- // Entries removed by `prune` since.
37
- fixed: Schema.Finite,
38
- // When an entry last left the ledger — what the stall clock reads.
39
- lastProgress: Schema.String,
40
- regressions: Schema.Array(Regression),
41
- // `file` or `file#subject`, deduplicated and sorted.
42
- entries: Schema.Array(Schema.String),
43
- });
44
-
45
- export type Ledger = typeof Ledger.Type;
46
- export type Regression = typeof Regression.Type;
47
-
48
- const decode = Schema.decodeUnknownResult(Ledger, { errors: "all", onExcessProperty: "error" });
49
-
50
- export const EMPTY_LEDGER = (id: string, now: number): Ledger => {
51
- const at = new Date(now).toISOString();
52
- return {
53
- version: 1,
54
- id,
55
- created: at,
56
- initial: 0,
57
- fixed: 0,
58
- lastProgress: at,
59
- regressions: [],
60
- entries: [],
61
- };
62
- };
63
-
64
- // A malformed ledger is refused, unlike a malformed baseline, which reads as
65
- // empty: an empty baseline reports every violation, the safe direction, while
66
- // an empty ledger would report every hit as unrecorded growth and demand a
67
- // regression record for debt that was already counted.
68
- export const decodeLedger = (raw: unknown): Result.Result<Ledger, string> => {
69
- const decoded = decode(raw);
70
- return Result.isFailure(decoded)
71
- ? Result.fail(String(decoded.failure.issue))
72
- : Result.succeed(decoded.success);
73
- };
74
-
75
- export const serializeLedger = (ledger: Ledger): string => `${JSON.stringify(ledger, null, 2)}\n`;
76
-
77
- // A hit's entry: the file, and its subject when the campaign's unit has one.
78
- export const entryOf = (violation: Violation): string =>
79
- violation.subject === null ? violation.file : `${violation.file}#${violation.subject}`;
80
-
81
- const allowedTotal = (ledger: Ledger): number =>
82
- ledger.initial + ledger.regressions.reduce((sum, one) => sum + one.delta, 0);
83
-
84
- export const ledgerArithmeticHolds = (ledger: Ledger): boolean =>
85
- ledger.entries.length === allowedTotal(ledger) - ledger.fixed;
86
-
87
- // A `match` entry is `file#anchor#hash`; an edit inside the anchored
88
- // declaration changes the hash and nothing else, and is the same entry.
89
- const anchorOf = (entry: string): string => entry.slice(0, entry.lastIndexOf("#"));
90
-
91
- export type Reconciliation = {
92
- // Hits the ledger already carries, exactly or by anchor.
93
- readonly ledgered: ReadonlyArray<Violation>;
94
- // Hits the ledger does not carry: unrecorded growth.
95
- readonly unrecorded: ReadonlyArray<Violation>;
96
- // Entries no hit produces: fixed, and waiting to be pruned.
97
- readonly stale: ReadonlyArray<string>;
98
- // Entries whose hash moved under a still-present anchor, with what they
99
- // read now. `prune` rewrites them; they count as neither fixed nor new.
100
- readonly drifted: ReadonlyArray<{ readonly from: string; readonly to: string }>;
101
- };
102
-
103
- export const reconcile = (
104
- ledger: Ledger,
105
- hits: Iterable<Violation>,
106
- unit: CampaignUnit,
107
- ): Reconciliation => {
108
- const entries = new Set(ledger.entries);
109
- const ledgered: Array<Violation> = [];
110
- const unmatched: Array<Violation> = [];
111
- const current = new Set<string>();
112
- for (const hit of hits) {
113
- const entry = entryOf(hit);
114
- current.add(entry);
115
- if (entries.has(entry)) ledgered.push(hit);
116
- else unmatched.push(hit);
117
- }
118
- let stale = ledger.entries.filter((entry) => !current.has(entry));
119
- const drifted: Array<{ from: string; to: string }> = [];
120
- const unrecorded: Array<Violation> = [];
121
-
122
- if (unit === "match") {
123
- // Pair each stale entry with an unmatched hit under the same anchor, in
124
- // order, so a declaration with two edited matches keeps two entries.
125
- const byAnchor = new Map<string, Array<string>>();
126
- for (const entry of stale) {
127
- const anchor = anchorOf(entry);
128
- byAnchor.set(anchor, [...(byAnchor.get(anchor) ?? []), entry]);
129
- }
130
- const paired = new Set<string>();
131
- for (const hit of unmatched) {
132
- const entry = entryOf(hit);
133
- const candidates = byAnchor.get(anchorOf(entry));
134
- const from = candidates?.shift();
135
- if (from === undefined) {
136
- unrecorded.push(hit);
137
- continue;
138
- }
139
- paired.add(from);
140
- drifted.push({ from, to: entry });
141
- ledgered.push(hit);
142
- }
143
- stale = stale.filter((entry) => !paired.has(entry));
144
- } else {
145
- for (const hit of unmatched) unrecorded.push(hit);
146
- }
147
-
148
- return { ledgered, unrecorded, stale, drifted };
149
- };
150
-
151
- export const staleEntriesOf = (
152
- ledger: Ledger,
153
- hits: Iterable<Violation>,
154
- unit: CampaignUnit,
155
- ): ReadonlyArray<string> => reconcile(ledger, hits, unit).stale;
156
-
157
- export const newEntriesOf = (
158
- ledger: Ledger,
159
- hits: Iterable<Violation>,
160
- unit: CampaignUnit,
161
- ): ReadonlyArray<string> => reconcile(ledger, hits, unit).unrecorded.map(entryOf);
162
-
163
- const sorted = (entries: Iterable<string>): ReadonlyArray<string> => [...new Set(entries)].sort();
164
-
165
- // A first ledger: every hit is an entry, and the count is the `initial`.
166
- export const ledgerOf = (id: string, hits: Iterable<Violation>, now: number): Ledger => {
167
- const entries = sorted([...hits].map(entryOf));
168
- return { ...EMPTY_LEDGER(id, now), initial: entries.length, entries };
169
- };
170
-
171
- // `prune`: the stale entries leave, `fixed` rises by as many, and
172
- // `lastProgress` moves — only when something left. A drifted entry is
173
- // rewritten in place and counts as neither.
174
- export const pruned = (
175
- ledger: Ledger,
176
- hits: Iterable<Violation>,
177
- unit: CampaignUnit,
178
- now: number,
179
- ): Ledger => {
180
- const { drifted, stale } = reconcile(ledger, hits, unit);
181
- const rewritten = new Map(drifted.map((one) => [one.from, one.to]));
182
- const gone = new Set(stale);
183
- const entries = sorted(
184
- ledger.entries
185
- .filter((entry) => !gone.has(entry))
186
- .map((entry) => rewritten.get(entry) ?? entry),
187
- );
188
- return {
189
- ...ledger,
190
- fixed: ledger.fixed + stale.length,
191
- lastProgress: stale.length > 0 ? new Date(now).toISOString() : ledger.lastProgress,
192
- entries,
193
- };
194
- };
195
-
196
- export type RegressionRecord = {
197
- readonly at: number;
198
- readonly by: string;
199
- readonly reason: string;
200
- };
201
-
202
- // `allow`: the entries join the ledger, and a regression records that they
203
- // did. The delta is what was actually added — an entry already present is
204
- // not growth.
205
- export const allowed = (
206
- ledger: Ledger,
207
- entries: Iterable<string>,
208
- record: RegressionRecord,
209
- ): Ledger => {
210
- const present = new Set(ledger.entries);
211
- const added = sorted([...entries].filter((entry) => !present.has(entry)));
212
- if (added.length === 0) return ledger;
213
- return {
214
- ...ledger,
215
- regressions: [
216
- ...ledger.regressions,
217
- {
218
- at: new Date(record.at).toISOString(),
219
- by: record.by,
220
- delta: added.length,
221
- reason: record.reason,
222
- entries: added,
223
- },
224
- ],
225
- entries: sorted([...ledger.entries, ...added]),
226
- };
227
- };
228
-
229
- // `1 − count / (initial + Σ delta)`: how much of everything the campaign was
230
- // ever asked to pay down has been paid.
231
- export const progressOf = (ledger: Ledger): number => {
232
- const total = allowedTotal(ledger);
233
- return total === 0 ? 1 : 1 - ledger.entries.length / total;
234
- };
235
-
236
- export const isComplete = (ledger: Ledger): boolean => ledger.entries.length === 0;
237
-
238
- export const isStalled = (
239
- rule: { readonly staleAfter: number },
240
- ledger: Ledger,
241
- now: number,
242
- ): boolean => !isComplete(ledger) && now - Date.parse(ledger.lastProgress) > rule.staleAfter;
@@ -1,203 +0,0 @@
1
- // A campaign's `report` term names a pattern no parser of ours sees: a type
2
- // error, a lint finding from another tool, anything a program prints one
3
- // line per occurrence. This is the vocabulary such a report is read into —
4
- // a diagnostic with a file, a position, a code and a message — and the
5
- // readers for the formats a report comes in. Pure: text in, diagnostics out.
6
- // Running the command or reading the file is the report source's business.
7
-
8
- export type Diagnostic = {
9
- // Repo-relative, forward slashes.
10
- readonly file: string;
11
- // Zero-based, as a syntax match's range is.
12
- readonly line: number;
13
- readonly column: number;
14
- // `TS2551`, `eslint(no-unused-vars)`, `no-debugger` — whatever the tool
15
- // calls the kind of finding; `""` when the format carries none.
16
- readonly code: string;
17
- // The first line of the message.
18
- readonly message: string;
19
- };
20
-
21
- export type ReportFormat = "tsc" | "eslint" | "oxlint" | "regex";
22
-
23
- export type ParseReportOptions = {
24
- // Absolute paths in the report are made relative to this.
25
- readonly repoRoot: string;
26
- // `regex` only: a pattern with named groups `file` and `line`, and
27
- // optionally `column`, `code` and `message`; one diagnostic per line that
28
- // matches, positions one-based as tools print them.
29
- readonly pattern?: string | undefined;
30
- };
31
-
32
- const relativeTo = (repoRoot: string, file: string): string => {
33
- const slashed = file.replaceAll("\\", "/");
34
- const root = repoRoot.replaceAll("\\", "/").replace(/\/$/, "");
35
- const relative =
36
- slashed === root
37
- ? ""
38
- : slashed.startsWith(`${root}/`)
39
- ? slashed.slice(root.length + 1)
40
- : slashed;
41
- return relative.replace(/^\.\//, "");
42
- };
43
-
44
- const oneBased = (value: string | undefined, fallback = 1): number => {
45
- const parsed = Number(value);
46
- return Number.isFinite(parsed) && parsed >= 1 ? parsed - 1 : fallback - 1;
47
- };
48
-
49
- const firstLine = (message: string): string => message.split(/\r?\n/, 1)[0]?.trim() ?? "";
50
-
51
- // `src/a.ts(12,5): error TS2551: Property 'x' does not exist…`, one per
52
- // line, with the continuation lines tsc indents beneath a message ignored —
53
- // the first line is the message.
54
- const TSC_LINE = /^(.+?)\((\d+),(\d+)\): error (TS\d+): (.*)$/;
55
-
56
- const parseTsc = (text: string, options: ParseReportOptions): ReadonlyArray<Diagnostic> => {
57
- const found: Array<Diagnostic> = [];
58
- for (const line of text.split(/\r?\n/)) {
59
- const match = TSC_LINE.exec(line);
60
- if (match === null) continue;
61
- const [, file = "", row, column, code = "", message = ""] = match;
62
- found.push({
63
- file: relativeTo(options.repoRoot, file),
64
- line: oneBased(row),
65
- column: oneBased(column),
66
- code,
67
- message: firstLine(message),
68
- });
69
- }
70
- return found;
71
- };
72
-
73
- const isRecord = (value: unknown): value is Record<string, unknown> =>
74
- typeof value === "object" && value !== null && !Array.isArray(value);
75
-
76
- const asString = (value: unknown): string => (typeof value === "string" ? value : "");
77
- const asNumber = (value: unknown): number | undefined =>
78
- typeof value === "number" && Number.isFinite(value) ? value : undefined;
79
-
80
- const parseJson = (text: string, format: string): unknown => {
81
- try {
82
- return JSON.parse(text);
83
- } catch (cause) {
84
- throw new Error(`a ${format} report is JSON, and this one does not parse: ${String(cause)}`);
85
- }
86
- };
87
-
88
- // `eslint --format json`: one object per file with its messages. A message
89
- // with no `ruleId` (a parse error) carries the code `""`.
90
- const parseEslint = (text: string, options: ParseReportOptions): ReadonlyArray<Diagnostic> => {
91
- const parsed = parseJson(text, "eslint");
92
- if (!Array.isArray(parsed)) throw new Error("an eslint report is a JSON array of files");
93
- const found: Array<Diagnostic> = [];
94
- for (const entry of parsed) {
95
- if (!isRecord(entry) || !Array.isArray(entry.messages)) continue;
96
- const file = relativeTo(options.repoRoot, asString(entry.filePath));
97
- for (const message of entry.messages) {
98
- if (!isRecord(message)) continue;
99
- found.push({
100
- file,
101
- line: (asNumber(message.line) ?? 1) - 1,
102
- column: (asNumber(message.column) ?? 1) - 1,
103
- code: asString(message.ruleId),
104
- message: firstLine(asString(message.message)),
105
- });
106
- }
107
- }
108
- return found;
109
- };
110
-
111
- // `oxlint --format json`: `{ diagnostics: [{ filename, code, message,
112
- // labels: [{ span: { line, column } }] }] }`, positions one-based.
113
- const parseOxlint = (text: string, options: ParseReportOptions): ReadonlyArray<Diagnostic> => {
114
- const parsed = parseJson(text, "oxlint");
115
- if (!isRecord(parsed) || !Array.isArray(parsed.diagnostics)) {
116
- throw new Error("an oxlint report is a JSON object with a `diagnostics` array");
117
- }
118
- const found: Array<Diagnostic> = [];
119
- for (const entry of parsed.diagnostics) {
120
- if (!isRecord(entry)) continue;
121
- const labels: ReadonlyArray<unknown> = Array.isArray(entry.labels) ? entry.labels : [];
122
- const label: unknown = labels[0];
123
- const span = isRecord(label) && isRecord(label.span) ? label.span : {};
124
- found.push({
125
- file: relativeTo(options.repoRoot, asString(entry.filename)),
126
- line: (asNumber(span.line) ?? 1) - 1,
127
- column: (asNumber(span.column) ?? 1) - 1,
128
- code: asString(entry.code),
129
- message: firstLine(asString(entry.message)),
130
- });
131
- }
132
- return found;
133
- };
134
-
135
- const parseRegex = (text: string, options: ParseReportOptions): ReadonlyArray<Diagnostic> => {
136
- if (options.pattern === undefined) {
137
- throw new Error("a `regex` report needs a `pattern` with named groups `file` and `line`");
138
- }
139
- const pattern = new RegExp(options.pattern);
140
- const found: Array<Diagnostic> = [];
141
- for (const line of text.split(/\r?\n/)) {
142
- const match = pattern.exec(line);
143
- if (match === null) continue;
144
- const groups = match.groups ?? {};
145
- if (groups.file === undefined) continue;
146
- found.push({
147
- file: relativeTo(options.repoRoot, groups.file),
148
- line: oneBased(groups.line),
149
- column: oneBased(groups.column),
150
- code: groups.code ?? "",
151
- message: firstLine(groups.message ?? ""),
152
- });
153
- }
154
- return found;
155
- };
156
-
157
- export const parseReport = (
158
- format: ReportFormat,
159
- text: string,
160
- options: ParseReportOptions,
161
- ): ReadonlyArray<Diagnostic> => {
162
- switch (format) {
163
- case "tsc":
164
- return parseTsc(text, options);
165
- case "eslint":
166
- return parseEslint(text, options);
167
- case "oxlint":
168
- return parseOxlint(text, options);
169
- case "regex":
170
- return parseRegex(text, options);
171
- }
172
- };
173
-
174
- // One report from several outputs. A tool that takes one project at a time
175
- // is run once per project, and a project's program includes the files of
176
- // the projects it references, so one diagnostic is printed under several
177
- // runs; it is one diagnostic. Kept once, at its first appearance, when
178
- // identical in file, position, code and message.
179
- export const uniqueDiagnostics = (diagnostics: Iterable<Diagnostic>): ReadonlyArray<Diagnostic> => {
180
- const seen = new Set<string>();
181
- const kept: Array<Diagnostic> = [];
182
- for (const one of diagnostics) {
183
- const key = JSON.stringify([one.file, one.line, one.column, one.code, one.message]);
184
- if (seen.has(key)) continue;
185
- seen.add(key);
186
- kept.push(one);
187
- }
188
- return kept;
189
- };
190
-
191
- // The diagnostics indexed by file, which is how a per-file evaluator asks
192
- // for them.
193
- export const indexByFile = (
194
- diagnostics: Iterable<Diagnostic>,
195
- ): ReadonlyMap<string, ReadonlyArray<Diagnostic>> => {
196
- const byFile = new Map<string, Array<Diagnostic>>();
197
- for (const one of diagnostics) {
198
- const found = byFile.get(one.file);
199
- if (found === undefined) byFile.set(one.file, [one]);
200
- else found.push(one);
201
- }
202
- return byFile;
203
- };
@@ -1,98 +0,0 @@
1
- import * as path from "node:path";
2
- import { pathToFileURL } from "node:url";
3
-
4
- import { ConfigInvalid } from "../domain/architecture-error.js";
5
- import type { CampaignPredicate } from "../ports/campaign-predicate.js";
6
-
7
- // A campaign's `fn` term names a predicate function as `module#export`,
8
- // resolved relative to the root manifest. Importing it touches the module
9
- // loader, which is why this lives in infrastructure and both hosts call it
10
- // before `loadPolicy`: the core receives the functions as a map and never
11
- // imports anything.
12
- //
13
- // A `.mjs` manifest may write the function itself in place of the string.
14
- // It is lifted out here, keyed by a synthesized name, and the manifest handed
15
- // back with the string in its place — so the schema, which decodes a data
16
- // file and a module alike, sees one shape. The whole value is walked, not
17
- // only `campaigns`, so a term written in a `defs` fragment is found too.
18
-
19
- export type LoadedCampaignFunctions = {
20
- readonly functions: ReadonlyMap<string, CampaignPredicate>;
21
- // The manifest with every function value replaced by its synthesized name.
22
- readonly manifest: unknown;
23
- };
24
-
25
- const isRecord = (value: unknown): value is Record<string, unknown> =>
26
- typeof value === "object" && value !== null && !Array.isArray(value);
27
-
28
- const REFERENCE = /^(.+)#([A-Za-z_$][\w$]*)$/;
29
-
30
- // A `fn` term is an object whose one key is `fn`.
31
- const isFnTerm = (value: Record<string, unknown>): boolean =>
32
- "fn" in value && Object.keys(value).length === 1;
33
-
34
- export const loadCampaignFunctions = async (
35
- manifestPath: string,
36
- rawManifest: unknown,
37
- ): Promise<LoadedCampaignFunctions> => {
38
- const functions = new Map<string, CampaignPredicate>();
39
- const root = path.dirname(manifestPath);
40
- const refuse = (detail: string): never => {
41
- throw new ConfigInvalid({ configPath: manifestPath, detail });
42
- };
43
-
44
- const imported = async (reference: string): Promise<void> => {
45
- if (functions.has(reference)) return;
46
- const parsed = REFERENCE.exec(reference);
47
- if (parsed === null) {
48
- return refuse(
49
- `the \`fn\` term ${JSON.stringify(reference)} is not \`module#export\`: name the ` +
50
- `module (relative to the manifest) and the export, joined by \`#\`.`,
51
- );
52
- }
53
- const [, module = "", exportName = ""] = parsed;
54
- const at = path.resolve(root, module);
55
- let loaded: unknown;
56
- try {
57
- loaded = await import(pathToFileURL(at).href);
58
- } catch (cause) {
59
- return refuse(
60
- `the \`fn\` term ${JSON.stringify(reference)} names a module that does not load: ${String(cause)}`,
61
- );
62
- }
63
- const found = isRecord(loaded) ? loaded[exportName] : undefined;
64
- if (typeof found !== "function") {
65
- return refuse(
66
- `the \`fn\` term ${JSON.stringify(reference)} names an export that is not a function ` +
67
- `(${found === undefined ? "absent" : typeof found}).`,
68
- );
69
- }
70
- functions.set(reference, found as CampaignPredicate);
71
- };
72
-
73
- let synthesized = 0;
74
- const lift = async (value: unknown): Promise<unknown> => {
75
- if (Array.isArray(value)) {
76
- const items: Array<unknown> = [];
77
- for (const item of value) items.push(await lift(item));
78
- return items;
79
- }
80
- if (!isRecord(value)) return value;
81
- if (isFnTerm(value)) {
82
- const fn = value.fn;
83
- if (typeof fn === "function") {
84
- synthesized += 1;
85
- const name = `<manifest>#fn${String(synthesized)}`;
86
- functions.set(name, fn as CampaignPredicate);
87
- return { fn: name };
88
- }
89
- if (typeof fn === "string") await imported(fn);
90
- return value;
91
- }
92
- const rebuilt: Record<string, unknown> = {};
93
- for (const [key, entry] of Object.entries(value)) rebuilt[key] = await lift(entry);
94
- return rebuilt;
95
- };
96
-
97
- return { functions, manifest: await lift(rawManifest) };
98
- };
@@ -1,10 +0,0 @@
1
- import type { Diagnostic } from "../domain/report.js";
2
- import type { ReportSource } from "../ports/report-source.js";
3
-
4
- // Keyed by file: a test states which diagnostics another tool reported on
5
- // which file, and never runs the tool. Every spec is answered alike.
6
- export const makeReportSourceFake = (
7
- staged: Readonly<Record<string, ReadonlyArray<Omit<Diagnostic, "file">>>>,
8
- ): ReportSource => ({
9
- diagnosticsOf: (_spec, file) => (staged[file] ?? []).map((one) => ({ ...one, file })),
10
- });