urtext 0.1.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 (74) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +229 -0
  3. package/dist/analyze/blast-radius.d.ts +28 -0
  4. package/dist/analyze/blast-radius.js +163 -0
  5. package/dist/analyze/canonical.d.ts +27 -0
  6. package/dist/analyze/canonical.js +74 -0
  7. package/dist/analyze/citations.d.ts +256 -0
  8. package/dist/analyze/citations.js +945 -0
  9. package/dist/analyze/effects.d.ts +15 -0
  10. package/dist/analyze/effects.js +255 -0
  11. package/dist/analyze/fact.d.ts +42 -0
  12. package/dist/analyze/fact.js +46 -0
  13. package/dist/analyze/guards.d.ts +70 -0
  14. package/dist/analyze/guards.js +211 -0
  15. package/dist/analyze/index.d.ts +26 -0
  16. package/dist/analyze/index.js +52 -0
  17. package/dist/analyze/program.d.ts +15 -0
  18. package/dist/analyze/program.js +229 -0
  19. package/dist/analyze/surface.d.ts +48 -0
  20. package/dist/analyze/surface.js +396 -0
  21. package/dist/bin.d.ts +2 -0
  22. package/dist/bin.js +12 -0
  23. package/dist/cli.d.ts +110 -0
  24. package/dist/cli.js +502 -0
  25. package/dist/extract/diff.d.ts +35 -0
  26. package/dist/extract/diff.js +116 -0
  27. package/dist/extract/git.d.ts +12 -0
  28. package/dist/extract/git.js +247 -0
  29. package/dist/extract/index.d.ts +4 -0
  30. package/dist/extract/index.js +57 -0
  31. package/dist/extract/intent.d.ts +64 -0
  32. package/dist/extract/intent.js +238 -0
  33. package/dist/extract/scope.d.ts +160 -0
  34. package/dist/extract/scope.js +284 -0
  35. package/dist/extract/symbols.d.ts +24 -0
  36. package/dist/extract/symbols.js +230 -0
  37. package/dist/interpret/client.d.ts +27 -0
  38. package/dist/interpret/client.js +80 -0
  39. package/dist/interpret/index.d.ts +41 -0
  40. package/dist/interpret/index.js +86 -0
  41. package/dist/interpret/prompt.d.ts +23 -0
  42. package/dist/interpret/prompt.js +128 -0
  43. package/dist/interpret/schema.d.ts +74 -0
  44. package/dist/interpret/schema.js +103 -0
  45. package/dist/report/conceal.d.ts +63 -0
  46. package/dist/report/conceal.js +129 -0
  47. package/dist/report/coverage.d.ts +43 -0
  48. package/dist/report/coverage.js +56 -0
  49. package/dist/report/html.d.ts +4 -0
  50. package/dist/report/html.js +634 -0
  51. package/dist/report/markdown.d.ts +2 -0
  52. package/dist/report/markdown.js +168 -0
  53. package/dist/report/model.d.ts +303 -0
  54. package/dist/report/model.js +289 -0
  55. package/dist/report/pdf.d.ts +2 -0
  56. package/dist/report/pdf.js +217 -0
  57. package/dist/report/terminal.d.ts +2 -0
  58. package/dist/report/terminal.js +206 -0
  59. package/dist/report/write.d.ts +105 -0
  60. package/dist/report/write.js +160 -0
  61. package/dist/score/index.d.ts +94 -0
  62. package/dist/score/index.js +572 -0
  63. package/dist/score/reach.d.ts +126 -0
  64. package/dist/score/reach.js +320 -0
  65. package/dist/score/reconcile.d.ts +52 -0
  66. package/dist/score/reconcile.js +208 -0
  67. package/dist/types.d.ts +221 -0
  68. package/dist/types.js +10 -0
  69. package/fonts/DejaVuSans-Bold.ttf +0 -0
  70. package/fonts/DejaVuSans-Oblique.ttf +0 -0
  71. package/fonts/DejaVuSans.ttf +0 -0
  72. package/fonts/DejaVuSansMono.ttf +0 -0
  73. package/fonts/LICENSE +187 -0
  74. package/package.json +44 -0
@@ -0,0 +1,74 @@
1
+ import type { Claim } from "../types.js";
2
+ /**
3
+ * The response shape the model's structured output must satisfy. Constrained
4
+ * rather than free prose because the output is merged with analyzer facts in
5
+ * `reconcile`, and a claim that cannot be attached to a fact or placed in a
6
+ * file is not usable there.
7
+ */
8
+ export declare const CLAIMS_SCHEMA: {
9
+ readonly type: "object";
10
+ readonly properties: {
11
+ readonly claims: {
12
+ readonly type: "array";
13
+ readonly items: {
14
+ readonly type: "object";
15
+ readonly properties: {
16
+ readonly file: {
17
+ readonly type: "string";
18
+ readonly description: "Repo-relative path this claim is about.";
19
+ };
20
+ readonly line: {
21
+ readonly type: "integer";
22
+ readonly description: "1-based line in that file.";
23
+ };
24
+ readonly summary: {
25
+ readonly type: "string";
26
+ readonly description: "One sentence. The finding headline.";
27
+ };
28
+ readonly reasoning: {
29
+ readonly type: "string";
30
+ readonly description: "Why it matters, in one or two sentences.";
31
+ };
32
+ readonly severity: {
33
+ readonly type: "number";
34
+ readonly description: "0..1. How much this should worry a reviewer.";
35
+ };
36
+ readonly correspondsTo: {
37
+ readonly type: "string";
38
+ readonly description: "The id of the analyzer fact this explains, when it explains one. Omit for an observation the analyzers did not make.";
39
+ };
40
+ readonly beyondIntent: {
41
+ readonly type: "boolean";
42
+ readonly description: "True when this change does something the stated intent does not account for. Only meaningful when a `Stated intent` block was given above; omit it otherwise, and omit it rather than guessing.";
43
+ };
44
+ };
45
+ readonly required: readonly ["file", "line", "summary", "reasoning", "severity"];
46
+ readonly additionalProperties: false;
47
+ };
48
+ };
49
+ };
50
+ readonly required: readonly ["claims"];
51
+ readonly additionalProperties: false;
52
+ };
53
+ /**
54
+ * Validates and coerces model output into `Claim[]`. Throws on the whole
55
+ * response — rather than keeping whatever claims happened to parse — when
56
+ * any single claim is malformed: a partially-parsed set is indistinguishable
57
+ * from a complete one to `reconcile`, and the tier system's value rests on
58
+ * knowing exactly what the model said. See
59
+ * `test/interpret/schema.test.ts`, "rejects the whole response when one
60
+ * claim among several is malformed".
61
+ *
62
+ * `severity` and `line` are coerced rather than rejected when out of range.
63
+ * `severity` is zeroed on non-finite input (matching, deliberately, what
64
+ * `reconcile.clampSeverity` independently does to the same value) before
65
+ * being clamped to 0..1 — the two layers must agree here, because a value
66
+ * that slipped past this one by mapping `Infinity` to the top of that range
67
+ * would hand a claim the top of the model tier, the exact outcome
68
+ * `clampSeverity`'s own non-finite guard exists to prevent. `line` has no
69
+ * second guard downstream, so an
70
+ * invalid, non-finite, or non-positive value is repaired here to the first
71
+ * line instead of propagating a value `reconcile` and the renderer both
72
+ * assume is a real line number.
73
+ */
74
+ export declare function parseClaims(text: string): Claim[];
@@ -0,0 +1,103 @@
1
+ /**
2
+ * The response shape the model's structured output must satisfy. Constrained
3
+ * rather than free prose because the output is merged with analyzer facts in
4
+ * `reconcile`, and a claim that cannot be attached to a fact or placed in a
5
+ * file is not usable there.
6
+ */
7
+ export const CLAIMS_SCHEMA = {
8
+ type: "object",
9
+ properties: {
10
+ claims: {
11
+ type: "array",
12
+ items: {
13
+ type: "object",
14
+ properties: {
15
+ file: { type: "string", description: "Repo-relative path this claim is about." },
16
+ line: { type: "integer", description: "1-based line in that file." },
17
+ summary: { type: "string", description: "One sentence. The finding headline." },
18
+ reasoning: { type: "string", description: "Why it matters, in one or two sentences." },
19
+ severity: { type: "number", description: "0..1. How much this should worry a reviewer." },
20
+ correspondsTo: {
21
+ type: "string",
22
+ description: "The id of the analyzer fact this explains, when it explains one. Omit for an observation the analyzers did not make.",
23
+ },
24
+ beyondIntent: {
25
+ type: "boolean",
26
+ description: "True when this change does something the stated intent does not account for. Only meaningful when a `Stated intent` block was given above; omit it otherwise, and omit it rather than guessing.",
27
+ },
28
+ },
29
+ required: ["file", "line", "summary", "reasoning", "severity"],
30
+ additionalProperties: false,
31
+ },
32
+ },
33
+ },
34
+ required: ["claims"],
35
+ additionalProperties: false,
36
+ };
37
+ /**
38
+ * Validates and coerces model output into `Claim[]`. Throws on the whole
39
+ * response — rather than keeping whatever claims happened to parse — when
40
+ * any single claim is malformed: a partially-parsed set is indistinguishable
41
+ * from a complete one to `reconcile`, and the tier system's value rests on
42
+ * knowing exactly what the model said. See
43
+ * `test/interpret/schema.test.ts`, "rejects the whole response when one
44
+ * claim among several is malformed".
45
+ *
46
+ * `severity` and `line` are coerced rather than rejected when out of range.
47
+ * `severity` is zeroed on non-finite input (matching, deliberately, what
48
+ * `reconcile.clampSeverity` independently does to the same value) before
49
+ * being clamped to 0..1 — the two layers must agree here, because a value
50
+ * that slipped past this one by mapping `Infinity` to the top of that range
51
+ * would hand a claim the top of the model tier, the exact outcome
52
+ * `clampSeverity`'s own non-finite guard exists to prevent. `line` has no
53
+ * second guard downstream, so an
54
+ * invalid, non-finite, or non-positive value is repaired here to the first
55
+ * line instead of propagating a value `reconcile` and the renderer both
56
+ * assume is a real line number.
57
+ */
58
+ export function parseClaims(text) {
59
+ const raw = JSON.parse(text);
60
+ if (typeof raw !== "object" || raw === null || !("claims" in raw)) {
61
+ throw new Error("interpretation response has no `claims` array");
62
+ }
63
+ const list = raw.claims;
64
+ if (!Array.isArray(list))
65
+ throw new Error("`claims` is not an array");
66
+ return list.map((item, i) => {
67
+ if (typeof item !== "object" || item === null) {
68
+ throw new Error(`claim ${i} is not an object`);
69
+ }
70
+ const c = item;
71
+ const str = (k) => {
72
+ const v = c[k];
73
+ if (typeof v !== "string" || v.length === 0) {
74
+ throw new Error(`claim ${i}: \`${k}\` must be a non-empty string`);
75
+ }
76
+ return v;
77
+ };
78
+ const severity = typeof c.severity === "number" && Number.isFinite(c.severity) ? c.severity : 0;
79
+ return {
80
+ id: `m${i + 1}`,
81
+ file: str("file"),
82
+ // Math.max after the floor, not a positivity guard before it: a
83
+ // fractional line between zero and one passes such a guard and then
84
+ // floors to zero, a line that does not exist under the 1-based
85
+ // contract.
86
+ line: typeof c.line === "number" && Number.isFinite(c.line)
87
+ ? Math.max(1, Math.floor(c.line))
88
+ : 1,
89
+ summary: str("summary"),
90
+ reasoning: str("reasoning"),
91
+ severity: Math.min(Math.max(severity, 0), 1),
92
+ correspondsTo: typeof c.correspondsTo === "string" ? c.correspondsTo : undefined,
93
+ // Strict `true` only, and deliberately not truthiness: this field puts an
94
+ // accusation in front of a reader, so nothing but the exact affirmative earns
95
+ // it. A string "true", a numeral, or a null is a malformed answer, and the
96
+ // honest repair for a malformed answer is the quiet default — the same
97
+ // direction `line` and `severity` are repaired in, toward the value that
98
+ // cannot mislead. See `test/interpret/schema.test.ts`, "marks a claim only on
99
+ // a literal boolean true".
100
+ beyondIntent: c.beyondIntent === true ? true : undefined,
101
+ };
102
+ });
103
+ }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * The concealing-character defense, shared by both report surfaces. It lived
3
+ * in `html.ts` first, which left the terminal — the *default* surface —
4
+ * printing the same hostile bytes raw: a Trojan-source excerpt could be shown
5
+ * to a terminal reader, under a `[verified]` badge, in an order it does not
6
+ * execute in. One table and one substitution, so the surfaces cannot drift:
7
+ * `buildReportModel` in `./model.ts` applies it to every content field while
8
+ * the model is built (structurally via `segmentConcealed`, or as
9
+ * `labelConcealed` strings for identifier-shaped fields), and `html.ts`
10
+ * builds its `visible` wrapper — the symbol table's scoped renderer-side
11
+ * exception — from `conceals` and `codePointLabel`.
12
+ */
13
+ export declare function conceals(code: number): boolean;
14
+ export declare function codePointLabel(code: number): string;
15
+ /**
16
+ * One piece of a segmented text: either a verbatim run of ordinary
17
+ * characters, or the label standing in for exactly one concealed character.
18
+ * For a `"concealed"` segment, `text` is the bare code-point label
19
+ * (`U+202E`, no brackets): the HTML report wraps it in its own markup while
20
+ * flat surfaces bracket it through `plainText`, and neither has to parse the
21
+ * other's rendering back apart.
22
+ *
23
+ * The raw character is dropped rather than kept beside the label. Copying an
24
+ * excerpt out of a report should not carry an invisible payload with it, and
25
+ * a reader who needs the original bytes has the file and line printed right
26
+ * above.
27
+ */
28
+ export interface ConcealSegment {
29
+ kind: "text" | "concealed";
30
+ text: string;
31
+ }
32
+ /**
33
+ * The segmenting primitive under every surface's concealment defense: each
34
+ * concealing character becomes its own `"concealed"` segment and everything
35
+ * between them stays one verbatim `"text"` run. The distinction is
36
+ * structural rather than in-band because a flattened label cannot be told
37
+ * apart from source code that literally spells it — a walker parsing
38
+ * `[U+202E]` back out of plain text would style an attacker-written literal
39
+ * as a concealed character. See `test/report/conceal.test.ts`, "leaves a
40
+ * source-written label literal as ordinary text".
41
+ *
42
+ * The ranges are written as code points rather than as a character class in
43
+ * a regular expression literal, because a literal would mean putting the
44
+ * very characters this defends against into this file, where the next
45
+ * reader cannot see them either.
46
+ */
47
+ export declare function segmentConcealed(text: string): ConcealSegment[];
48
+ /**
49
+ * Flattens segments for a surface that cannot mark concealment structurally
50
+ * (terminal, Markdown, PDF): text runs verbatim, each concealed label in
51
+ * brackets — `[U+202E]`. By construction this reproduces exactly what
52
+ * `labelConcealed` says about the same input; see
53
+ * `test/report/conceal.test.ts`, "reproduces exactly the string
54
+ * labelConcealed produces, for any input".
55
+ */
56
+ export declare function plainText(segments: ConcealSegment[]): string;
57
+ /**
58
+ * Plain text with every concealing character replaced by a bracketed label
59
+ * of its own code point — `[U+202E]` — and everything else kept verbatim.
60
+ * Built on the segmenting primitive above so the flattened and structural
61
+ * forms cannot disagree about what was concealed.
62
+ */
63
+ export declare function labelConcealed(text: string): string;
@@ -0,0 +1,129 @@
1
+ /**
2
+ * The concealing-character defense, shared by both report surfaces. It lived
3
+ * in `html.ts` first, which left the terminal — the *default* surface —
4
+ * printing the same hostile bytes raw: a Trojan-source excerpt could be shown
5
+ * to a terminal reader, under a `[verified]` badge, in an order it does not
6
+ * execute in. One table and one substitution, so the surfaces cannot drift:
7
+ * `buildReportModel` in `./model.ts` applies it to every content field while
8
+ * the model is built (structurally via `segmentConcealed`, or as
9
+ * `labelConcealed` strings for identifier-shaped fields), and `html.ts`
10
+ * builds its `visible` wrapper — the symbol table's scoped renderer-side
11
+ * exception — from `conceals` and `codePointLabel`.
12
+ */
13
+ /**
14
+ * Characters that render as something other than themselves, or as nothing
15
+ * at all: the bidirectional overrides and isolates a Trojan Source attack
16
+ * uses to make a line of code display in an order it does not execute in,
17
+ * the zero-width and joining characters that can hide inside an identifier,
18
+ * the characters of both control blocks, and the Tag block — deprecated for
19
+ * its original purpose and now the standard way to smuggle an entire ASCII
20
+ * string through a surface that renders nothing for it. Tab and newline are
21
+ * excluded: they are layout in a code excerpt, not concealment.
22
+ *
23
+ * This matters more here than in most renderers. The whole promise of a
24
+ * `verified` finding is "here is the line, look at it yourself", and an
25
+ * excerpt that displays differently from the bytes it quotes breaks exactly
26
+ * that promise — the reader checks the evidence and is shown something
27
+ * else. See `test/report/html.test.ts`, "shows a bidi override in an
28
+ * excerpt rather than obeying it".
29
+ *
30
+ * What this table deliberately does not cover, so that the next reader knows
31
+ * where it stops rather than assuming it stops nowhere:
32
+ *
33
+ * - **Variation selectors** — the `U+FE00`–`U+FE0F` block and the Variation
34
+ * Selectors Supplement above the Tag block. They can carry a payload the
35
+ * same way tag characters can, but `U+FE0F` is load-bearing for ordinary
36
+ * emoji presentation and the supplement encodes legitimate ideographic
37
+ * variants, so labelling them would corrupt real content in a string
38
+ * literal or a comment. They are also the weakest of these channels: a
39
+ * variation selector cannot reorder the text around it or hide another
40
+ * character — it can only hold data for a decoder that is already
41
+ * co-operating. The trade lands the other way for the Tag block, whose
42
+ * only surviving legitimate use is the subdivision-flag emoji sequences;
43
+ * an excerpt containing one of those will render its base character
44
+ * followed by tag labels, and that is a cost worth paying in source code.
45
+ * - **Confusables** — a Cyrillic small a (U+0430) standing in for Latin `a`,
46
+ * a non-breaking space for a space, curly quotes for straight ones. Those
47
+ * are visible characters that look like *other visible characters*, which
48
+ * is a different problem needing a confusables table and a policy about
49
+ * which scripts an identifier may mix. The rule here is narrower and
50
+ * checkable: a character belongs in this table when it renders as nothing,
51
+ * or changes the order of what surrounds it.
52
+ */
53
+ const CONCEALING_RANGES = [
54
+ [0x00, 0x08],
55
+ [0x0b, 0x0c],
56
+ [0x0e, 0x1f],
57
+ [0x7f, 0x9f],
58
+ [0xad, 0xad],
59
+ [0x61c, 0x61c],
60
+ [0x200b, 0x200f],
61
+ [0x2028, 0x202e],
62
+ [0x2060, 0x2064],
63
+ [0x2066, 0x2069],
64
+ [0xfeff, 0xfeff],
65
+ [0xe0000, 0xe007f],
66
+ ];
67
+ export function conceals(code) {
68
+ return CONCEALING_RANGES.some(([lo, hi]) => code >= lo && code <= hi);
69
+ }
70
+ export function codePointLabel(code) {
71
+ return `U+${code.toString(16).toUpperCase().padStart(4, "0")}`;
72
+ }
73
+ /**
74
+ * The segmenting primitive under every surface's concealment defense: each
75
+ * concealing character becomes its own `"concealed"` segment and everything
76
+ * between them stays one verbatim `"text"` run. The distinction is
77
+ * structural rather than in-band because a flattened label cannot be told
78
+ * apart from source code that literally spells it — a walker parsing
79
+ * `[U+202E]` back out of plain text would style an attacker-written literal
80
+ * as a concealed character. See `test/report/conceal.test.ts`, "leaves a
81
+ * source-written label literal as ordinary text".
82
+ *
83
+ * The ranges are written as code points rather than as a character class in
84
+ * a regular expression literal, because a literal would mean putting the
85
+ * very characters this defends against into this file, where the next
86
+ * reader cannot see them either.
87
+ */
88
+ export function segmentConcealed(text) {
89
+ const segments = [];
90
+ let run = "";
91
+ for (const ch of text) {
92
+ const code = ch.codePointAt(0) ?? 0;
93
+ if (conceals(code)) {
94
+ if (run) {
95
+ segments.push({ kind: "text", text: run });
96
+ run = "";
97
+ }
98
+ segments.push({ kind: "concealed", text: codePointLabel(code) });
99
+ }
100
+ else {
101
+ run += ch;
102
+ }
103
+ }
104
+ if (run)
105
+ segments.push({ kind: "text", text: run });
106
+ return segments;
107
+ }
108
+ /**
109
+ * Flattens segments for a surface that cannot mark concealment structurally
110
+ * (terminal, Markdown, PDF): text runs verbatim, each concealed label in
111
+ * brackets — `[U+202E]`. By construction this reproduces exactly what
112
+ * `labelConcealed` says about the same input; see
113
+ * `test/report/conceal.test.ts`, "reproduces exactly the string
114
+ * labelConcealed produces, for any input".
115
+ */
116
+ export function plainText(segments) {
117
+ return segments
118
+ .map((s) => (s.kind === "concealed" ? `[${s.text}]` : s.text))
119
+ .join("");
120
+ }
121
+ /**
122
+ * Plain text with every concealing character replaced by a bracketed label
123
+ * of its own code point — `[U+202E]` — and everything else kept verbatim.
124
+ * Built on the segmenting primitive above so the flattened and structural
125
+ * forms cannot disagree about what was concealed.
126
+ */
127
+ export function labelConcealed(text) {
128
+ return plainText(segmentConcealed(text));
129
+ }
@@ -0,0 +1,43 @@
1
+ import type { Changeset } from "../types.js";
2
+ /**
3
+ * What the analyzers did not look at, stated the same way on all three
4
+ * surfaces: the terminal's note, the HTML report's header, and `--json`'s
5
+ * `coverage` field, which `review` in `../cli.ts` fills from here. `--json`
6
+ * carried nothing at all until it did, so the one consumer that cannot read
7
+ * prose was the one left blind to the gap.
8
+ *
9
+ * Lives in its own module because both renderers need it and neither may import
10
+ * the other, and because a sentence making a claim about analyzer coverage has
11
+ * to be checked in one place — the copy that lived in both renderers was wrong
12
+ * in both.
13
+ */
14
+ /** Deleted TypeScript files in this range, in the order the diff listed them. */
15
+ export declare function deletedTypeScriptFiles(changeset: Changeset): string[];
16
+ /**
17
+ * What a reader is owed about a deleted TypeScript file, and no more than is
18
+ * true. `guardsAnalyzer` and `surfaceAnalyzer` skip a file whose status is
19
+ * "deleted", `blastRadiusAnalyzer` skips it too, and `mapSymbols` returns no
20
+ * symbols for one — so its exports, its callers, and its guards go unexamined,
21
+ * and without a word about it a reader cannot tell that gap from "nothing in it
22
+ * was worth reporting".
23
+ *
24
+ * `effectsAnalyzer` is the exception and the reason this sentence was rewritten:
25
+ * it reads the before side of a deletion on purpose (see the `file.status !==
26
+ * "deleted"` condition on its unreadable-after-side guard) and reports every
27
+ * effect kind that vanished with the file. The earlier wording — "every
28
+ * analyzer skips a deleted file, so nothing below describes what it contained"
29
+ * — could therefore print directly above a `verified` finding about that exact
30
+ * file, telling a reviewer to disregard a real one. See
31
+ * `test/report/coverage.test.ts`.
32
+ *
33
+ * Naming a path reports coverage; it asserts nothing about the deleted code.
34
+ */
35
+ export declare function deletedFilesNote(paths: string[]): string;
36
+ /**
37
+ * The one sentence disclosing reconcile's standalone-reach filter, shared by
38
+ * the terminal and HTML renderers so the two surfaces cannot drift apart —
39
+ * the same single-source rule `deletedFilesNote` above exists for. The copy
40
+ * is filter-shaped: it says what was removed from this report and why the
41
+ * filter fired, not anything about the code under review.
42
+ */
43
+ export declare function suppressionNote(count: number): string;
@@ -0,0 +1,56 @@
1
+ import { isTypeScriptFile } from "../extract/symbols.js";
2
+ /**
3
+ * What the analyzers did not look at, stated the same way on all three
4
+ * surfaces: the terminal's note, the HTML report's header, and `--json`'s
5
+ * `coverage` field, which `review` in `../cli.ts` fills from here. `--json`
6
+ * carried nothing at all until it did, so the one consumer that cannot read
7
+ * prose was the one left blind to the gap.
8
+ *
9
+ * Lives in its own module because both renderers need it and neither may import
10
+ * the other, and because a sentence making a claim about analyzer coverage has
11
+ * to be checked in one place — the copy that lived in both renderers was wrong
12
+ * in both.
13
+ */
14
+ /** Deleted TypeScript files in this range, in the order the diff listed them. */
15
+ export function deletedTypeScriptFiles(changeset) {
16
+ return changeset.files
17
+ .filter((f) => f.status === "deleted" && isTypeScriptFile(f.path))
18
+ .map((f) => f.path);
19
+ }
20
+ /**
21
+ * What a reader is owed about a deleted TypeScript file, and no more than is
22
+ * true. `guardsAnalyzer` and `surfaceAnalyzer` skip a file whose status is
23
+ * "deleted", `blastRadiusAnalyzer` skips it too, and `mapSymbols` returns no
24
+ * symbols for one — so its exports, its callers, and its guards go unexamined,
25
+ * and without a word about it a reader cannot tell that gap from "nothing in it
26
+ * was worth reporting".
27
+ *
28
+ * `effectsAnalyzer` is the exception and the reason this sentence was rewritten:
29
+ * it reads the before side of a deletion on purpose (see the `file.status !==
30
+ * "deleted"` condition on its unreadable-after-side guard) and reports every
31
+ * effect kind that vanished with the file. The earlier wording — "every
32
+ * analyzer skips a deleted file, so nothing below describes what it contained"
33
+ * — could therefore print directly above a `verified` finding about that exact
34
+ * file, telling a reviewer to disregard a real one. See
35
+ * `test/report/coverage.test.ts`.
36
+ *
37
+ * Naming a path reports coverage; it asserts nothing about the deleted code.
38
+ */
39
+ export function deletedFilesNote(paths) {
40
+ const count = paths.length === 1
41
+ ? "1 deleted TypeScript file"
42
+ : `${paths.length} deleted TypeScript files`;
43
+ const subject = paths.length === 1 ? "it" : "them";
44
+ const possessive = paths.length === 1 ? "its" : "their";
45
+ return `${count}: ${paths.join(", ")} — only effects that vanished with ${subject} are reported; ${possessive} exports, callers, and guards are not analyzed.`;
46
+ }
47
+ /**
48
+ * The one sentence disclosing reconcile's standalone-reach filter, shared by
49
+ * the terminal and HTML renderers so the two surfaces cannot drift apart —
50
+ * the same single-source rule `deletedFilesNote` above exists for. The copy
51
+ * is filter-shaped: it says what was removed from this report and why the
52
+ * filter fired, not anything about the code under review.
53
+ */
54
+ export function suppressionNote(count) {
55
+ return `Filtered: ${count} finding${count === 1 ? "" : "s"} suppressed (low-signal: single unclaimed reference).`;
56
+ }
@@ -0,0 +1,4 @@
1
+ import type { Changeset, Finding } from "../types.js";
2
+ import type { ReportMeta } from "./model.js";
3
+ export type { ReportMeta } from "./model.js";
4
+ export declare function renderHtml(changeset: Changeset, findings: Finding[], meta: ReportMeta): string;