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,52 @@
1
+ import { blastRadiusAnalyzer } from "./blast-radius.js";
2
+ import { citationsAnalyzer } from "./citations.js";
3
+ import { effectsAnalyzer } from "./effects.js";
4
+ import { guardsAnalyzer } from "./guards.js";
5
+ import { surfaceAnalyzer } from "./surface.js";
6
+ export { detectEffects, effectsAnalyzer } from "./effects.js";
7
+ export { collectGuards, guardsAnalyzer } from "./guards.js";
8
+ export { exportedSignatures, surfaceAnalyzer } from "./surface.js";
9
+ export { countReferences, blastRadiusAnalyzer } from "./blast-radius.js";
10
+ export { citationsAnalyzer, makeCitationsAnalyzer } from "./citations.js";
11
+ export { createProgramAt, listTypeScriptFilesAt } from "./program.js";
12
+ export { makeFact } from "./fact.js";
13
+ export const ANALYZERS = [
14
+ effectsAnalyzer,
15
+ guardsAnalyzer,
16
+ surfaceAnalyzer,
17
+ blastRadiusAnalyzer,
18
+ citationsAnalyzer,
19
+ ];
20
+ /**
21
+ * Runs every analyzer and returns the facts from the ones that succeeded.
22
+ *
23
+ * `Promise.allSettled`, not `Promise.all`: one compiler-API or git failure
24
+ * used to discard every analyzer's facts and exit non-zero, so a single
25
+ * unreadable revision turned a review with three real findings into no
26
+ * review at all. A degraded review beats no review — but only if the
27
+ * degradation is visible, which is what `onFailure` is for. Callers that
28
+ * pass no handler get the facts and no indication anything was lost, so
29
+ * anything user-facing should pass one.
30
+ */
31
+ export async function runAnalyzers(changeset, ctx, analyzers = ANALYZERS, onFailure) {
32
+ const results = await Promise.allSettled(analyzers.map((a) => a(changeset, ctx)));
33
+ const facts = [];
34
+ results.forEach((result, i) => {
35
+ if (result.status === "fulfilled") {
36
+ facts.push(...result.value);
37
+ return;
38
+ }
39
+ const reason = result.reason;
40
+ onFailure?.({
41
+ // An anonymous arrow assigned to a typed const takes the binding's
42
+ // name (NamedEvaluation of the variable declaration), so `.name` is
43
+ // "surfaceAnalyzer" rather than "" for every analyzer declared that
44
+ // way. The one a factory returns states its name outright instead,
45
+ // because a transform that renames shadowed bindings would otherwise
46
+ // rewrite it — see `makeCitationsAnalyzer` in `./citations.ts`.
47
+ analyzer: analyzers[i].name || `analyzer #${i + 1}`,
48
+ message: reason instanceof Error ? reason.message : String(reason),
49
+ });
50
+ });
51
+ return facts;
52
+ }
@@ -0,0 +1,15 @@
1
+ import ts from "typescript";
2
+ /**
3
+ * Repo-relative TypeScript source paths at a revision. Declaration files are
4
+ * excluded: they contribute no analyzable implementation and inflate the
5
+ * program.
6
+ */
7
+ export declare function listTypeScriptFilesAt(root: string, rev: string): Promise<string[]>;
8
+ /**
9
+ * Build a program over a git revision. For WORKTREE this is equivalent to
10
+ * reading from disk; for a commit, file contents come from git, so a
11
+ * "before" side can be type-checked without touching the working tree.
12
+ */
13
+ export declare function createProgramAt(root: string, rev: string): Promise<ts.Program>;
14
+ /** Repo-relative path for a program source file. */
15
+ export declare function relativePathOf(root: string, sf: ts.SourceFile): string;
@@ -0,0 +1,229 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { isAbsolute, join, relative, resolve } from "node:path";
3
+ import ts from "typescript";
4
+ import { git, readAt } from "../extract/git.js";
5
+ import { isTypeScriptFile } from "../extract/symbols.js";
6
+ import { WORKTREE } from "../types.js";
7
+ /**
8
+ * Any TypeScript file at all, declaration files included — the read-into-the-
9
+ * host filter, wider than `isTypeScriptFile` (which picks program *roots*):
10
+ * an ambient `.d.ts` is part of a revision's type surface and must stay
11
+ * readable and resolvable without being a root. All four implementation
12
+ * extensions and all three declaration flavours end in one of these.
13
+ */
14
+ const TS_SOURCE = /\.(?:ts|tsx|mts|cts)$/;
15
+ const CASE_SENSITIVE = ts.sys.useCaseSensitiveFileNames;
16
+ /**
17
+ * Key for a path that is stable across separators and filesystem casing.
18
+ *
19
+ * TypeScript normalizes every path it hands the host to forward slashes, so
20
+ * a map keyed on Windows `join()` output would never be hit and every source
21
+ * file would come back undefined. One shared implementation, because the
22
+ * host and the map builder disagreeing is precisely the kind of silent miss
23
+ * that would leave the program empty rather than failing.
24
+ */
25
+ function canonicalPath(root, fileName) {
26
+ const abs = isAbsolute(fileName) ? fileName : resolve(root, fileName);
27
+ const slashed = abs.split("\\").join("/").replace(/\/+$/, "");
28
+ return CASE_SENSITIVE ? slashed : slashed.toLowerCase();
29
+ }
30
+ /**
31
+ * Every repo-relative path tracked at a revision, minus anything under
32
+ * node_modules: an installed dependency is toolchain, is served from disk by
33
+ * the compiler host, and is not a function of the revision even on the rare
34
+ * repository that commits it.
35
+ */
36
+ async function listPathsAt(root, rev) {
37
+ const out = rev === WORKTREE
38
+ ? await git(["ls-files", "--cached", "--others", "--exclude-standard"], root)
39
+ : await git(["ls-tree", "-r", "--name-only", rev], root);
40
+ const seen = new Set();
41
+ for (const line of out.split("\n")) {
42
+ const path = line.trim();
43
+ if (!path || path.split("/").includes("node_modules"))
44
+ continue;
45
+ seen.add(path);
46
+ }
47
+ return [...seen];
48
+ }
49
+ /**
50
+ * Repo-relative TypeScript source paths at a revision. Declaration files are
51
+ * excluded: they contribute no analyzable implementation and inflate the
52
+ * program.
53
+ */
54
+ export async function listTypeScriptFilesAt(root, rev) {
55
+ return (await listPathsAt(root, rev)).filter(isTypeScriptFile);
56
+ }
57
+ /** The repo's compiler options, or defaults when it has no usable tsconfig. */
58
+ function compilerOptions(root) {
59
+ const fallback = {
60
+ target: ts.ScriptTarget.ES2022,
61
+ module: ts.ModuleKind.ESNext,
62
+ moduleResolution: ts.ModuleResolutionKind.Bundler,
63
+ strict: true,
64
+ skipLibCheck: true,
65
+ noEmit: true,
66
+ allowJs: false,
67
+ };
68
+ // Deliberately not ts.findConfigFile: that walks up past the repository
69
+ // root and would silently adopt an unrelated ancestor's tsconfig (a real
70
+ // hazard for repos created under a temp or home directory).
71
+ const configPath = join(root, "tsconfig.json");
72
+ if (!ts.sys.fileExists(configPath))
73
+ return fallback;
74
+ const read = ts.readConfigFile(configPath, ts.sys.readFile);
75
+ if (read.error || !read.config)
76
+ return fallback;
77
+ const parsed = ts.parseJsonConfigFileContent(read.config, ts.sys, root);
78
+ // We type-check, never build. Emit-shaped options are dropped rather than
79
+ // merely overridden so that `composite`/`incremental` cannot make the
80
+ // program try to write or read a build-info file.
81
+ const { composite: _composite, incremental: _incremental, tsBuildInfoFile: _tsBuildInfoFile, ...options } = parsed.options;
82
+ return { ...options, noEmit: true, skipLibCheck: true };
83
+ }
84
+ /**
85
+ * A CompilerHost whose repository files come from a git revision rather than
86
+ * disk. Library files (lib.es2022.d.ts and friends) and anything under
87
+ * node_modules still come from the filesystem — they are part of the
88
+ * toolchain, not the repository under review.
89
+ */
90
+ function hostFor(root, contents, directories) {
91
+ const canonicalRoot = canonical(root);
92
+ return {
93
+ getSourceFile(fileName, languageVersion) {
94
+ const text = contents.get(canonical(fileName)) ?? readToolchainFile(fileName);
95
+ if (text === undefined)
96
+ return undefined;
97
+ return ts.createSourceFile(fileName, text, languageVersion, true);
98
+ },
99
+ getDefaultLibFileName: (o) => ts.getDefaultLibFilePath(o),
100
+ writeFile: () => undefined,
101
+ getCurrentDirectory: () => root,
102
+ getCanonicalFileName: (f) => (CASE_SENSITIVE ? f : f.toLowerCase()),
103
+ useCaseSensitiveFileNames: () => CASE_SENSITIVE,
104
+ getNewLine: () => "\n",
105
+ fileExists(f) {
106
+ if (isSubject(f))
107
+ return contents.has(canonical(f));
108
+ return ts.sys.fileExists(f);
109
+ },
110
+ readFile(f) {
111
+ if (isSubject(f))
112
+ return contents.get(canonical(f));
113
+ return ts.sys.readFile(f);
114
+ },
115
+ directoryExists(d) {
116
+ if (isSubject(d))
117
+ return directories.has(canonical(d));
118
+ return ts.sys.directoryExists(d);
119
+ },
120
+ getDirectories(d) {
121
+ if (!isSubject(d))
122
+ return ts.sys.getDirectories(d);
123
+ const prefix = `${canonical(d)}/`;
124
+ const names = new Set();
125
+ for (const dir of directories) {
126
+ if (!dir.startsWith(prefix))
127
+ continue;
128
+ const rest = dir.slice(prefix.length);
129
+ if (rest && !rest.includes("/"))
130
+ names.add(rest);
131
+ }
132
+ return [...names];
133
+ },
134
+ };
135
+ function canonical(fileName) {
136
+ return canonicalPath(root, fileName);
137
+ }
138
+ /**
139
+ * True for paths that belong to the repository under review, i.e. whose
140
+ * content is a function of the revision. Dependencies are excluded: an
141
+ * installed package is toolchain, and its tree is not stored in git.
142
+ */
143
+ function isSubject(fileName) {
144
+ const path = canonical(fileName);
145
+ if (path === canonicalRoot)
146
+ return false; // the root itself always exists
147
+ if (!path.startsWith(`${canonicalRoot}/`))
148
+ return false;
149
+ return !path
150
+ .slice(canonicalRoot.length + 1)
151
+ .split("/")
152
+ .includes("node_modules");
153
+ }
154
+ /**
155
+ * Read a file that is *not* subject to the revision. A repository file
156
+ * absent from `contents` genuinely does not exist at this revision and must
157
+ * NOT be served from the working tree — that is exactly how a "before"
158
+ * program would silently type-check current code and report confident
159
+ * nonsense under a `verified` badge.
160
+ */
161
+ function readToolchainFile(fileName) {
162
+ if (isSubject(fileName))
163
+ return undefined;
164
+ const abs = isAbsolute(fileName) ? fileName : resolve(root, fileName);
165
+ try {
166
+ return readFileSync(abs, "utf8");
167
+ }
168
+ catch {
169
+ return undefined;
170
+ }
171
+ }
172
+ }
173
+ /**
174
+ * Build a program over a git revision. For WORKTREE this is equivalent to
175
+ * reading from disk; for a commit, file contents come from git, so a
176
+ * "before" side can be type-checked without touching the working tree.
177
+ */
178
+ export async function createProgramAt(root, rev) {
179
+ const options = compilerOptions(root);
180
+ // Sources, plus the manifests module resolution consults. A package.json
181
+ // is what tells the compiler whether a directory is ESM or CommonJS under
182
+ // node16/nodenext; without it every relative import in such a repo is
183
+ // resolved under the wrong module format, which is the silent kind of
184
+ // wrongness this host exists to avoid. They come from the revision like
185
+ // any other repository file.
186
+ const paths = (await listPathsAt(root, rev)).filter((p) => TS_SOURCE.test(p) || p === "package.json" || p.endsWith("/package.json"));
187
+ // Reading a commit's files means one `git show` per file. Done serially
188
+ // that is a subprocess round-trip per file and dominates the build on any
189
+ // sizeable repository, so read a few at a time — but collect into a
190
+ // position-indexed array, because the program's root order must not depend
191
+ // on which read happened to finish first.
192
+ const texts = new Array(paths.length);
193
+ let cursor = 0;
194
+ const workers = Array.from({ length: Math.min(8, paths.length) }, async () => {
195
+ while (cursor < paths.length) {
196
+ const i = cursor++;
197
+ texts[i] = await readAt(root, rev, paths[i]);
198
+ }
199
+ });
200
+ await Promise.all(workers);
201
+ const contents = new Map();
202
+ const directories = new Set();
203
+ const rootNames = [];
204
+ for (const [i, p] of paths.entries()) {
205
+ const text = texts[i];
206
+ if (text === null)
207
+ continue; // absent at this revision; not an error here
208
+ const abs = join(root, p);
209
+ const key = canonicalPath(root, abs);
210
+ contents.set(key, text);
211
+ // Only implementation sources are program roots. Declaration files and
212
+ // manifests stay readable and resolvable — an ambient .d.ts is part of
213
+ // the revision's type surface — without inflating the program.
214
+ if (isTypeScriptFile(p))
215
+ rootNames.push(abs);
216
+ const segments = key.split("/");
217
+ for (let i = segments.length - 1; i > 0; i--) {
218
+ const dir = segments.slice(0, i).join("/");
219
+ if (directories.has(dir))
220
+ break;
221
+ directories.add(dir);
222
+ }
223
+ }
224
+ return ts.createProgram(rootNames, options, hostFor(root, contents, directories));
225
+ }
226
+ /** Repo-relative path for a program source file. */
227
+ export function relativePathOf(root, sf) {
228
+ return relative(root, sf.fileName).split("\\").join("/");
229
+ }
@@ -0,0 +1,48 @@
1
+ import ts from "typescript";
2
+ import type { Analyzer } from "../types.js";
3
+ /**
4
+ * A stored signature beyond this length is truncated when a fact records
5
+ * it, so that one enormous type — a huge object literal, or a union with
6
+ * many members — cannot flood a fact with a single string. The marker
7
+ * makes the cut visible: a reader must be able to tell the text was cut,
8
+ * not mistake it for the whole type. Exported for
9
+ * `test/comment-contract.test.ts`, which derives part of its forbidden set
10
+ * from it.
11
+ *
12
+ * Applied at the fact-emission boundary, not inside `exportedSignatures`:
13
+ * the true, uncut text has to exist long enough for its code-point length
14
+ * to be recorded beside the capped text (`beforeChars`/`afterChars` in the
15
+ * fact's detail), because the renderer's own length marker states that
16
+ * true length — a length measured after this cap asserted a false size
17
+ * for exactly the long-literal class the marker exists for.
18
+ */
19
+ export declare const MAX_SIGNATURE_LENGTH = 400;
20
+ /**
21
+ * Matches the marker `truncateSignature` appends. Exported for the render
22
+ * layer (`../score/index.ts`), which drops the marker before applying its
23
+ * own, shorter middle-truncation — the render marker states the true
24
+ * length, and keeping both would leave this one's tail fragment inside
25
+ * the rendered text.
26
+ */
27
+ export declare const SIGNATURE_TRUNCATION_MARKER: RegExp;
28
+ /**
29
+ * Exported name → printed type, for one file in a program. `undefined` when
30
+ * the program has no such source file: that is an unreadable file, not a
31
+ * file with no exports, and the two must not be conflated. Returning an
32
+ * empty map for a missing before-side file would make every export of the
33
+ * after-side file read as `export_added` — absence treated as evidence,
34
+ * one level up from the file-read rule the other analyzers follow.
35
+ *
36
+ * The printed text is the *full* signature, uncapped: `surfaceAnalyzer`
37
+ * measures its true length and applies `truncateSignature` when it stores
38
+ * the text on a fact, and truncating here instead would destroy the length
39
+ * before anything could record it.
40
+ */
41
+ export declare function exportedSignatures(program: ts.Program, root: string, path: string): Map<string, string> | undefined;
42
+ /**
43
+ * Reports changes to a file's public contract: exports added, exports
44
+ * removed, and exports whose type signature changed. A changed signature is
45
+ * the class of change that breaks callers without breaking the build at the
46
+ * point of change, which is why it ranks above an added export.
47
+ */
48
+ export declare const surfaceAnalyzer: Analyzer;