@cplieger/deadset-ts 3.13.0 → 4.0.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 (88) hide show
  1. package/README.md +3 -3
  2. package/dist/bin/node-host.js +10 -7
  3. package/dist/src/alias-chain.js +59 -7
  4. package/dist/src/analysis.js +119 -23
  5. package/dist/src/calls.js +23 -22
  6. package/dist/src/class-members.js +10 -11
  7. package/dist/src/configured-declarations.js +22 -9
  8. package/dist/src/dependencies.js +34 -17
  9. package/dist/src/deprecation.js +35 -24
  10. package/dist/src/destructuring.js +37 -29
  11. package/dist/src/discover.js +105 -28
  12. package/dist/src/entry-points.js +7 -0
  13. package/dist/src/enum-group.js +46 -40
  14. package/dist/src/exempt.js +42 -1
  15. package/dist/src/findings/emitters.js +15 -1
  16. package/dist/src/findings/interfaces.js +7 -1
  17. package/dist/src/framework-lifecycle.js +8 -6
  18. package/dist/src/implementations.js +45 -23
  19. package/dist/src/interface-satisfaction.js +30 -27
  20. package/dist/src/intra-function-parts.js +37 -20
  21. package/dist/src/inventory.js +85 -4
  22. package/dist/src/json-chunks.js +76 -0
  23. package/dist/src/partial.js +23 -0
  24. package/dist/src/query.js +207 -0
  25. package/dist/src/references.js +92 -20
  26. package/dist/src/reflective-lookup.js +18 -9
  27. package/dist/src/report.js +3 -1
  28. package/dist/src/reporters/annotations.js +9 -5
  29. package/dist/src/reporters/json.js +3 -2
  30. package/dist/src/reporters/sarif.js +2 -1
  31. package/dist/src/reporters/template.js +48 -7
  32. package/dist/src/reporters/text.js +8 -6
  33. package/dist/src/roots.js +27 -11
  34. package/dist/src/serialization-contract.js +12 -10
  35. package/dist/src/session.js +79 -39
  36. package/dist/src/suppress-file.js +2 -1
  37. package/dist/src/sweep.js +4 -1
  38. package/dist/src/value-reach.js +14 -19
  39. package/dist/src/verbs/analyze.js +20 -3
  40. package/dist/src/verbs/explain.js +22 -9
  41. package/dist/src/verbs/print-projects.js +40 -7
  42. package/dist/src/verbs/print-retained.js +6 -2
  43. package/dist/src/verbs/print-roots.js +5 -1
  44. package/jsr.json +1 -1
  45. package/package.json +3 -3
  46. package/src/alias-chain.ts +75 -11
  47. package/src/analysis.ts +223 -61
  48. package/src/calls.ts +10 -9
  49. package/src/class-members.ts +10 -11
  50. package/src/configured-declarations.ts +29 -13
  51. package/src/dependencies.ts +42 -20
  52. package/src/deprecation.ts +53 -27
  53. package/src/destructuring.ts +50 -39
  54. package/src/discover.ts +177 -33
  55. package/src/entry-points.ts +7 -0
  56. package/src/enum-group.ts +56 -42
  57. package/src/exempt.ts +50 -2
  58. package/src/findings/emitters.ts +18 -1
  59. package/src/findings/interfaces.ts +7 -1
  60. package/src/framework-lifecycle.ts +10 -6
  61. package/src/host.ts +5 -3
  62. package/src/implementations.ts +59 -23
  63. package/src/interface-satisfaction.ts +30 -27
  64. package/src/intra-function-parts.ts +43 -25
  65. package/src/inventory.ts +97 -4
  66. package/src/json-chunks.ts +87 -0
  67. package/src/partial.ts +40 -0
  68. package/src/query.ts +522 -0
  69. package/src/references.ts +119 -38
  70. package/src/reflective-lookup.ts +21 -11
  71. package/src/report.ts +14 -2
  72. package/src/reporters/annotations.ts +23 -21
  73. package/src/reporters/json.ts +3 -2
  74. package/src/reporters/reporter.ts +2 -2
  75. package/src/reporters/sarif.ts +3 -2
  76. package/src/reporters/template.ts +50 -8
  77. package/src/reporters/text.ts +12 -12
  78. package/src/roots.ts +34 -12
  79. package/src/serialization-contract.ts +12 -10
  80. package/src/session.ts +146 -58
  81. package/src/suppress-file.ts +6 -2
  82. package/src/sweep.ts +9 -4
  83. package/src/value-reach.ts +20 -23
  84. package/src/verbs/analyze.ts +25 -3
  85. package/src/verbs/explain.ts +20 -9
  86. package/src/verbs/print-projects.ts +53 -7
  87. package/src/verbs/print-retained.ts +6 -2
  88. package/src/verbs/print-roots.ts +5 -1
@@ -17,6 +17,7 @@ import {
17
17
  } from "@typescript/native/unstable/sync";
18
18
  import { classNames, typesAt } from "./class-members.ts";
19
19
  import type { Inventory } from "./inventory.ts";
20
+ import { must } from "./query.ts";
20
21
  import type { ProjectView } from "./session.ts";
21
22
 
22
23
  /** The object types whose members the walk reads whatever declares them. */
@@ -46,11 +47,11 @@ const CONVERSION_NAMES: ReadonlySet<string> = new Set(["toJSON", "toString"]);
46
47
 
47
48
  /** The global array types, whose element type is what an array value carries. */
48
49
  function arrayTargets<Brand>(project: ProjectView<Brand>): ReadonlySet<number> {
49
- const checker = project.checker;
50
+ const queries = project.queries;
50
51
  return new Set(
51
52
  ["Array", "ReadonlyArray"].flatMap((name) => {
52
- const symbol = checker.resolveName(name, SymbolFlags.Type, undefined, false);
53
- return symbol === undefined ? [] : [checker.getDeclaredTypeOfSymbol(symbol).id];
53
+ const symbol = must(queries.resolveName(name, SymbolFlags.Type, undefined, false));
54
+ return symbol === undefined ? [] : [must(queries.declaredTypeOf(symbol)).id];
54
55
  }),
55
56
  );
56
57
  }
@@ -60,7 +61,7 @@ function arrayTargets<Brand>(project: ProjectView<Brand>): ReadonlySet<number> {
60
61
  * project's inventory.
61
62
  */
62
63
  export function valueReach<Brand>(project: ProjectView<Brand>, held: Inventory): ValueReach {
63
- const checker = project.checker;
64
+ const queries = project.queries;
64
65
  const names = [...classNames(project, held)];
65
66
  const declared = typesAt(
66
67
  project,
@@ -74,14 +75,12 @@ export function valueReach<Brand>(project: ProjectView<Brand>, held: Inventory):
74
75
  }
75
76
  });
76
77
  const arrays = arrayTargets(project);
77
- const own = new Set(project.ownSourceFiles().map((file) => file.fileName));
78
+ const own = project.ownPaths();
78
79
  const children = new Map<number, readonly Type[]>();
79
80
 
80
81
  /** The generic target of a type reference, and the type itself otherwise. */
81
82
  const targetOf = (type: ObjectType): Type =>
82
- (type.objectFlags & ObjectFlags.Reference) !== 0 && type.isTypeReference()
83
- ? type.getTarget()
84
- : type;
83
+ (type.objectFlags & ObjectFlags.Reference) !== 0 ? must(queries.targetOf(type)) : type;
85
84
 
86
85
  const classOf = (type: Type): string | undefined => {
87
86
  if (!type.isObjectType()) {
@@ -95,7 +94,7 @@ export function valueReach<Brand>(project: ProjectView<Brand>, held: Inventory):
95
94
  if ((type.objectFlags & WRITTEN) !== 0) {
96
95
  return true;
97
96
  }
98
- const symbol: TSSymbol | undefined = type.getSymbol();
97
+ const symbol: TSSymbol | undefined = must(queries.symbolOfType(type));
99
98
  if (symbol === undefined) {
100
99
  return false;
101
100
  }
@@ -110,13 +109,13 @@ export function valueReach<Brand>(project: ProjectView<Brand>, held: Inventory):
110
109
 
111
110
  /** The types a value of one object type holds in its data members. */
112
111
  const memberTypes = (type: Type): readonly Type[] => {
113
- const properties = checker
114
- .getPropertiesOfType(type)
115
- .filter((property) => (property.flags & SymbolFlags.Property) !== 0);
116
- const types = properties.length === 0 ? [] : checker.getTypeOfSymbol(properties);
112
+ const properties = must(queries.propertiesOf(type)).filter(
113
+ (property) => (property.flags & SymbolFlags.Property) !== 0,
114
+ );
115
+ const types = queries.typesOfSymbols(properties).map(must);
117
116
  return [
118
117
  ...types.filter((held): held is Type => held !== undefined),
119
- ...checker.getIndexInfosOfType(type).map((info) => info.valueType),
118
+ ...must(queries.indexInfosOf(type)).map((info) => info.valueType),
120
119
  ];
121
120
  };
122
121
 
@@ -135,13 +134,11 @@ export function valueReach<Brand>(project: ProjectView<Brand>, held: Inventory):
135
134
 
136
135
  /** The data members and conversion methods of one written object type. */
137
136
  const carried = (type: Type): readonly TSSymbol[] =>
138
- checker
139
- .getPropertiesOfType(type)
140
- .filter(
141
- (property) =>
142
- (property.flags & SymbolFlags.Property) !== 0 ||
143
- ((property.flags & SymbolFlags.Method) !== 0 && CONVERSION_NAMES.has(property.name)),
144
- );
137
+ must(queries.propertiesOf(type)).filter(
138
+ (property) =>
139
+ (property.flags & SymbolFlags.Property) !== 0 ||
140
+ ((property.flags & SymbolFlags.Method) !== 0 && CONVERSION_NAMES.has(property.name)),
141
+ );
145
142
 
146
143
  /** The types a value of one type holds, read once per type. */
147
144
  const childrenOf = (type: Type): readonly Type[] => {
@@ -151,7 +148,7 @@ export function valueReach<Brand>(project: ProjectView<Brand>, held: Inventory):
151
148
  }
152
149
  let found: readonly Type[] = [];
153
150
  if (type.isUnionType() || type.isIntersectionType()) {
154
- found = type.getTypes();
151
+ found = must(queries.constituents(type));
155
152
  } else if (type.isObjectType()) {
156
153
  const target = targetOf(type);
157
154
  const isList =
@@ -161,7 +158,7 @@ export function valueReach<Brand>(project: ProjectView<Brand>, held: Inventory):
161
158
  if (classOf(type) !== undefined || (!isList && isWritten(type))) {
162
159
  found = memberTypes(type);
163
160
  } else if (isList && type.isTypeReference()) {
164
- found = checker.getTypeArguments(type);
161
+ found = must(queries.typeArguments(type));
165
162
  }
166
163
  }
167
164
  children.set(type.id, found);
@@ -5,12 +5,15 @@ import { moduleIdentity } from "../consumers.ts";
5
5
  import { analyzerProvenance, verdictOf } from "../findings-pass.ts";
6
6
  import { joinPath, normalizePath, relativePath, resolvePath } from "../paths.ts";
7
7
  import type { Host } from "../host.ts";
8
+ import type { NotBuilt } from "../discover.ts";
9
+ import { partialNotes } from "../partial.ts";
8
10
  import {
9
11
  buildReport,
10
12
  capFindings,
11
13
  sortFindings,
12
14
  type Report,
13
15
  type ReportConfiguration,
16
+ type ReportNotBuilt,
14
17
  } from "../report.ts";
15
18
  import { json } from "../reporters/json.ts";
16
19
  import { RENDERINGS } from "../reporters/reporters.ts";
@@ -81,6 +84,19 @@ function runRelative(invoked: string, path: string): string {
81
84
  return below;
82
85
  }
83
86
 
87
+ /** Each project the run dropped as a report names it, by its configuration file below the target. */
88
+ function notBuiltOf(dropped: readonly NotBuilt[], targetRoot: string): readonly ReportNotBuilt[] {
89
+ return dropped.map((one) => {
90
+ const project = relativePath(targetRoot, one.configFile);
91
+ if (project === undefined) {
92
+ throw new ReportPathError(
93
+ `the project ${one.id} would be opened from ${one.configFile}, which is not below the target ${targetRoot}, so no report can name it`,
94
+ );
95
+ }
96
+ return { id: one.id, project, error: one.error };
97
+ });
98
+ }
99
+
84
100
  /** Each project of the run as a report names it, by its configuration file below the target. */
85
101
  function configurationsOf(
86
102
  projects: readonly RunProject[],
@@ -203,6 +219,7 @@ export const analyzeVerb: Verb = ({ err, host, inputs, scope, openClient, option
203
219
  let report: Report;
204
220
  let recorded: readonly Recorded[];
205
221
  let verdict: number;
222
+ let partial: readonly string[];
206
223
  try {
207
224
  const root = runRelative(invoked, targetRoot);
208
225
  const mode = { production: true };
@@ -225,6 +242,7 @@ export const analyzeVerb: Verb = ({ err, host, inputs, scope, openClient, option
225
242
  recorded = write.rows;
226
243
  }
227
244
  const { result, run } = analysis;
245
+ partial = partialNotes({ notBuilt: run.notBuilt, unanswered: run.unanswered });
228
246
  const built = buildReport({
229
247
  contractVersion: CONTRACT_VERSION,
230
248
  version,
@@ -232,6 +250,7 @@ export const analyzeVerb: Verb = ({ err, host, inputs, scope, openClient, option
232
250
  declaredGaps: DECLARED_GAPS,
233
251
  target: { kind, root, identity: moduleIdentity(host, scoped.target) },
234
252
  configurations: configurationsOf(run.projects, targetRoot),
253
+ notBuilt: notBuiltOf(run.notBuilt, targetRoot),
235
254
  loaded: run.consumers.map((consumer) => ({
236
255
  id: consumer.id,
237
256
  path: runRelative(invoked, consumer.path),
@@ -251,7 +270,7 @@ export const analyzeVerb: Verb = ({ err, host, inputs, scope, openClient, option
251
270
  }
252
271
 
253
272
  try {
254
- host.writeFile(documents.report, json(report));
273
+ host.writeDocument(documents.report, json(report));
255
274
  const options = {
256
275
  failOn: config.reporters.failOn,
257
276
  readSource: (path: string) => host.readFile(joinPath(targetRoot, path)),
@@ -260,17 +279,20 @@ export const analyzeVerb: Verb = ({ err, host, inputs, scope, openClient, option
260
279
  for (const format of documents.formats) {
261
280
  const rendering = RENDERINGS.get(format);
262
281
  if (rendering !== undefined) {
263
- host.writeFile(documents.report + rendering.suffix, rendering.render(report, options));
282
+ host.writeDocument(documents.report + rendering.suffix, rendering.render(report, options));
264
283
  }
265
284
  }
266
285
  if (documents.baseline !== undefined) {
267
- host.writeFile(documents.baseline, writeBaseline(recorded, analyzerProvenance(version)));
286
+ host.writeDocument(documents.baseline, writeBaseline(recorded, analyzerProvenance(version)));
268
287
  }
269
288
  } catch (error: unknown) {
270
289
  err.write(`deadset-ts: ${error instanceof Error ? error.message : String(error)}\n`);
271
290
  return EXIT_FAILURE;
272
291
  }
273
292
 
293
+ for (const note of partial) {
294
+ err.write(`deadset-ts: ${note}\n`);
295
+ }
274
296
  if (report.totals.pending > 0) {
275
297
  err.write(
276
298
  `deadset-ts: ${counted(report.totals.pending, "pending finding")}: the report names a declared cross-language edge, and no merge has resolved it\n`,
@@ -6,6 +6,7 @@ import type { CompletedFinding } from "../finding.ts";
6
6
  import { OUTSIDE, swept as judged } from "../graph.ts";
7
7
  import type { InventorySymbol } from "../inventory.ts";
8
8
  import { referenceKey, type Configured } from "../matrix.ts";
9
+ import { partialNotes } from "../partial.ts";
9
10
  import { positionKey } from "../position.ts";
10
11
  import type { Reference } from "../references.ts";
11
12
  import { resolve } from "../resolve.ts";
@@ -344,6 +345,9 @@ function explanation(analysis: RunAnalysis, subject: InventorySymbol, cascade: C
344
345
  for (const one of retained) {
345
346
  text += ` class: ${one.class}\t${positionKey(one.site)}\t${one.detail}\n`;
346
347
  }
348
+ } else if (analysis.swept.heldByUnanswered.includes(subject.id)) {
349
+ text +=
350
+ "answer: held\n held by: a question the checker did not answer, which could have kept it live, so no finding names it\n";
347
351
  } else {
348
352
  text += liveness(analysis, subject, cascade);
349
353
  }
@@ -351,12 +355,12 @@ function explanation(analysis: RunAnalysis, subject: InventorySymbol, cascade: C
351
355
  }
352
356
 
353
357
  /**
354
- * Explains one symbol from the analysis the report is built from, so an explanation cannot
355
- * disagree with a finding: why a reported symbol is reported, which exemption classes held a
356
- * retained one back, and for any other declaration the relations that hold it live and the
357
- * shortest path of references from a root or a loaded consumer, or the references it has where
358
- * no path reaches it. A request naming no one declaration exits with the usage code and names
359
- * what partially matches it.
358
+ * Explains one symbol from the analysis the report is built from: why a reported symbol is
359
+ * reported, which exemption classes held a retained one back, that an unanswered question held
360
+ * a held one, and for any other declaration the relations that hold it live and the shortest
361
+ * path from a root or a loaded consumer, or the references it has where no path reaches it. A
362
+ * request naming no one declaration exits with the usage code and names what partially
363
+ * matches it. Where the analysis was partial, the error stream says so.
360
364
  */
361
365
  export const explainVerb: Verb = ({ out, err, host, inputs, scope, openClient, option }) => {
362
366
  const named = namedSymbol(option);
@@ -365,6 +369,10 @@ export const explainVerb: Verb = ({ out, err, host, inputs, scope, openClient, o
365
369
  production: true,
366
370
  });
367
371
  const { subject, candidates } = subjectOf(analysis.swept.matrix.union.symbols, named);
372
+ const notes = partialNotes({
373
+ notBuilt: analysis.run.notBuilt,
374
+ unanswered: analysis.run.unanswered,
375
+ });
368
376
  if (subject === undefined) {
369
377
  err.write(`deadset-ts: ${JSON.stringify(named)} names no one symbol of the target\n`);
370
378
  for (const candidate of candidates) {
@@ -373,8 +381,11 @@ export const explainVerb: Verb = ({ out, err, host, inputs, scope, openClient, o
373
381
  if (candidates.length === 0) {
374
382
  err.write(" no symbol of the target partially matches it either\n");
375
383
  }
376
- return EXIT_USAGE;
384
+ } else {
385
+ out.write(explanation(analysis, subject, config.reporters.cascade));
386
+ }
387
+ for (const note of notes) {
388
+ err.write(`deadset-ts: ${note}\n`);
377
389
  }
378
- out.write(explanation(analysis, subject, config.reporters.cascade));
379
- return EXIT_CLEAN;
390
+ return subject === undefined ? EXIT_USAGE : EXIT_CLEAN;
380
391
  };
@@ -1,4 +1,14 @@
1
- import { diagnosticErrors, discoverProjects, renderDiagnostic } from "../discover.ts";
1
+ import {
2
+ discoverProjects,
3
+ DiscoveryError,
4
+ isGuess,
5
+ projectErrors,
6
+ renderDiagnostic,
7
+ targetRelative,
8
+ type NotBuilt,
9
+ } from "../discover.ts";
10
+ import { partialNotes } from "../partial.ts";
11
+ import { relativePath, resolvePath } from "../paths.ts";
2
12
  import { resolveMatrix } from "../resolve.ts";
3
13
  import { diagnosticsOf, runSession } from "../session.ts";
4
14
  import { EXIT_CLEAN, EXIT_FAILURE, type Verb } from "./verb.ts";
@@ -7,7 +17,8 @@ import { EXIT_CLEAN, EXIT_FAILURE, type Verb } from "./verb.ts";
7
17
  * Writes the name of every project one run analyzes, one per line, having read each
8
18
  * project's diagnostics first: the identifier the build matrix declares for it, or,
9
19
  * where discovery derived the project, its configuration file's path below the
10
- * target root.
20
+ * target root. A derived project that does not load is not one the run analyzes, so it
21
+ * is named on the error stream instead, as the analysis drops it.
11
22
  */
12
23
  export const printProjectsVerb: Verb = ({ out, err, host, inputs, scope, openClient }) => {
13
24
  const matrix = resolveMatrix(inputs());
@@ -15,12 +26,36 @@ export const printProjectsVerb: Verb = ({ out, err, host, inputs, scope, openCli
15
26
  const engine = openClient(false);
16
27
  const discovered = discoverProjects(engine, host, scoped, matrix);
17
28
  const ids = new Map(discovered.projects.map((project) => [project.configFile, project.id]));
29
+ const root = resolvePath(host.workingDirectory(), scoped.target.path);
30
+ const notBuilt: NotBuilt[] = [...discovered.notBuilt];
18
31
  const failures: string[] = [];
19
- const { projects } = runSession(engine, discovered.configFiles, (project) => {
20
- const errors = diagnosticErrors(diagnosticsOf(project));
21
- failures.push(...errors.map(renderDiagnostic));
22
- return ids.get(project.configFile) ?? project.configFile;
23
- });
32
+ const session = runSession(
33
+ engine,
34
+ discovered.configFiles,
35
+ (project) => {
36
+ const id = ids.get(project.configFile) ?? project.configFile;
37
+ const errors = projectErrors(diagnosticsOf(project));
38
+ const [unloaded] = errors.load;
39
+ if (unloaded !== undefined && isGuess(discovered, project.configFile)) {
40
+ const error = renderDiagnostic(unloaded, (path) => relativePath(root, path) ?? path);
41
+ notBuilt.push({ id, configFile: project.configFile, error: targetRelative(error, root) });
42
+ return undefined;
43
+ }
44
+ failures.push(...[...errors.load, ...errors.check].map((one) => renderDiagnostic(one)));
45
+ return id;
46
+ },
47
+ discovered.derived ? "record" : "refuse",
48
+ );
49
+ for (const configFile of session.unopened) {
50
+ if (!isGuess(discovered, configFile)) {
51
+ throw new DiscoveryError(`${configFile} opened no project`, []);
52
+ }
53
+ notBuilt.push({
54
+ id: ids.get(configFile) ?? configFile,
55
+ configFile,
56
+ error: "the compiler opened no project for the configuration",
57
+ });
58
+ }
24
59
  if (failures.length > 0) {
25
60
  for (const line of failures) {
26
61
  err.write(`${line}\n`);
@@ -28,8 +63,19 @@ export const printProjectsVerb: Verb = ({ out, err, host, inputs, scope, openCli
28
63
  err.write(`deadset-ts: ${String(failures.length)} error(s); no answer was produced\n`);
29
64
  return EXIT_FAILURE;
30
65
  }
66
+ const projects = session.projects.filter((id): id is string => id !== undefined);
67
+ if (projects.length === 0 && notBuilt.length > 0) {
68
+ for (const note of partialNotes({ notBuilt, unanswered: [] })) {
69
+ err.write(`deadset-ts: ${note}\n`);
70
+ }
71
+ err.write("deadset-ts: no configuration discovery derived could be built\n");
72
+ return EXIT_FAILURE;
73
+ }
31
74
  for (const id of projects) {
32
75
  out.write(`${id}\n`);
33
76
  }
77
+ for (const note of partialNotes({ notBuilt, unanswered: [] })) {
78
+ err.write(`deadset-ts: ${note}\n`);
79
+ }
34
80
  return EXIT_CLEAN;
35
81
  };
@@ -1,5 +1,6 @@
1
1
  import { runSweep } from "../analysis.ts";
2
2
  import { retainedLines } from "../exempt.ts";
3
+ import { partialNotes } from "../partial.ts";
3
4
  import { resolve } from "../resolve.ts";
4
5
  import { EXIT_CLEAN, type Verb } from "./verb.ts";
5
6
 
@@ -8,9 +9,9 @@ import { EXIT_CLEAN, type Verb } from "./verb.ts";
8
9
  * built from, one per line, and nothing where no exemption held one back. One fact
9
10
  * several configurations found is one record, so the line names no configuration. The
10
11
  * line shape is this command's own rather than a Contract format, so that sort and cut
11
- * read it.
12
+ * read it. Where the analysis was partial, the error stream says so.
12
13
  */
13
- export const printRetainedVerb: Verb = ({ out, host, inputs, scope, openClient }) => {
14
+ export const printRetainedVerb: Verb = ({ out, err, host, inputs, scope, openClient }) => {
14
15
  const { config } = resolve(inputs());
15
16
  const scoped = scope();
16
17
  const swept = runSweep(openClient(false), host, scoped, config, {
@@ -20,5 +21,8 @@ export const printRetainedVerb: Verb = ({ out, host, inputs, scope, openClient }
20
21
  for (const line of retainedLines(swept.matrix.union.symbols, swept.retained)) {
21
22
  out.write(`${line}\n`);
22
23
  }
24
+ for (const note of partialNotes({ notBuilt: swept.notBuilt, unanswered: swept.unanswered })) {
25
+ err.write(`deadset-ts: ${note}\n`);
26
+ }
23
27
  return EXIT_CLEAN;
24
28
  };
@@ -1,4 +1,5 @@
1
1
  import { runRoots, type RunRoot } from "../analysis.ts";
2
+ import { partialNotes } from "../partial.ts";
2
3
  import { resolve } from "../resolve.ts";
3
4
  import { EXIT_CLEAN, EXIT_FINDINGS, type Verb } from "./verb.ts";
4
5
 
@@ -20,7 +21,7 @@ function rootLine(root: RunRoot, several: boolean): string {
20
21
  /**
21
22
  * Writes the root set of the run, one root per line, and then names on the error
22
23
  * stream, by its issue kind, every configured root or pattern that named nothing;
23
- * one such string fails the run. The line shape is this command's own rather than a
24
+ * one such string fails the run, and then where the analysis was partial. The line shape is this command's own rather than a
24
25
  * Contract format, so that sort and cut read it.
25
26
  */
26
27
  export const printRootsVerb: Verb = ({ out, err, host, inputs, scope, openClient }) => {
@@ -34,5 +35,8 @@ export const printRootsVerb: Verb = ({ out, err, host, inputs, scope, openClient
34
35
  for (const finding of answer.findings) {
35
36
  err.write(`${finding.code}: roots.patterns names nothing: ${finding.symbol.ref}\n`);
36
37
  }
38
+ for (const note of partialNotes({ notBuilt: answer.notBuilt, unanswered: answer.unanswered })) {
39
+ err.write(`deadset-ts: ${note}\n`);
40
+ }
37
41
  return answer.findings.length > 0 ? EXIT_FINDINGS : EXIT_CLEAN;
38
42
  };