@cplieger/deadset-ts 3.4.0 → 3.6.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 (60) hide show
  1. package/README.md +1 -1
  2. package/dist/src/analysis.js +70 -14
  3. package/dist/src/components.js +66 -107
  4. package/dist/src/config.js +6 -0
  5. package/dist/src/dependencies.js +493 -0
  6. package/dist/src/deprecation.js +83 -0
  7. package/dist/src/discover.js +1 -1
  8. package/dist/src/enum-group.js +217 -0
  9. package/dist/src/file-facts.js +101 -0
  10. package/dist/src/findings/boundary.js +1 -0
  11. package/dist/src/findings/completion.js +99 -0
  12. package/dist/src/findings/dependencies-and-module-machinery.js +65 -1
  13. package/dist/src/findings/emitters.js +8 -0
  14. package/dist/src/findings/interfaces.js +95 -2
  15. package/dist/src/findings/non-code-artifacts.js +97 -1
  16. package/dist/src/findings/reads-and-writes.js +199 -2
  17. package/dist/src/findings/unused-declarations.js +182 -1
  18. package/dist/src/findings/visibility-narrowing.js +239 -2
  19. package/dist/src/implementations.js +210 -0
  20. package/dist/src/inventory.js +2 -2
  21. package/dist/src/kinds.js +40 -0
  22. package/dist/src/print.js +10 -1
  23. package/dist/src/resolve.js +91 -10
  24. package/dist/src/run.js +9 -3
  25. package/dist/src/schema.js +37 -1
  26. package/dist/src/scope.js +55 -21
  27. package/dist/src/serialization-contract.js +109 -19
  28. package/dist/src/stores.js +32 -0
  29. package/dist/src/version.js +1 -1
  30. package/jsr.json +1 -1
  31. package/package.json +1 -1
  32. package/src/analysis.ts +113 -16
  33. package/src/components.ts +73 -131
  34. package/src/config.ts +37 -2
  35. package/src/dependencies.ts +617 -0
  36. package/src/deprecation.ts +101 -0
  37. package/src/discover.ts +1 -1
  38. package/src/enum-group.ts +265 -0
  39. package/src/file-facts.ts +143 -0
  40. package/src/findings/boundary.ts +39 -0
  41. package/src/findings/completion.ts +151 -0
  42. package/src/findings/dependencies-and-module-machinery.ts +79 -1
  43. package/src/findings/emitter.ts +21 -1
  44. package/src/findings/emitters.ts +13 -1
  45. package/src/findings/interfaces.ts +149 -3
  46. package/src/findings/non-code-artifacts.ts +124 -2
  47. package/src/findings/reads-and-writes.ts +310 -3
  48. package/src/findings/unused-declarations.ts +223 -2
  49. package/src/findings/visibility-narrowing.ts +324 -3
  50. package/src/implementations.ts +286 -0
  51. package/src/inventory.ts +2 -2
  52. package/src/kinds.ts +41 -0
  53. package/src/print.ts +17 -1
  54. package/src/resolve.ts +124 -10
  55. package/src/run.ts +10 -3
  56. package/src/schema.ts +47 -2
  57. package/src/scope.ts +63 -22
  58. package/src/serialization-contract.ts +138 -20
  59. package/src/stores.ts +77 -0
  60. package/src/version.ts +1 -1
package/src/analysis.ts CHANGED
@@ -11,7 +11,10 @@
11
11
  import type { Diagnostic } from "@typescript/native/unstable/sync";
12
12
  import type { Config, Provenance } from "./config.ts";
13
13
  import { decorator } from "./decorator.ts";
14
+ import { dependenciesOf, projectNeeds } from "./dependencies.ts";
15
+ import { deprecatedDeclarations } from "./deprecation.ts";
14
16
  import { diagnosticErrors, discoverProjects, DiscoveryError } from "./discover.ts";
17
+ import { enumGroup } from "./enum-group.ts";
15
18
  import {
16
19
  computeExemptions,
17
20
  disabledClasses,
@@ -21,9 +24,12 @@ import {
21
24
  type Detectors,
22
25
  } from "./exempt.ts";
23
26
  import type { TSExemptionClass } from "./exempt-classes.ts";
27
+ import { heldByInclusion, sourceTree } from "./file-facts.ts";
24
28
  import type { Finding } from "./finding.ts";
29
+ import type { EmitterInput } from "./findings/emitter.ts";
25
30
  import { frameworkLifecycle } from "./framework-lifecycle.ts";
26
31
  import type { Host } from "./host.ts";
32
+ import { implementations, mergeImplementations } from "./implementations.ts";
27
33
  import { injectionContainer } from "./injection-container.ts";
28
34
  import { interfaceSatisfaction } from "./interface-satisfaction.ts";
29
35
  import { inventory, type Inventory } from "./inventory.ts";
@@ -37,6 +43,7 @@ import { roots, unmatchedEverywhere, unmatchedRoots, type RootKind, type Roots }
37
43
  import type { Scope } from "./scope.ts";
38
44
  import { serializationContract } from "./serialization-contract.ts";
39
45
  import { diagnosticsOf, runSession, type Engine, type ProjectView } from "./session.ts";
46
+ import { accessorsOf, storesOf } from "./stores.ts";
40
47
  import type { Exemption, SweepInput } from "./sweep.ts";
41
48
  import { readTemplates, templateField } from "./template-field.ts";
42
49
 
@@ -222,6 +229,7 @@ export interface RunSweep {
222
229
  /** The exemption classes this analyzer detects, each by its detector; any other retains nothing. */
223
230
  const DETECTORS: Detectors = new Map<TSExemptionClass, Detector>([
224
231
  ["interface-satisfaction", interfaceSatisfaction],
232
+ ["enum-group", enumGroup],
225
233
  ["template-field", templateField],
226
234
  ["reflective-lookup", reflectiveLookup],
227
235
  ["decorator", decorator],
@@ -230,31 +238,40 @@ const DETECTORS: Detectors = new Map<TSExemptionClass, Detector>([
230
238
  ["serialization-contract", serializationContract],
231
239
  ]);
232
240
 
241
+ /** What the sweep of one run reads of each project, beside what a caller's stage adds. */
242
+ interface SweptProject<Extra> {
243
+ readonly configured: Configured;
244
+ readonly exempt: readonly Exemption[];
245
+ readonly extra: Extra;
246
+ }
247
+
248
+ /** The run's sweep, the exemption records it was swept with, and each project's extra answer. */
249
+ interface SweptRun<Extra> {
250
+ readonly swept: RunSweep;
251
+ readonly exempt: readonly Exemption[];
252
+ readonly projects: readonly SweptProject<Extra>[];
253
+ }
254
+
233
255
  /**
234
- * Sweeps the run the scope and the configuration describe. Each project's references
235
- * and exemptions are read against its own inventory while its view is open; the
236
- * projects then merge into one matrix in the run's order, which is swept under the
237
- * caller's marks and mode with the exemptions every project found. A record found in
238
- * one configuration holds in every configuration that holds its declaration, because
239
- * an exemption is evidence of a use the analysis cannot see and a use in one
240
- * configuration is a use. A project that carries an error ends the run before any
241
- * project is swept.
256
+ * Sweeps the run, reading each project's references and exemptions, and `extra`'s
257
+ * answer, while the project's view is open.
242
258
  */
243
- export function runSweep(
259
+ function sweepRun<Extra>(
244
260
  engine: Engine,
245
261
  host: Host,
246
262
  scope: Scope,
247
263
  config: Config,
248
264
  input: Pick<SweepInput, "marked" | "mode">,
249
- detectors: Detectors = DETECTORS,
250
- ): RunSweep {
265
+ detectors: Detectors,
266
+ extra: <Brand>(project: ProjectView<Brand>, read: ProjectRead) => Extra,
267
+ ): SweptRun<Extra> {
251
268
  const targetRoot = scope.target.path;
252
269
  const disabled = disabledClasses(config);
253
270
  const templates = disabled.has("template-field")
254
271
  ? { delimiters: config.analysis.templateDelimiters, files: [] }
255
272
  : readTemplates(host, targetRoot, config.analysis);
256
273
  const consumers = scope.consumers.map((consumer) => consumer.path);
257
- const read = readProjects(engine, host, scope, config, (project, projectRead) => {
274
+ const projects = readProjects(engine, host, scope, config, (project, projectRead) => {
258
275
  const resolved = references(project, projectRead.held, targetRoot, {
259
276
  testFiles: config.ts.testFiles,
260
277
  });
@@ -274,9 +291,89 @@ export function runSweep(
274
291
  testFiles: new Set(resolved.testFilePaths),
275
292
  },
276
293
  );
277
- return { configured, exempt };
294
+ return { configured, exempt, extra: extra(project, projectRead) };
278
295
  });
279
- const matrix = matrixOf(read.map((one) => one.configured));
280
- const swept: SweepInput = { ...input, exempt: exemptionsOf(read.flatMap((one) => one.exempt)) };
281
- return { matrix, sweep: sweepMatrix(matrix, swept), retained: retainedIn(matrix, swept) };
296
+ const matrix = matrixOf(projects.map((one) => one.configured));
297
+ const exempt = exemptionsOf(projects.flatMap((one) => one.exempt));
298
+ const swept: SweepInput = { ...input, exempt };
299
+ return {
300
+ swept: { matrix, sweep: sweepMatrix(matrix, swept), retained: retainedIn(matrix, swept) },
301
+ exempt,
302
+ projects,
303
+ };
304
+ }
305
+
306
+ /**
307
+ * Sweeps the run the scope and the configuration describe. Each project's references
308
+ * and exemptions are read against its own inventory while its view is open; the
309
+ * projects then merge into one matrix in the run's order, which is swept under the
310
+ * caller's marks and mode with the exemptions every project found. A record found in
311
+ * one configuration holds in every configuration that holds its declaration, because
312
+ * an exemption is evidence of a use the analysis cannot see and a use in one
313
+ * configuration is a use. A project that carries an error ends the run before any
314
+ * project is swept.
315
+ */
316
+ export function runSweep(
317
+ engine: Engine,
318
+ host: Host,
319
+ scope: Scope,
320
+ config: Config,
321
+ input: Pick<SweepInput, "marked" | "mode">,
322
+ detectors: Detectors = DETECTORS,
323
+ ): RunSweep {
324
+ return sweepRun(engine, host, scope, config, input, detectors, () => undefined).swept;
325
+ }
326
+
327
+ /**
328
+ * What every emitter reads of the run the scope and the configuration describe: the
329
+ * run's sweep, and the facts beside it each kind family reads, every per-project one
330
+ * read in the same session as the sweep. The run loads no consumer and reads no
331
+ * declared cross-language edge, so its boundary names neither.
332
+ */
333
+ export function runEmitterInput(
334
+ engine: Engine,
335
+ host: Host,
336
+ scope: Scope,
337
+ config: Config,
338
+ input: Pick<SweepInput, "marked" | "mode">,
339
+ detectors: Detectors = DETECTORS,
340
+ ): EmitterInput {
341
+ const targetRoot = scope.target.path;
342
+ const run = sweepRun(engine, host, scope, config, input, detectors, (project, read) => ({
343
+ deprecated: deprecatedDeclarations(project, read.held),
344
+ accessors: accessorsOf(project, read.held),
345
+ needs: projectNeeds(project, read.held, host),
346
+ implementations: implementations(project, read.held),
347
+ heldByInclusion: heldByInclusion(project, targetRoot),
348
+ }));
349
+ const { swept, projects } = run;
350
+ return {
351
+ config,
352
+ swept,
353
+ deprecated: new Set(projects.flatMap((one) => one.extra.deprecated)),
354
+ stores: storesOf(
355
+ projects.map((one) => ({
356
+ references: one.configured.references,
357
+ accessors: one.extra.accessors,
358
+ })),
359
+ run.exempt,
360
+ input.mode,
361
+ ),
362
+ dependencies: dependenciesOf(
363
+ host,
364
+ targetRoot,
365
+ projects.map((one) => one.extra.needs),
366
+ swept.sweep.candidates.map((candidate) => candidate.id),
367
+ ),
368
+ implementations: mergeImplementations(projects.map((one) => one.extra.implementations)),
369
+ files: {
370
+ tree: sourceTree(host, targetRoot),
371
+ heldByInclusion: new Set(projects.flatMap((one) => one.extra.heldByInclusion)),
372
+ },
373
+ boundary: {
374
+ consumers: { declared: scope.consumers.map((consumer) => consumer.id), loaded: [] },
375
+ encapsulated: readManifest(host, targetRoot).declaresExports,
376
+ edges: [],
377
+ },
378
+ };
282
379
  }
package/src/components.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
- * The dead components of one graph: the strongly connected components of the subgraph
3
- * the dead declarations induce, ordered so a report is worked from the top down, each
4
- * with the root members a deletion starts at and what falls with it.
2
+ * The dead components of one graph. A component is a set of dead declarations one
3
+ * deletion removes together: its root members, which are where the deletion starts,
4
+ * and every dead declaration that falls with them, being dead only through them.
5
5
  */
6
6
 
7
7
  import type { Cascade } from "./config.ts";
@@ -14,28 +14,30 @@ const ANALYZER = "deadset-ts";
14
14
  /** The fewest digits a component identifier's number is written with. */
15
15
  const ID_DIGITS = 4;
16
16
 
17
+ /** The identifier of the component at one place of the run's order, counted from one. */
18
+ export function componentId(place: number): string {
19
+ return `${ANALYZER}/c-${String(place).padStart(ID_DIGITS, "0")}`;
20
+ }
21
+
17
22
  /** The index of a declaration the component walk has not reached. */
18
23
  const UNVISITED = -1;
19
24
 
20
- /** One dead component and its place in the acyclic graph over the components. */
25
+ /** One dead component. */
21
26
  export interface Component {
22
27
  /** The analyzer's name, a solidus, and `c-` followed by the component's place in the order. */
23
28
  readonly id: string;
24
- /** The component's declarations, by site. A dead member of a dead container is one of them. */
25
- readonly members: readonly string[];
26
29
  /**
27
- * The members no other dead component references and whose container is not dead,
28
- * which are where a deletion starts. A cycle has as many as it has members no dead
29
- * component outside it references.
30
+ * The component's declarations by site: its root members and every dead declaration
31
+ * that falls with them. A dead member of a dead container is one of them.
30
32
  */
31
- readonly roots: readonly string[];
33
+ readonly members: readonly string[];
32
34
  /**
33
- * What one deletion of the component removes: its members, and every dead declaration
34
- * only this component reaches. A dead declaration two components both reach falls with
35
- * neither and is counted by its own component.
35
+ * The root members, by site. A root member is a member of a cycle no dead declaration
36
+ * outside the cycle references, whose container is not dead; every member of such a
37
+ * cycle with a live container is one, so a component has at least one.
36
38
  */
37
- readonly falls: readonly string[];
38
- /** The source lines the deletion removes: the distinct lines {@link Component.falls} spans. */
39
+ readonly roots: readonly string[];
40
+ /** The source lines one deletion of the component removes: the distinct lines its members span. */
39
41
  readonly deletableLines: number;
40
42
  }
41
43
 
@@ -48,22 +50,22 @@ export interface Listing {
48
50
  readonly deletableLines: number;
49
51
  }
50
52
 
51
- /** What a report carries for one component: its roots and what falls, and its members in full. */
53
+ /** What a report carries for one component: its roots and counts, and its members in full. */
52
54
  export function listing(component: Component, cascade: Cascade): Listing {
53
55
  return {
54
56
  roots: component.roots,
55
57
  members: cascade === "full" ? component.members : [],
56
- symbolCount: component.falls.length,
58
+ symbolCount: component.members.length,
57
59
  deletableLines: component.deletableLines,
58
60
  };
59
61
  }
60
62
 
61
63
  /**
62
- * Groups the declarations `dead` marks into components and orders them so each precedes
63
- * every component it reaches, two that neither reaches ordered by their first member's
64
- * site. `dead` and `testOfDeadCode` hold one flag per declaration of the graph; the dead
65
- * set arrives rather than being read from a sweep, so a set several sweeps intersect is
66
- * grouped over this graph's edges the same way.
64
+ * Groups the declarations `dead` marks into components, ordered by their first member's
65
+ * site; `dead` and `testOfDeadCode` hold one flag per declaration of the graph. Root
66
+ * members sit on the cycles no other cycle references. A cycle two roots both reach is
67
+ * dead only through both, so a component is a weakly connected part of the dead
68
+ * subgraph: no dead declaration of one references a declaration of another.
67
69
  */
68
70
  export function componentsOf(
69
71
  graph: Graph,
@@ -75,26 +77,22 @@ export function componentsOf(
75
77
  return [];
76
78
  }
77
79
  const { componentOf, members } = connected(adjacent);
78
- const { edges, into } = condense(adjacent, componentOf, members.length);
79
- const falls = fallSets(edges, members, into);
80
- const outsideReference = externallyReferenced(adjacent, componentOf);
80
+ const referenced = referencedCycles(adjacent, componentOf, members.length);
81
+ const groups = joined(adjacent);
81
82
  const idOf = (position: number): string => graph.symbols[at[position] ?? 0]?.id ?? "";
82
83
 
83
- return order(edges, members).map((group, place) => {
84
- const own = members[group] ?? [];
85
- const roots = own.filter((position) => {
86
- // A dead member of a dead container is never a root: the container is where the
87
- // deletion starts and the member falls with it.
84
+ return groups.map((group, place) => {
85
+ const roots = group.filter((position) => {
86
+ // A dead member's dead container is on the member's own cycle, and the container
87
+ // is where the deletion starts.
88
88
  const container = graph.parent[at[position] ?? 0] ?? OUTSIDE;
89
- return outsideReference[position] !== true && dead[container] !== true;
89
+ return referenced[componentOf[position] ?? 0] !== true && dead[container] !== true;
90
90
  });
91
- const fallen = falls[group] ?? [];
92
91
  return {
93
- id: `${ANALYZER}/c-${String(place + 1).padStart(ID_DIGITS, "0")}`,
94
- members: own.map(idOf),
92
+ id: componentId(place + 1),
93
+ members: group.map(idOf),
95
94
  roots: roots.map(idOf),
96
- falls: fallen.map(idOf),
97
- deletableLines: linesSpanned(fallen.map((position) => graph.symbols[at[position] ?? 0])),
95
+ deletableLines: linesSpanned(group.map((position) => graph.symbols[at[position] ?? 0])),
98
96
  };
99
97
  });
100
98
  }
@@ -185,10 +183,9 @@ interface Frame {
185
183
  }
186
184
 
187
185
  /**
188
- * Each declaration's component and each component's members by site, by Tarjan's
186
+ * Each declaration's cycle and each cycle's members by site, by Tarjan's
189
187
  * algorithm in one pass. The walk keeps its own stack of frames, so a chain of any length
190
- * is walked without growing the call stack. Components are closed in an order in which
191
- * each follows every component it reaches.
188
+ * is walked without growing the call stack.
192
189
  */
193
190
  function connected(adjacent: readonly (readonly number[])[]): {
194
191
  readonly componentOf: readonly number[];
@@ -257,45 +254,18 @@ function connected(adjacent: readonly (readonly number[])[]): {
257
254
  return { componentOf, members };
258
255
  }
259
256
 
260
- /**
261
- * Per component, the components it reaches in one step, and how many components reach
262
- * it, each pair counted once. An edge inside a component is not an edge between two, so
263
- * the result is acyclic.
264
- */
265
- function condense(
257
+ /** Per cycle, whether a dead declaration outside it references one of its members. */
258
+ function referencedCycles(
266
259
  adjacent: readonly (readonly number[])[],
267
260
  componentOf: readonly number[],
268
261
  count: number,
269
- ): { readonly edges: readonly (readonly number[])[]; readonly into: readonly number[] } {
270
- const edges: number[][] = Array.from({ length: count }, () => []);
271
- const into = Array.from({ length: count }, () => 0);
272
- const seen = new Set<string>();
273
- adjacent.forEach((targets, from) => {
274
- for (const to of targets) {
275
- const a = componentOf[from] ?? 0;
276
- const b = componentOf[to] ?? 0;
277
- const pair = `${String(a)}>${String(b)}`;
278
- if (a === b || seen.has(pair)) {
279
- continue;
280
- }
281
- seen.add(pair);
282
- edges[a]?.push(b);
283
- into[b] = (into[b] ?? 0) + 1;
284
- }
285
- });
286
- return { edges, into };
287
- }
288
-
289
- /** Per dead declaration, whether a dead component other than its own references it. */
290
- function externallyReferenced(
291
- adjacent: readonly (readonly number[])[],
292
- componentOf: readonly number[],
293
262
  ): readonly boolean[] {
294
- const referenced = adjacent.map(() => false);
263
+ const referenced = Array.from({ length: count }, () => false);
295
264
  adjacent.forEach((targets, from) => {
296
265
  for (const to of targets) {
297
- if (componentOf[from] !== componentOf[to]) {
298
- referenced[to] = true;
266
+ const cycle = componentOf[to] ?? 0;
267
+ if (componentOf[from] !== cycle) {
268
+ referenced[cycle] = true;
299
269
  }
300
270
  }
301
271
  });
@@ -303,70 +273,42 @@ function externallyReferenced(
303
273
  }
304
274
 
305
275
  /**
306
- * The components in the order a report is worked in. Tarjan closes each component after
307
- * every one it reaches, so reading the closing order backwards is a topological order,
308
- * and one pass over it gives each component its depth below the roots.
309
- */
310
- function order(
311
- edges: readonly (readonly number[])[],
312
- members: readonly (readonly number[])[],
313
- ): readonly number[] {
314
- const sequence = edges.map((_targets, component) => component).reverse();
315
- const depth = edges.map(() => 0);
316
- for (const from of sequence) {
317
- for (const to of edges[from] ?? []) {
318
- depth[to] = Math.max(depth[to] ?? 0, (depth[from] ?? 0) + 1);
319
- }
320
- }
321
- return sequence.sort(
322
- (a, b) => (depth[a] ?? 0) - (depth[b] ?? 0) || (members[a]?.[0] ?? 0) - (members[b]?.[0] ?? 0),
323
- );
324
- }
325
-
326
- /**
327
- * Per component, the dead declarations one deletion of it removes. A root of the
328
- * condensation carries its members and every component only it reaches; any other
329
- * component carries its own members alone, because what it reaches is reached through a
330
- * root above it as well.
276
+ * The weakly connected parts of the dead subgraph, each its positions in ascending
277
+ * order, ordered by their first position. Positions follow the graph's declaration
278
+ * order, so a part's first position is its first member's site.
331
279
  */
332
- function fallSets(
333
- edges: readonly (readonly number[])[],
334
- members: readonly (readonly number[])[],
335
- into: readonly number[],
336
- ): readonly (readonly number[])[] {
337
- // Each root's walk stamps what it reaches with the root's own number, so the walks
338
- // share one array rather than allocating one per root.
339
- const stamp = edges.map(() => UNVISITED);
340
- const reach = new Map<number, readonly number[]>();
341
- const owners = edges.map(() => 0);
342
- edges.forEach((_targets, root) => {
343
- if (into[root] !== 0) {
344
- return;
280
+ function joined(adjacent: readonly (readonly number[])[]): readonly (readonly number[])[] {
281
+ const leader = adjacent.map((_edges, position) => position);
282
+ const find = (position: number): number => {
283
+ let top = position;
284
+ while ((leader[top] ?? top) !== top) {
285
+ top = leader[top] ?? top;
345
286
  }
346
- const reached = [root];
347
- stamp[root] = root;
348
- // An array's iterator reads its length at every step, so what a step pushes is
349
- // walked in the same loop.
350
- for (const at of reached) {
351
- for (const to of edges[at] ?? []) {
352
- if (stamp[to] !== root) {
353
- stamp[to] = root;
354
- reached.push(to);
355
- }
356
- }
287
+ // Every position on the walk points at the top afterwards, so a later find is short.
288
+ for (let step = position; step !== top;) {
289
+ const next = leader[step] ?? top;
290
+ leader[step] = top;
291
+ step = next;
357
292
  }
358
- reach.set(root, reached);
359
- for (const component of reached) {
360
- owners[component] = (owners[component] ?? 0) + 1;
293
+ return top;
294
+ };
295
+ adjacent.forEach((targets, from) => {
296
+ for (const to of targets) {
297
+ const a = find(from);
298
+ const b = find(to);
299
+ // The smaller position leads, so a part's leader is its first position.
300
+ leader[Math.max(a, b)] = Math.min(a, b);
361
301
  }
362
302
  });
363
- return members.map((own, component) => {
364
- const set = [...own];
365
- for (const other of reach.get(component) ?? []) {
366
- if (other !== component && owners[other] === 1) {
367
- set.push(...(members[other] ?? []));
368
- }
303
+ const parts = new Map<number, number[]>();
304
+ adjacent.forEach((_edges, position) => {
305
+ const top = find(position);
306
+ const part = parts.get(top);
307
+ if (part === undefined) {
308
+ parts.set(top, [position]);
309
+ } else {
310
+ part.push(position);
369
311
  }
370
- return set.sort((a, b) => a - b);
371
312
  });
313
+ return [...parts.values()];
372
314
  }
package/src/config.ts CHANGED
@@ -122,6 +122,34 @@ export interface LifecycleContract {
122
122
  readonly members: readonly string[];
123
123
  }
124
124
 
125
+ /**
126
+ * One analyzer the orchestrator may run, in exactly one of two shapes: an installed
127
+ * analyzer, named by its command alone, or an acquirable one, which also names the
128
+ * artifact acquisition fetches and verifies. This analyzer validates and prints the
129
+ * list and runs nothing from it.
130
+ */
131
+ export type Provider =
132
+ | {
133
+ readonly shape: "installed";
134
+ readonly name: string;
135
+ readonly languages: readonly Language[];
136
+ readonly command: string;
137
+ }
138
+ | {
139
+ readonly shape: "acquirable";
140
+ readonly name: string;
141
+ readonly languages: readonly Language[];
142
+ readonly command: string;
143
+ readonly source: string;
144
+ readonly version: string;
145
+ readonly digest: string;
146
+ };
147
+
148
+ /** The analyzers the orchestrator runs. */
149
+ export interface Providers {
150
+ readonly analyzers: readonly Provider[];
151
+ }
152
+
125
153
  /** The section the TypeScript analyzer owns. */
126
154
  export interface TSSection {
127
155
  readonly testFiles: readonly string[];
@@ -152,6 +180,7 @@ export interface Config {
152
180
  readonly severity: ReadonlyMap<string, Severity>;
153
181
  readonly exemptionsDisabled: readonly string[];
154
182
  readonly reporters: Reporters;
183
+ readonly providers: Providers;
155
184
  readonly ts: TSSection;
156
185
  }
157
186
 
@@ -184,6 +213,12 @@ export function defaultConfig(): Config {
184
213
  maxFindings: 0,
185
214
  failOn: "deny",
186
215
  },
216
+ providers: {
217
+ analyzers: [
218
+ { shape: "installed", name: "deadset-go", languages: ["go"], command: "deadset-go" },
219
+ { shape: "installed", name: "deadset-ts", languages: ["ts"], command: "deadset-ts" },
220
+ ],
221
+ },
187
222
  ts: {
188
223
  testFiles: ["**/*.test.{ts,tsx,mts,cts}"],
189
224
  entryFiles: [],
@@ -246,8 +281,8 @@ export type ErrorKind =
246
281
  */
247
282
  | "malformed"
248
283
  /**
249
- * A key the closed key list does not declare, or a severity key naming a kind
250
- * whose severity the Contract fixes.
284
+ * A key the closed key list does not declare, or a severity key naming no live
285
+ * kind or a kind whose severity the Contract fixes.
251
286
  */
252
287
  | "unimplemented-key"
253
288
  /** A resolved configuration no source supplied a target kind for. */