@goodbones/core 0.1.0-beta.6 → 0.1.0-beta.8
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.
- package/build/dts/core/campaigns.d.ts +140 -0
- package/build/dts/core/campaigns.d.ts.map +1 -0
- package/build/dts/core/ledger.d.ts +61 -0
- package/build/dts/core/ledger.d.ts.map +1 -0
- package/build/dts/core/structure.d.ts +1 -0
- package/build/dts/core/structure.d.ts.map +1 -1
- package/build/dts/domain/architecture-config.d.ts +309 -1
- package/build/dts/domain/architecture-config.d.ts.map +1 -1
- package/build/dts/domain/facts.d.ts +1 -0
- package/build/dts/domain/facts.d.ts.map +1 -1
- package/build/dts/domain/report.d.ts +15 -0
- package/build/dts/domain/report.d.ts.map +1 -0
- package/build/dts/domain/snapshot.d.ts +55 -3
- package/build/dts/domain/snapshot.d.ts.map +1 -1
- package/build/dts/domain/violation.d.ts +1 -1
- package/build/dts/domain/violation.d.ts.map +1 -1
- package/build/dts/index.d.ts +11 -3
- package/build/dts/index.d.ts.map +1 -1
- package/build/dts/infrastructure/campaign-functions.d.ts +7 -0
- package/build/dts/infrastructure/campaign-functions.d.ts.map +1 -0
- package/build/dts/infrastructure/report-source-fake.d.ts +4 -0
- package/build/dts/infrastructure/report-source-fake.d.ts.map +1 -0
- package/build/dts/infrastructure/report-source-live.d.ts +3 -0
- package/build/dts/infrastructure/report-source-live.d.ts.map +1 -0
- package/build/dts/infrastructure/syntax-matcher-fake.d.ts +10 -0
- package/build/dts/infrastructure/syntax-matcher-fake.d.ts.map +1 -0
- package/build/dts/load/policy.d.ts +14 -0
- package/build/dts/load/policy.d.ts.map +1 -1
- package/build/dts/manifest/compile.d.ts +2 -1
- package/build/dts/manifest/compile.d.ts.map +1 -1
- package/build/dts/manifest/json-schema.d.ts.map +1 -1
- package/build/dts/manifest/manifest.d.ts +362 -0
- package/build/dts/manifest/manifest.d.ts.map +1 -1
- package/build/dts/ports/campaign-predicate.d.ts +24 -0
- package/build/dts/ports/campaign-predicate.d.ts.map +1 -0
- package/build/dts/ports/language.d.ts +2 -0
- package/build/dts/ports/language.d.ts.map +1 -1
- package/build/dts/ports/report-source.d.ts +12 -0
- package/build/dts/ports/report-source.d.ts.map +1 -0
- package/build/dts/ports/syntax-matcher.d.ts +21 -0
- package/build/dts/ports/syntax-matcher.d.ts.map +1 -0
- package/build/dts/testing.d.ts +2 -0
- package/build/dts/testing.d.ts.map +1 -1
- package/build/esm/core/campaigns.js +732 -0
- package/build/esm/core/campaigns.js.map +1 -0
- package/build/esm/core/ledger.js +171 -0
- package/build/esm/core/ledger.js.map +1 -0
- package/build/esm/core/structure.js +5 -2
- package/build/esm/core/structure.js.map +1 -1
- package/build/esm/domain/architecture-config.js +133 -1
- package/build/esm/domain/architecture-config.js.map +1 -1
- package/build/esm/domain/report.js +151 -0
- package/build/esm/domain/report.js.map +1 -0
- package/build/esm/domain/snapshot.js +21 -0
- package/build/esm/domain/snapshot.js.map +1 -1
- package/build/esm/domain/violation.js.map +1 -1
- package/build/esm/index.js +7 -1
- package/build/esm/index.js.map +1 -1
- package/build/esm/infrastructure/campaign-functions.js +67 -0
- package/build/esm/infrastructure/campaign-functions.js.map +1 -0
- package/build/esm/infrastructure/report-source-fake.js +6 -0
- package/build/esm/infrastructure/report-source-fake.js.map +1 -0
- package/build/esm/infrastructure/report-source-live.js +57 -0
- package/build/esm/infrastructure/report-source-live.js.map +1 -0
- package/build/esm/infrastructure/syntax-matcher-fake.js +33 -0
- package/build/esm/infrastructure/syntax-matcher-fake.js.map +1 -0
- package/build/esm/load/policy.js +121 -1
- package/build/esm/load/policy.js.map +1 -1
- package/build/esm/manifest/compile.js +186 -1
- package/build/esm/manifest/compile.js.map +1 -1
- package/build/esm/manifest/json-schema.js +7 -3
- package/build/esm/manifest/json-schema.js.map +1 -1
- package/build/esm/manifest/manifest.js +148 -1
- package/build/esm/manifest/manifest.js.map +1 -1
- package/build/esm/ports/campaign-predicate.js +2 -0
- package/build/esm/ports/campaign-predicate.js.map +1 -0
- package/build/esm/ports/report-source.js +2 -0
- package/build/esm/ports/report-source.js.map +1 -0
- package/build/esm/ports/syntax-matcher.js +12 -0
- package/build/esm/ports/syntax-matcher.js.map +1 -0
- package/build/esm/testing.js +2 -0
- package/build/esm/testing.js.map +1 -1
- package/package.json +1 -1
- package/schema/architecture-node.schema.json +843 -0
- package/schema/architecture.schema.json +1253 -0
- package/schema/conformance.schema.json +118 -3
- package/src/core/campaigns.ts +1006 -0
- package/src/core/ledger.ts +242 -0
- package/src/core/structure.ts +10 -5
- package/src/domain/architecture-config.ts +178 -1
- package/src/domain/facts.ts +9 -4
- package/src/domain/report.ts +186 -0
- package/src/domain/snapshot.ts +52 -0
- package/src/domain/violation.ts +4 -1
- package/src/index.ts +74 -0
- package/src/infrastructure/campaign-functions.ts +98 -0
- package/src/infrastructure/report-source-fake.ts +10 -0
- package/src/infrastructure/report-source-live.ts +66 -0
- package/src/infrastructure/syntax-matcher-fake.ts +51 -0
- package/src/load/policy.ts +187 -1
- package/src/manifest/compile.ts +221 -0
- package/src/manifest/json-schema.ts +7 -3
- package/src/manifest/manifest.ts +192 -1
- package/src/ports/campaign-predicate.ts +30 -0
- package/src/ports/language.ts +6 -0
- package/src/ports/report-source.ts +21 -0
- package/src/ports/syntax-matcher.ts +42 -0
- package/src/testing.ts +2 -0
package/src/domain/snapshot.ts
CHANGED
|
@@ -34,6 +34,7 @@ const VIOLATION_KINDS = [
|
|
|
34
34
|
"member",
|
|
35
35
|
"surface",
|
|
36
36
|
"graph",
|
|
37
|
+
"campaign",
|
|
37
38
|
] as const satisfies ReadonlyArray<ViolationKind>;
|
|
38
39
|
|
|
39
40
|
const Path = describe(Schema.String, "Repo-relative, with forward slashes.");
|
|
@@ -63,6 +64,10 @@ export const SnapshotViolation = Schema.Struct({
|
|
|
63
64
|
),
|
|
64
65
|
message: Schema.String,
|
|
65
66
|
baselined: describe(Schema.Boolean, "Carried by the baseline, so `check` does not fail on it."),
|
|
67
|
+
ledgered: describe(
|
|
68
|
+
Schema.Boolean,
|
|
69
|
+
"For a campaign hit: carried by the campaign's ledger, so `check` does not fail on it.",
|
|
70
|
+
),
|
|
66
71
|
});
|
|
67
72
|
|
|
68
73
|
const AllowanceKind = describe(
|
|
@@ -109,6 +114,48 @@ export const SnapshotVacancy = Schema.Struct({
|
|
|
109
114
|
),
|
|
110
115
|
});
|
|
111
116
|
|
|
117
|
+
// One campaign's burn-down: what its ledger says, and what the clock says
|
|
118
|
+
// about it. Every count is over ledger entries, which are hits by fingerprint.
|
|
119
|
+
export const SnapshotCampaign = Schema.Struct({
|
|
120
|
+
id: describe(Schema.String, "The campaign's id; its ledger is `<ledger>/<id>.json`."),
|
|
121
|
+
title: Schema.optionalKey(describe(Schema.String, "The campaign's title, when it states one.")),
|
|
122
|
+
owner: Schema.optionalKey(
|
|
123
|
+
describe(Schema.String, "Who is running the campaign, as the manifest names them."),
|
|
124
|
+
),
|
|
125
|
+
initial: describe(Schema.Finite, "Entries the day the ledger was written."),
|
|
126
|
+
allowed: describe(
|
|
127
|
+
Schema.Finite,
|
|
128
|
+
"Entries added since by `campaigns allow`, each with a regression record.",
|
|
129
|
+
),
|
|
130
|
+
count: describe(Schema.Finite, "Entries in the ledger now."),
|
|
131
|
+
fixed: describe(
|
|
132
|
+
Schema.Finite,
|
|
133
|
+
"Entries removed by `campaigns prune` since the ledger was written.",
|
|
134
|
+
),
|
|
135
|
+
progress: describe(
|
|
136
|
+
Schema.Finite,
|
|
137
|
+
"`1 - count / (initial + allowed)`: how much of everything ever ledgered has been paid down.",
|
|
138
|
+
),
|
|
139
|
+
lastProgress: describe(
|
|
140
|
+
Schema.String,
|
|
141
|
+
"When an entry last left the ledger, ISO 8601 — the stall clock's reading.",
|
|
142
|
+
),
|
|
143
|
+
regressions: describe(Schema.Finite, "How many times the count was allowed to go up."),
|
|
144
|
+
stalled: describe(
|
|
145
|
+
Schema.Boolean,
|
|
146
|
+
"Entries remain and `lastProgress` is older than the campaign's `staleAfter`.",
|
|
147
|
+
),
|
|
148
|
+
complete: describe(Schema.Boolean, "No entries remain."),
|
|
149
|
+
onComplete: describe(
|
|
150
|
+
Schema.Literals(["keep", "remove"]),
|
|
151
|
+
"What the manifest asks once complete: keep the campaign as a guard, or remove it.",
|
|
152
|
+
),
|
|
153
|
+
ledgered: describe(
|
|
154
|
+
Schema.Boolean,
|
|
155
|
+
"Whether a ledger exists. A campaign with none has been declared and not yet initialised.",
|
|
156
|
+
),
|
|
157
|
+
});
|
|
158
|
+
|
|
112
159
|
export const Snapshot = Schema.Struct({
|
|
113
160
|
version: describe(Schema.Literal(SNAPSHOT_VERSION), "The shape of this document."),
|
|
114
161
|
manifest: describe(
|
|
@@ -187,6 +234,10 @@ export const Snapshot = Schema.Struct({
|
|
|
187
234
|
}),
|
|
188
235
|
'The tiers that said "not tightened yet", by name; `limits` caps how many may.',
|
|
189
236
|
),
|
|
237
|
+
campaigns: describe(
|
|
238
|
+
Schema.Array(SnapshotCampaign),
|
|
239
|
+
"Every campaign the manifest declares, in manifest order, with its burn-down. Campaign hits are not counted in `coverage` or `residue`: a campaign is scoped by construction.",
|
|
240
|
+
),
|
|
190
241
|
});
|
|
191
242
|
|
|
192
243
|
export type Snapshot = typeof Snapshot.Type;
|
|
@@ -194,6 +245,7 @@ export type SnapshotViolation = typeof SnapshotViolation.Type;
|
|
|
194
245
|
export type SnapshotSlack = typeof SnapshotSlack.Type;
|
|
195
246
|
export type SnapshotConcentration = typeof SnapshotConcentration.Type;
|
|
196
247
|
export type SnapshotVacancy = typeof SnapshotVacancy.Type;
|
|
248
|
+
export type SnapshotCampaign = typeof SnapshotCampaign.Type;
|
|
197
249
|
|
|
198
250
|
// Decodes a document some other run wrote — the base of a pull request, a
|
|
199
251
|
// stored one — refusing a key the shape does not declare, so a consumer never
|
package/src/domain/violation.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
|
|
1
|
+
// `campaign` is the one kind whose entries live in a ledger rather than the
|
|
2
|
+
// baseline: a hit is debt a campaign is paying down, not a rule being broken.
|
|
3
|
+
export type ViolationKind =
|
|
4
|
+
"import" | "export" | "structure" | "member" | "surface" | "graph" | "campaign";
|
|
2
5
|
|
|
3
6
|
export type Violation = {
|
|
4
7
|
readonly kind: ViolationKind;
|
package/src/index.ts
CHANGED
|
@@ -14,6 +14,24 @@ export {
|
|
|
14
14
|
staleEntriesOf,
|
|
15
15
|
unbaselined,
|
|
16
16
|
} from "./core/baseline.js";
|
|
17
|
+
export {
|
|
18
|
+
type CampaignHit,
|
|
19
|
+
type CampaignInput,
|
|
20
|
+
campaignsFailingTheirProbe,
|
|
21
|
+
campaignsSelecting,
|
|
22
|
+
compileCampaignRule,
|
|
23
|
+
compileCampaignRules,
|
|
24
|
+
type CompiledCampaign,
|
|
25
|
+
type CompiledDetector,
|
|
26
|
+
evaluateCampaign,
|
|
27
|
+
evaluateCampaigns,
|
|
28
|
+
explainCampaign,
|
|
29
|
+
type FailedProbe,
|
|
30
|
+
leafTermsOf,
|
|
31
|
+
matchKeyOf,
|
|
32
|
+
probeInputOf,
|
|
33
|
+
type TermAnswer,
|
|
34
|
+
} from "./core/campaigns.js";
|
|
17
35
|
export {
|
|
18
36
|
type Coverage,
|
|
19
37
|
type CoverageFamily,
|
|
@@ -64,6 +82,27 @@ export {
|
|
|
64
82
|
rulesSelecting,
|
|
65
83
|
type SelectedRule,
|
|
66
84
|
} from "./core/imports.js";
|
|
85
|
+
export {
|
|
86
|
+
allowed,
|
|
87
|
+
decodeLedger,
|
|
88
|
+
EMPTY_LEDGER,
|
|
89
|
+
entryOf,
|
|
90
|
+
isComplete,
|
|
91
|
+
isStalled,
|
|
92
|
+
type Ledger,
|
|
93
|
+
ledgerArithmeticHolds,
|
|
94
|
+
ledgerOf,
|
|
95
|
+
Ledger as LedgerSchema,
|
|
96
|
+
newEntriesOf,
|
|
97
|
+
progressOf,
|
|
98
|
+
pruned,
|
|
99
|
+
reconcile,
|
|
100
|
+
type Reconciliation,
|
|
101
|
+
type Regression,
|
|
102
|
+
type RegressionRecord,
|
|
103
|
+
serializeLedger,
|
|
104
|
+
staleEntriesOf as staleLedgerEntriesOf,
|
|
105
|
+
} from "./core/ledger.js";
|
|
67
106
|
export {
|
|
68
107
|
type CompiledMemberRule,
|
|
69
108
|
compileMemberRules,
|
|
@@ -96,7 +135,11 @@ export {
|
|
|
96
135
|
export {
|
|
97
136
|
type Allowance,
|
|
98
137
|
type BindingKind,
|
|
138
|
+
type CampaignProbe,
|
|
139
|
+
type CampaignRule,
|
|
140
|
+
type CampaignUnit,
|
|
99
141
|
type DeclarationKind,
|
|
142
|
+
type Detector,
|
|
100
143
|
type ExportFix,
|
|
101
144
|
type ExportRule,
|
|
102
145
|
type GraphConfig,
|
|
@@ -108,6 +151,7 @@ export {
|
|
|
108
151
|
type ImportRule,
|
|
109
152
|
type MemberRule,
|
|
110
153
|
type MemberSubject,
|
|
154
|
+
type ProbeDiagnostic,
|
|
111
155
|
type ResolveConfig,
|
|
112
156
|
type ResolveScope,
|
|
113
157
|
type StructureConfig,
|
|
@@ -131,11 +175,19 @@ export {
|
|
|
131
175
|
type ManifestPosition,
|
|
132
176
|
renderManifestPath,
|
|
133
177
|
} from "./domain/manifest-location.js";
|
|
178
|
+
export {
|
|
179
|
+
type Diagnostic,
|
|
180
|
+
indexByFile,
|
|
181
|
+
parseReport,
|
|
182
|
+
type ParseReportOptions,
|
|
183
|
+
type ReportFormat,
|
|
184
|
+
} from "./domain/report.js";
|
|
134
185
|
export {
|
|
135
186
|
decodeSnapshot,
|
|
136
187
|
type Snapshot,
|
|
137
188
|
SNAPSHOT_SCHEMA_ID,
|
|
138
189
|
SNAPSHOT_VERSION,
|
|
190
|
+
type SnapshotCampaign,
|
|
139
191
|
type SnapshotConcentration,
|
|
140
192
|
snapshotJsonSchema,
|
|
141
193
|
Snapshot as SnapshotSchema,
|
|
@@ -149,6 +201,10 @@ export {
|
|
|
149
201
|
type Violation,
|
|
150
202
|
type ViolationKind,
|
|
151
203
|
} from "./domain/violation.js";
|
|
204
|
+
export {
|
|
205
|
+
loadCampaignFunctions,
|
|
206
|
+
type LoadedCampaignFunctions,
|
|
207
|
+
} from "./infrastructure/campaign-functions.js";
|
|
152
208
|
export { makeFileSystemLive } from "./infrastructure/file-system-live.js";
|
|
153
209
|
export {
|
|
154
210
|
findManifestFile,
|
|
@@ -163,6 +219,7 @@ export {
|
|
|
163
219
|
type IncludeReader,
|
|
164
220
|
type SourceDocument,
|
|
165
221
|
} from "./infrastructure/manifest-include.js";
|
|
222
|
+
export { makeReportSourceLive } from "./infrastructure/report-source-live.js";
|
|
166
223
|
export {
|
|
167
224
|
listPackageRoots,
|
|
168
225
|
listSourceFiles,
|
|
@@ -204,13 +261,23 @@ export {
|
|
|
204
261
|
manifestNodeJsonSchema,
|
|
205
262
|
} from "./manifest/json-schema.js";
|
|
206
263
|
export {
|
|
264
|
+
type CampaignSpec,
|
|
207
265
|
type DecodedManifest,
|
|
208
266
|
decodeManifest,
|
|
209
267
|
type DecodeManifestOptions,
|
|
268
|
+
DEFAULT_LEDGER_DIR,
|
|
269
|
+
type DetectorSpec,
|
|
270
|
+
durationMs,
|
|
210
271
|
type Manifest,
|
|
211
272
|
type ManifestNode,
|
|
212
273
|
Manifest as ManifestSchema,
|
|
213
274
|
} from "./manifest/manifest.js";
|
|
275
|
+
export {
|
|
276
|
+
type CampaignPredicate,
|
|
277
|
+
type CampaignPredicateInput,
|
|
278
|
+
type CampaignSubject,
|
|
279
|
+
type Range,
|
|
280
|
+
} from "./ports/campaign-predicate.js";
|
|
214
281
|
export { type FactExtractor } from "./ports/fact-extractor.js";
|
|
215
282
|
export { type FileSystem } from "./ports/file-system.js";
|
|
216
283
|
export { type Language } from "./ports/language.js";
|
|
@@ -219,3 +286,10 @@ export {
|
|
|
219
286
|
type ModuleResolver,
|
|
220
287
|
type ResolvedTarget,
|
|
221
288
|
} from "./ports/module-resolver.js";
|
|
289
|
+
export { NO_REPORTS, type ReportSource, type ReportSpec } from "./ports/report-source.js";
|
|
290
|
+
export {
|
|
291
|
+
type Position,
|
|
292
|
+
type SyntaxMatch,
|
|
293
|
+
type SyntaxMatcher,
|
|
294
|
+
type SyntaxTree,
|
|
295
|
+
} from "./ports/syntax-matcher.js";
|
|
@@ -0,0 +1,98 @@
|
|
|
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
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
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
|
+
});
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
|
|
5
|
+
import { type Diagnostic, indexByFile, parseReport } from "../domain/report.js";
|
|
6
|
+
import type { ReportSource, ReportSpec } from "../ports/report-source.js";
|
|
7
|
+
|
|
8
|
+
// The report source on this host: runs a `report` term's command from the
|
|
9
|
+
// repository root, or reads the file it names, and parses the output once —
|
|
10
|
+
// a report is about the whole repository, a campaign asks per file, and the
|
|
11
|
+
// plugin asks for one file at a time across a whole lint run. Cached per
|
|
12
|
+
// spec for the life of the process: the CLI is one process per `check`, and
|
|
13
|
+
// oxlint's language server is one process per editor session, which sees the
|
|
14
|
+
// report as of when it loaded the plugin. A report the build writes to a file
|
|
15
|
+
// is the predictable form for the editor.
|
|
16
|
+
//
|
|
17
|
+
// A non-zero exit is not a failure — `tsc` exits 2 when there are errors,
|
|
18
|
+
// which is the case the term exists for. A command that cannot be spawned,
|
|
19
|
+
// or a file that is not there, is.
|
|
20
|
+
|
|
21
|
+
const MAX_BUFFER = 256 * 1024 * 1024;
|
|
22
|
+
|
|
23
|
+
const textOf = (repoRoot: string, spec: ReportSpec): string => {
|
|
24
|
+
if (spec.file !== undefined) {
|
|
25
|
+
const at = path.resolve(repoRoot, spec.file);
|
|
26
|
+
try {
|
|
27
|
+
return readFileSync(at, "utf8");
|
|
28
|
+
} catch (cause) {
|
|
29
|
+
throw new Error(
|
|
30
|
+
`the report file ${spec.file} cannot be read: ${String(cause)}. A \`report\` term's ` +
|
|
31
|
+
`\`file\` is written by an earlier step; run that first, or name a \`command\`.`,
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const command = spec.command ?? "";
|
|
36
|
+
const run = spawnSync(command, {
|
|
37
|
+
cwd: repoRoot,
|
|
38
|
+
shell: true,
|
|
39
|
+
encoding: "utf8",
|
|
40
|
+
maxBuffer: MAX_BUFFER,
|
|
41
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
42
|
+
});
|
|
43
|
+
if (run.error !== undefined) {
|
|
44
|
+
throw new Error(`the report command \`${command}\` could not be run: ${run.error.message}`);
|
|
45
|
+
}
|
|
46
|
+
return run.stdout;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const keyOf = (spec: ReportSpec): string => JSON.stringify(spec);
|
|
50
|
+
|
|
51
|
+
export const makeReportSourceLive = (repoRoot: string): ReportSource => {
|
|
52
|
+
const cache = new Map<string, ReadonlyMap<string, ReadonlyArray<Diagnostic>>>();
|
|
53
|
+
return {
|
|
54
|
+
diagnosticsOf: (spec, file) => {
|
|
55
|
+
const key = keyOf(spec);
|
|
56
|
+
let indexed = cache.get(key);
|
|
57
|
+
if (indexed === undefined) {
|
|
58
|
+
indexed = indexByFile(
|
|
59
|
+
parseReport(spec.format, textOf(repoRoot, spec), { repoRoot, pattern: spec.pattern }),
|
|
60
|
+
);
|
|
61
|
+
cache.set(key, indexed);
|
|
62
|
+
}
|
|
63
|
+
return indexed.get(file) ?? [];
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { SyntaxMatch, SyntaxMatcher } from "../ports/syntax-matcher.js";
|
|
2
|
+
|
|
3
|
+
// One match as a test states it: the text, its captures as a plain object,
|
|
4
|
+
// the anchor, and optionally a line — the rest of the range is filled in.
|
|
5
|
+
// `rule` narrows the match to one rule (compared by its JSON), so a test
|
|
6
|
+
// about two `syntax` terms can answer each differently; omit it to answer
|
|
7
|
+
// every rule the same.
|
|
8
|
+
export type StagedMatch = {
|
|
9
|
+
readonly text: string;
|
|
10
|
+
readonly captures?: Readonly<Record<string, string>>;
|
|
11
|
+
readonly anchor?: string | null;
|
|
12
|
+
readonly line?: number;
|
|
13
|
+
readonly rule?: unknown;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const ZERO = { line: 0, column: 0 };
|
|
17
|
+
|
|
18
|
+
// Keyed by the source text, as the extractor fake is: a core test states
|
|
19
|
+
// what a snippet's syntax yields and never the parse, which has its own
|
|
20
|
+
// tests in the matcher package. A text not staged parses to no matches; a
|
|
21
|
+
// file whose extension is not among `extensions` does not parse at all,
|
|
22
|
+
// which is the shape of a language with no matcher.
|
|
23
|
+
export const makeSyntaxMatcherFake = (
|
|
24
|
+
staged: Readonly<Record<string, ReadonlyArray<StagedMatch>>>,
|
|
25
|
+
extensions: ReadonlyArray<string> | null = null,
|
|
26
|
+
): SyntaxMatcher => ({
|
|
27
|
+
parse: (file, text) => {
|
|
28
|
+
if (extensions !== null && !extensions.some((extension) => file.endsWith(extension))) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
const matches = staged[text] ?? [];
|
|
32
|
+
return {
|
|
33
|
+
// A staged match's line is what a position is anchored by.
|
|
34
|
+
anchorAt: (position) => matches.find((one) => one.line === position.line)?.anchor ?? null,
|
|
35
|
+
findAll: (rule) => {
|
|
36
|
+
const asked = JSON.stringify(rule);
|
|
37
|
+
return matches
|
|
38
|
+
.filter((one) => one.rule === undefined || JSON.stringify(one.rule) === asked)
|
|
39
|
+
.map((one): SyntaxMatch => ({
|
|
40
|
+
text: one.text,
|
|
41
|
+
captures: new Map(Object.entries(one.captures ?? {})),
|
|
42
|
+
range: {
|
|
43
|
+
start: one.line === undefined ? ZERO : { line: one.line, column: 0 },
|
|
44
|
+
end: one.line === undefined ? ZERO : { line: one.line, column: one.text.length },
|
|
45
|
+
},
|
|
46
|
+
anchor: one.anchor ?? null,
|
|
47
|
+
}));
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
},
|
|
51
|
+
});
|