@jpeakai/mermaidjs-lint 0.7.1 → 0.7.2

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.
@@ -0,0 +1,98 @@
1
+ // The element-level structure of a diagram, read from mermaid's parser result:
2
+ // each node, each edge and each subgraph, with what contains what. The stats in
3
+ // stats.ts count the same result, and this keeps the elements themselves, so
4
+ // the docs site can draw what the parser returned beside the score it earns.
5
+ // Only the types the site draws are read, and any other type throws.
6
+
7
+ import { getMermaidCore } from "./stats.ts";
8
+
9
+ export interface GraphNode {
10
+ id: string;
11
+ label: string;
12
+ // The subgraph that holds this node, or null at the top level.
13
+ parent: string | null;
14
+ }
15
+
16
+ export interface GraphEdge {
17
+ source: string;
18
+ target: string;
19
+ }
20
+
21
+ export interface DiagramGraph {
22
+ diagram_type: string;
23
+ nodes: GraphNode[];
24
+ edges: GraphEdge[];
25
+ // A subgraph holds nodes, or other subgraphs through their parent.
26
+ subgraphs: GraphNode[];
27
+ }
28
+
29
+ type AnyDb = Record<string, unknown>;
30
+
31
+ interface FlowVertex {
32
+ id: string;
33
+ text?: string;
34
+ }
35
+ interface FlowEdge {
36
+ start: string;
37
+ end: string;
38
+ }
39
+ interface FlowSubgraph {
40
+ id: string;
41
+ title?: string;
42
+ nodes?: string[];
43
+ }
44
+
45
+ // Mermaid lists each member once, under its innermost subgraph, and a nested
46
+ // subgraph is listed by id as a member of the one around it.
47
+ const flowchartGraph = (type: string, db: AnyDb): DiagramGraph => {
48
+ const vertices = db.vertices instanceof Map ? ([...db.vertices.values()] as FlowVertex[]) : [];
49
+ const edges = (Array.isArray(db.edges) ? db.edges : []) as FlowEdge[];
50
+ const subGraphs = (Array.isArray(db.subGraphs) ? db.subGraphs : []) as FlowSubgraph[];
51
+ const parentOf = new Map<string, string>();
52
+ for (const sg of subGraphs) for (const member of sg.nodes ?? []) parentOf.set(member, sg.id);
53
+ return {
54
+ diagram_type: type,
55
+ nodes: vertices.map((v) => ({ id: v.id, label: v.text ?? v.id, parent: parentOf.get(v.id) ?? null })),
56
+ edges: edges.map((e) => ({ source: e.start, target: e.end })),
57
+ subgraphs: subGraphs.map((sg) => ({ id: sg.id, label: sg.title ?? sg.id, parent: parentOf.get(sg.id) ?? null })),
58
+ };
59
+ };
60
+
61
+ interface MindmapNode {
62
+ id: number;
63
+ descr?: string;
64
+ children?: MindmapNode[];
65
+ }
66
+
67
+ // A mindmap's labels may repeat, so each entry is named by the parser's own id.
68
+ const mindmapGraph = (type: string, db: AnyDb): DiagramGraph => {
69
+ const getRoot = db.getMindmap;
70
+ const root = typeof getRoot === "function" ? (getRoot.call(db) as MindmapNode | null) : null;
71
+ const nodes: GraphNode[] = [];
72
+ const edges: GraphEdge[] = [];
73
+ const walk = (node: MindmapNode, parent: string | null): void => {
74
+ const id = `n${node.id}`;
75
+ nodes.push({ id, label: node.descr ?? id, parent: null });
76
+ if (parent) edges.push({ source: parent, target: id });
77
+ for (const child of node.children ?? []) walk(child, id);
78
+ };
79
+ if (root) walk(root, null);
80
+ return { diagram_type: type, nodes, edges, subgraphs: [] };
81
+ };
82
+
83
+ const READERS: Record<string, (type: string, db: AnyDb) => DiagramGraph> = {
84
+ "flowchart-v2": flowchartGraph,
85
+ flowchart: flowchartGraph,
86
+ graph: flowchartGraph,
87
+ mindmap: mindmapGraph,
88
+ };
89
+
90
+ export const extractGraph = async (content: string): Promise<DiagramGraph> => {
91
+ const mermaid = await getMermaidCore();
92
+ await mermaid.parse(content);
93
+ const diagram = (await mermaid.mermaidAPI.getDiagramFromText(content)) as { db: AnyDb; type?: string };
94
+ const type = diagram.type ?? "unknown";
95
+ const read = READERS[type];
96
+ if (!read) throw new Error(`extractGraph reads ${Object.keys(READERS).join(", ")}, not ${type}`);
97
+ return read(type, diagram.db);
98
+ };
@@ -111,7 +111,7 @@ export const canonicalPreset = (name: string): string | null => {
111
111
  export const configFromPreset = (name: string): ThresholdConfig => {
112
112
  const canonical = canonicalPreset(name);
113
113
  const preset = canonical ? PRESETS[canonical] : undefined;
114
- if (!canonical || !preset) {
114
+ if (!(canonical && preset)) {
115
115
  throw new Error(`Unknown preset '${name}'. Valid: ${PRESET_NAMES.join(", ")}`);
116
116
  }
117
117
  return {
@@ -71,10 +71,18 @@ export interface DiagramStats {
71
71
 
72
72
  type MermaidStats = DiagramStats;
73
73
 
74
- const LANGIUM_TYPES: Record<
75
- string,
76
- "architecture" | "info" | "pie" | "gitGraph" | "packet" | "radar" | "treemap" | "treeView" | "wardley"
77
- > = {
74
+ type LangiumType =
75
+ | "architecture"
76
+ | "info"
77
+ | "pie"
78
+ | "gitGraph"
79
+ | "packet"
80
+ | "radar"
81
+ | "treemap"
82
+ | "treeView"
83
+ | "wardley";
84
+
85
+ const LANGIUM_TYPES: Record<string, LangiumType> = {
78
86
  "architecture-beta": "architecture",
79
87
  architecture: "architecture",
80
88
  info: "info",
@@ -180,7 +188,7 @@ export const parserReason = (err: unknown): string => {
180
188
  const lines = message.split("\n");
181
189
  if (message.startsWith("No diagram type detected")) return (lines[0] ?? "").trim();
182
190
  return lines
183
- .filter((line, i) => !POINTER.test(line.trim()) && !POINTER.test((lines[i + 1] ?? "").trim()))
191
+ .filter((line, i) => !(POINTER.test(line.trim()) || POINTER.test((lines[i + 1] ?? "").trim())))
184
192
  .join(" ")
185
193
  .replace(/\s+/g, " ")
186
194
  .trim();
@@ -217,83 +225,7 @@ async function extractLangiumStats(content: string, keyword: string): Promise<Me
217
225
  const mapped = LANGIUM_TYPES[keyword];
218
226
  if (!mapped) throw new Error(`not a langium keyword: ${keyword}`);
219
227
  const ast = (await langiumParseAny(mapped, content)) as Record<string, unknown>;
220
-
221
- const nodeIds: string[] = [];
222
- const subgraphNames: string[] = [];
223
- let edges = 0;
224
- let maxDepth = 0;
225
- let treeDepth = 0;
226
-
227
- switch (mapped) {
228
- case "architecture": {
229
- const services = (ast.services as Array<{ id?: string }> | undefined) ?? [];
230
- const groups = (ast.groups as Array<{ id?: string; title?: string; in?: string }> | undefined) ?? [];
231
- const edgeArr = (ast.edges as Array<unknown> | undefined) ?? [];
232
- nodeIds.push(...services.map((s) => s.id ?? ""));
233
- subgraphNames.push(...groups.map((g) => g.title ?? g.id ?? ""));
234
- maxDepth = architectureDepth(groups);
235
- edges = edgeArr.length;
236
- break;
237
- }
238
- case "pie": {
239
- const sections = (ast.sections as Array<{ label?: string }> | undefined) ?? [];
240
- nodeIds.push(...sections.map((s, i) => s.label ?? `slice_${i}`));
241
- break;
242
- }
243
- case "packet": {
244
- const blocks = (ast.blocks as Array<unknown> | undefined) ?? [];
245
- nodeIds.push(...blocks.map((_, i) => `block_${i}`));
246
- break;
247
- }
248
- case "gitGraph": {
249
- const statements = (ast.statements as Array<{ $type?: string; id?: string; name?: string }> | undefined) ?? [];
250
- for (const s of statements) {
251
- if (s.$type === "Commit") nodeIds.push(s.id ?? `commit_${nodeIds.length}`);
252
- else if (s.$type === "Branch") subgraphNames.push(s.name ?? `branch_${subgraphNames.length}`);
253
- else if (s.$type === "Merge") edges++;
254
- }
255
- break;
256
- }
257
- case "radar": {
258
- const axes = (ast.axes as Array<{ name?: string }> | undefined) ?? [];
259
- const curves = (ast.curves as Array<{ name?: string }> | undefined) ?? [];
260
- nodeIds.push(...axes.map((a, i) => a.name ?? `axis_${i}`));
261
- nodeIds.push(...curves.map((c, i) => c.name ?? `curve_${i}`));
262
- edges = axes.length * curves.length;
263
- break;
264
- }
265
- case "treeView": {
266
- // AST shape: flat ast.nodes[] with { name, indent }. Tree structure
267
- // derives from indent values; edges = nodes - roots.
268
- const nodes = (ast.nodes as Array<{ name?: string; indent?: number }> | undefined) ?? [];
269
- const stats = indentTreeStats(nodes.map((n, i) => ({ label: n.name ?? `node_${i}`, indent: n.indent ?? 0 })));
270
- nodeIds.push(...stats.labels);
271
- edges = stats.edges;
272
- treeDepth = stats.depth;
273
- break;
274
- }
275
- case "treemap": {
276
- // AST shape: ast.TreemapRows[] with .indent and .item.{Section|Leaf}.name.
277
- const rows = (ast.TreemapRows as Array<{ indent?: number; item?: { name?: string } }> | undefined) ?? [];
278
- const stats = indentTreeStats(rows.map((r, i) => ({ label: r.item?.name ?? `row_${i}`, indent: r.indent ?? 0 })));
279
- nodeIds.push(...stats.labels);
280
- edges = stats.edges;
281
- treeDepth = stats.depth;
282
- break;
283
- }
284
- case "wardley": {
285
- // AST shape: ast.anchors[], ast.components[], ast.links[] — NOT ast.edges.
286
- const components = (ast.components as Array<{ name?: string }> | undefined) ?? [];
287
- const anchors = (ast.anchors as Array<{ name?: string }> | undefined) ?? [];
288
- const links = (ast.links as Array<unknown> | undefined) ?? [];
289
- nodeIds.push(...anchors.map((a, i) => a.name ?? `anchor_${i}`));
290
- nodeIds.push(...components.map((c, i) => c.name ?? `comp_${i}`));
291
- edges = links.length;
292
- break;
293
- }
294
- case "info":
295
- break; // trivial diagram type, no structure
296
- }
228
+ const { nodeIds, edges, subgraphNames, maxDepth, treeDepth = 0 } = LANGIUM_ADAPTERS[mapped](ast);
297
229
 
298
230
  return {
299
231
  nodes: new Set(nodeIds.filter(Boolean)).size,
@@ -309,6 +241,103 @@ async function extractLangiumStats(content: string, keyword: string): Promise<Me
309
241
  };
310
242
  }
311
243
 
244
+ // ─── Per-type Langium AST adapters ──
245
+ // Each adapter reads ONLY the typed AST its grammar produces, the way the
246
+ // mermaid-core adapters below read only their type's DB.
247
+
248
+ type LangiumAdapter = (ast: Record<string, unknown>) => Structure;
249
+
250
+ const LANGIUM_ADAPTERS: Record<LangiumType, LangiumAdapter> = {
251
+ architecture: adaptLangiumArchitecture,
252
+ info: adaptLangiumInfo,
253
+ pie: adaptLangiumPie,
254
+ gitGraph: adaptLangiumGitGraph,
255
+ packet: adaptLangiumPacket,
256
+ radar: adaptLangiumRadar,
257
+ treemap: adaptLangiumTreemap,
258
+ treeView: adaptLangiumTreeView,
259
+ wardley: adaptLangiumWardley,
260
+ };
261
+
262
+ function adaptLangiumArchitecture(ast: Record<string, unknown>): Structure {
263
+ const services = (ast.services as Array<{ id?: string }> | undefined) ?? [];
264
+ const groups = (ast.groups as Array<{ id?: string; title?: string; in?: string }> | undefined) ?? [];
265
+ const edgeArr = (ast.edges as Array<unknown> | undefined) ?? [];
266
+ return {
267
+ nodeIds: services.map((s) => s.id ?? ""),
268
+ edges: edgeArr.length,
269
+ subgraphNames: groups.map((g) => g.title ?? g.id ?? ""),
270
+ maxDepth: architectureDepth(groups),
271
+ };
272
+ }
273
+
274
+ // A trivial diagram type, with no structure.
275
+ function adaptLangiumInfo(): Structure {
276
+ return { nodeIds: [], edges: 0, subgraphNames: [], maxDepth: 0 };
277
+ }
278
+
279
+ function adaptLangiumPie(ast: Record<string, unknown>): Structure {
280
+ const sections = (ast.sections as Array<{ label?: string }> | undefined) ?? [];
281
+ return { nodeIds: sections.map((s, i) => s.label ?? `slice_${i}`), edges: 0, subgraphNames: [], maxDepth: 0 };
282
+ }
283
+
284
+ function adaptLangiumPacket(ast: Record<string, unknown>): Structure {
285
+ const blocks = (ast.blocks as Array<unknown> | undefined) ?? [];
286
+ return { nodeIds: blocks.map((_, i) => `block_${i}`), edges: 0, subgraphNames: [], maxDepth: 0 };
287
+ }
288
+
289
+ function adaptLangiumGitGraph(ast: Record<string, unknown>): Structure {
290
+ const statements = (ast.statements as Array<{ $type?: string; id?: string; name?: string }> | undefined) ?? [];
291
+ const nodeIds: string[] = [];
292
+ const subgraphNames: string[] = [];
293
+ let edges = 0;
294
+ for (const s of statements) {
295
+ if (s.$type === "Commit") nodeIds.push(s.id ?? `commit_${nodeIds.length}`);
296
+ else if (s.$type === "Branch") subgraphNames.push(s.name ?? `branch_${subgraphNames.length}`);
297
+ else if (s.$type === "Merge") edges++;
298
+ }
299
+ return { nodeIds, edges, subgraphNames, maxDepth: 0 };
300
+ }
301
+
302
+ function adaptLangiumRadar(ast: Record<string, unknown>): Structure {
303
+ const axes = (ast.axes as Array<{ name?: string }> | undefined) ?? [];
304
+ const curves = (ast.curves as Array<{ name?: string }> | undefined) ?? [];
305
+ return {
306
+ nodeIds: [...axes.map((a, i) => a.name ?? `axis_${i}`), ...curves.map((c, i) => c.name ?? `curve_${i}`)],
307
+ edges: axes.length * curves.length,
308
+ subgraphNames: [],
309
+ maxDepth: 0,
310
+ };
311
+ }
312
+
313
+ function adaptLangiumTreeView(ast: Record<string, unknown>): Structure {
314
+ // AST shape: flat ast.nodes[] with { name, indent }. Tree structure
315
+ // derives from indent values; edges = nodes - roots.
316
+ const nodes = (ast.nodes as Array<{ name?: string; indent?: number }> | undefined) ?? [];
317
+ const stats = indentTreeStats(nodes.map((n, i) => ({ label: n.name ?? `node_${i}`, indent: n.indent ?? 0 })));
318
+ return { nodeIds: stats.labels, edges: stats.edges, subgraphNames: [], maxDepth: 0, treeDepth: stats.depth };
319
+ }
320
+
321
+ function adaptLangiumTreemap(ast: Record<string, unknown>): Structure {
322
+ // AST shape: ast.TreemapRows[] with .indent and .item.{Section|Leaf}.name.
323
+ const rows = (ast.TreemapRows as Array<{ indent?: number; item?: { name?: string } }> | undefined) ?? [];
324
+ const stats = indentTreeStats(rows.map((r, i) => ({ label: r.item?.name ?? `row_${i}`, indent: r.indent ?? 0 })));
325
+ return { nodeIds: stats.labels, edges: stats.edges, subgraphNames: [], maxDepth: 0, treeDepth: stats.depth };
326
+ }
327
+
328
+ function adaptLangiumWardley(ast: Record<string, unknown>): Structure {
329
+ // AST shape: ast.anchors[], ast.components[], ast.links[] — NOT ast.edges.
330
+ const components = (ast.components as Array<{ name?: string }> | undefined) ?? [];
331
+ const anchors = (ast.anchors as Array<{ name?: string }> | undefined) ?? [];
332
+ const links = (ast.links as Array<unknown> | undefined) ?? [];
333
+ return {
334
+ nodeIds: [...anchors.map((a, i) => a.name ?? `anchor_${i}`), ...components.map((c, i) => c.name ?? `comp_${i}`)],
335
+ edges: links.length,
336
+ subgraphNames: [],
337
+ maxDepth: 0,
338
+ };
339
+ }
340
+
312
341
  function architectureDepth(groups: Array<{ id?: string; in?: string }>): number {
313
342
  const parentOf = new Map<string, string | undefined>();
314
343
  for (const g of groups) parentOf.set(g.id ?? "", g.in);
@@ -79,23 +79,21 @@ const localSources = async (root: string): Promise<PluginSource[]> => {
79
79
 
80
80
  // An approximation of Node resolution, read from a package's package.json:
81
81
  // exports "." by node, import, default priority, then main, then index.js.
82
+ const CONDITIONS = ["node", "import", "default"];
83
+
84
+ // The first candidate that resolves to a target, in order.
85
+ const pickFirst = (candidates: unknown[]): string | null => {
86
+ for (const item of candidates) {
87
+ const found = pick(item);
88
+ if (found) return found;
89
+ }
90
+ return null;
91
+ };
92
+
82
93
  const pick = (target: unknown): string | null => {
83
94
  if (typeof target === "string") return target;
84
- if (Array.isArray(target)) {
85
- for (const item of target) {
86
- const found = pick(item);
87
- if (found) return found;
88
- }
89
- return null;
90
- }
91
- if (isRecord(target)) {
92
- for (const condition of ["node", "import", "default"]) {
93
- if (condition in target) {
94
- const found = pick(target[condition]);
95
- if (found) return found;
96
- }
97
- }
98
- }
95
+ if (Array.isArray(target)) return pickFirst(target);
96
+ if (isRecord(target)) return pickFirst(CONDITIONS.filter((c) => c in target).map((c) => target[c]));
99
97
  return null;
100
98
  };
101
99
 
@@ -12,7 +12,7 @@ import { PluginConflictError, PluginLoadError } from "../errors.ts";
12
12
  import { BUILTIN_CATEGORIES, type LoadedSource } from "../rules/registry.ts";
13
13
  import type { Rule } from "../rules/types.ts";
14
14
  import type { Plugin } from "./contract.ts";
15
- import { discoverSources, LOCAL_RULES, type SourceKind } from "./discover.ts";
15
+ import { discoverSources, LOCAL_RULES, type PluginSource, type SourceKind } from "./discover.ts";
16
16
  import { loadLocal, loadPackage, toRule } from "./load.ts";
17
17
 
18
18
  export type PluginMode = "off" | "strict" | "lenient" | "collect";
@@ -33,27 +33,17 @@ export interface PluginSet {
33
33
  failures: PluginFailure[];
34
34
  }
35
35
 
36
- export const loadPlugins = async (root: string, config: Config, mode: PluginMode): Promise<PluginSet> => {
37
- const { sources: found, failures: unresolved } = await discoverSources(root, config, mode === "off");
38
- const set: PluginSet = {
39
- rules: [],
40
- categories: new Map(BUILTIN_CATEGORIES),
41
- sources: [],
42
- skipped: [],
43
- failures: [],
44
- };
45
- if (mode === "off") {
46
- return { ...set, skipped: [...found.map((s) => s.spec), ...unresolved.map((u) => u.error.source)] };
47
- }
36
+ type Settle = (error: PluginLoadError, kind: SourceKind) => void;
48
37
 
49
- // Strict stops at the first failure. The others record it and carry on.
50
- const settle = (error: PluginLoadError, kind: SourceKind): void => {
51
- if (mode === "strict") throw error;
52
- if (!set.failures.some((f) => f.source === error.source)) set.failures.push({ source: error.source, kind, error });
53
- };
54
- for (const { error, kind } of unresolved) settle(error, kind);
38
+ interface LoadedPlugin {
39
+ plugin: Plugin;
40
+ kind: SourceKind;
41
+ spec: string;
42
+ }
55
43
 
56
- const loaded: { plugin: Plugin; kind: SourceKind; spec: string }[] = [];
44
+ // Local files load as one plugin, then each package or config source on its own.
45
+ const loadAll = async (found: PluginSource[], settle: Settle): Promise<LoadedPlugin[]> => {
46
+ const loaded: LoadedPlugin[] = [];
57
47
  const locals = found.filter((s) => s.kind === "local");
58
48
  if (locals.length > 0) {
59
49
  const local = await loadLocal(locals);
@@ -68,34 +58,73 @@ export const loadPlugins = async (root: string, config: Config, mode: PluginMode
68
58
  settle(err, source.kind);
69
59
  }
70
60
  }
61
+ return loaded;
62
+ };
63
+
64
+ // Who already holds each namespace and each new category word.
65
+ interface Claims {
66
+ namespaces: Map<string, string>;
67
+ declaredBy: Map<string, string>;
68
+ }
71
69
 
72
- // Namespaces and new categories are global. The first plugin to claim one
73
- // keeps it, and a later claimant fails, naming both.
74
- const namespaces = new Map<string, string>();
75
- const declaredBy = new Map<string, string>();
76
- const accepted: typeof loaded = [];
70
+ // The reason a plugin cannot claim its namespace or categories, or null when
71
+ // it can.
72
+ const conflictOf = (plugin: Plugin, spec: string, claims: Claims): PluginConflictError | null => {
73
+ const { namespace } = plugin.meta;
74
+ const words = Object.keys(plugin.meta.categories ?? {});
75
+ const taken = claims.namespaces.get(namespace);
76
+ const clash = words.find((w) => claims.declaredBy.has(w));
77
+ if (!(taken || clash)) return null;
78
+ const other = (taken ?? claims.declaredBy.get(clash as string)) as string;
79
+ const reason = taken
80
+ ? `namespace "${namespace}" is already used by ${other}`
81
+ : `category "${clash}" is already declared by ${other}`;
82
+ return new PluginConflictError(spec, other, reason);
83
+ };
84
+
85
+ // Namespaces and new categories are global. The first plugin to claim one
86
+ // keeps it, and a later claimant fails, naming both.
87
+ const acceptClaims = (loaded: LoadedPlugin[], set: PluginSet, settle: Settle): LoadedPlugin[] => {
88
+ const claims: Claims = { namespaces: new Map(), declaredBy: new Map() };
89
+ const accepted: LoadedPlugin[] = [];
77
90
  for (const entry of loaded) {
78
91
  const { plugin, spec, kind } = entry;
79
- const { namespace } = plugin.meta;
80
- const words = Object.keys(plugin.meta.categories ?? {});
81
- const taken = namespaces.get(namespace);
82
- const clash = words.find((w) => declaredBy.has(w));
83
- if (taken || clash) {
84
- const other = (taken ?? declaredBy.get(clash as string)) as string;
85
- const reason = taken
86
- ? `namespace "${namespace}" is already used by ${other}`
87
- : `category "${clash}" is already declared by ${other}`;
88
- settle(new PluginConflictError(spec, other, reason), kind);
92
+ const conflict = conflictOf(plugin, spec, claims);
93
+ if (conflict) {
94
+ settle(conflict, kind);
89
95
  continue;
90
96
  }
91
- namespaces.set(namespace, spec);
97
+ claims.namespaces.set(plugin.meta.namespace, spec);
92
98
  for (const [word, description] of Object.entries(plugin.meta.categories ?? {})) {
93
- declaredBy.set(word, spec);
99
+ claims.declaredBy.set(word, spec);
94
100
  set.categories.set(word, description);
95
101
  }
96
102
  accepted.push(entry);
97
103
  }
104
+ return accepted;
105
+ };
106
+
107
+ export const loadPlugins = async (root: string, config: Config, mode: PluginMode): Promise<PluginSet> => {
108
+ const { sources: found, failures: unresolved } = await discoverSources(root, config, mode === "off");
109
+ const set: PluginSet = {
110
+ rules: [],
111
+ categories: new Map(BUILTIN_CATEGORIES),
112
+ sources: [],
113
+ skipped: [],
114
+ failures: [],
115
+ };
116
+ if (mode === "off") {
117
+ return { ...set, skipped: [...found.map((s) => s.spec), ...unresolved.map((u) => u.error.source)] };
118
+ }
119
+
120
+ // Strict stops at the first failure. The others record it and carry on.
121
+ const settle: Settle = (error, kind) => {
122
+ if (mode === "strict") throw error;
123
+ if (!set.failures.some((f) => f.source === error.source)) set.failures.push({ source: error.source, kind, error });
124
+ };
125
+ for (const { error, kind } of unresolved) settle(error, kind);
98
126
 
127
+ const accepted = acceptClaims(await loadAll(found, settle), set, settle);
99
128
  for (const { plugin, kind, spec } of accepted) {
100
129
  const rules = Object.entries(plugin.rules).map(([key, rule]) => toRule(plugin.meta.namespace, key, rule));
101
130
  set.rules.push(...rules);