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.
- package/LICENSE +21 -0
- package/README.md +229 -0
- package/dist/analyze/blast-radius.d.ts +28 -0
- package/dist/analyze/blast-radius.js +163 -0
- package/dist/analyze/canonical.d.ts +27 -0
- package/dist/analyze/canonical.js +74 -0
- package/dist/analyze/citations.d.ts +256 -0
- package/dist/analyze/citations.js +945 -0
- package/dist/analyze/effects.d.ts +15 -0
- package/dist/analyze/effects.js +255 -0
- package/dist/analyze/fact.d.ts +42 -0
- package/dist/analyze/fact.js +46 -0
- package/dist/analyze/guards.d.ts +70 -0
- package/dist/analyze/guards.js +211 -0
- package/dist/analyze/index.d.ts +26 -0
- package/dist/analyze/index.js +52 -0
- package/dist/analyze/program.d.ts +15 -0
- package/dist/analyze/program.js +229 -0
- package/dist/analyze/surface.d.ts +48 -0
- package/dist/analyze/surface.js +396 -0
- package/dist/bin.d.ts +2 -0
- package/dist/bin.js +12 -0
- package/dist/cli.d.ts +110 -0
- package/dist/cli.js +502 -0
- package/dist/extract/diff.d.ts +35 -0
- package/dist/extract/diff.js +116 -0
- package/dist/extract/git.d.ts +12 -0
- package/dist/extract/git.js +247 -0
- package/dist/extract/index.d.ts +4 -0
- package/dist/extract/index.js +57 -0
- package/dist/extract/intent.d.ts +64 -0
- package/dist/extract/intent.js +238 -0
- package/dist/extract/scope.d.ts +160 -0
- package/dist/extract/scope.js +284 -0
- package/dist/extract/symbols.d.ts +24 -0
- package/dist/extract/symbols.js +230 -0
- package/dist/interpret/client.d.ts +27 -0
- package/dist/interpret/client.js +80 -0
- package/dist/interpret/index.d.ts +41 -0
- package/dist/interpret/index.js +86 -0
- package/dist/interpret/prompt.d.ts +23 -0
- package/dist/interpret/prompt.js +128 -0
- package/dist/interpret/schema.d.ts +74 -0
- package/dist/interpret/schema.js +103 -0
- package/dist/report/conceal.d.ts +63 -0
- package/dist/report/conceal.js +129 -0
- package/dist/report/coverage.d.ts +43 -0
- package/dist/report/coverage.js +56 -0
- package/dist/report/html.d.ts +4 -0
- package/dist/report/html.js +634 -0
- package/dist/report/markdown.d.ts +2 -0
- package/dist/report/markdown.js +168 -0
- package/dist/report/model.d.ts +303 -0
- package/dist/report/model.js +289 -0
- package/dist/report/pdf.d.ts +2 -0
- package/dist/report/pdf.js +217 -0
- package/dist/report/terminal.d.ts +2 -0
- package/dist/report/terminal.js +206 -0
- package/dist/report/write.d.ts +105 -0
- package/dist/report/write.js +160 -0
- package/dist/score/index.d.ts +94 -0
- package/dist/score/index.js +572 -0
- package/dist/score/reach.d.ts +126 -0
- package/dist/score/reach.js +320 -0
- package/dist/score/reconcile.d.ts +52 -0
- package/dist/score/reconcile.js +208 -0
- package/dist/types.d.ts +221 -0
- package/dist/types.js +10 -0
- package/fonts/DejaVuSans-Bold.ttf +0 -0
- package/fonts/DejaVuSans-Oblique.ttf +0 -0
- package/fonts/DejaVuSans.ttf +0 -0
- package/fonts/DejaVuSansMono.ttf +0 -0
- package/fonts/LICENSE +187 -0
- 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
|
+
}
|