carrick 0.3.106 → 0.3.108

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 (43) hide show
  1. package/dist/hook/refresh.js +8 -1
  2. package/dist/hook/refresh.js.map +1 -1
  3. package/package.json +6 -6
  4. package/plugin/.claude-plugin/plugin.json +1 -1
  5. package/sidecar/dist/src/capture/anchors.d.ts +15 -2
  6. package/sidecar/dist/src/capture/anchors.js +28 -11
  7. package/sidecar/dist/src/capture/api.d.ts +15 -0
  8. package/sidecar/dist/src/capture/check-classify.d.ts +9 -0
  9. package/sidecar/dist/src/capture/check-classify.js +20 -0
  10. package/sidecar/dist/src/capture/check-fields.d.ts +24 -0
  11. package/sidecar/dist/src/capture/check-fields.js +54 -36
  12. package/sidecar/dist/src/capture/check-poison.js +4 -14
  13. package/sidecar/dist/src/capture/check-union.d.ts +40 -0
  14. package/sidecar/dist/src/capture/check-union.js +92 -0
  15. package/sidecar/dist/src/capture/check.js +5 -0
  16. package/sidecar/dist/src/capture/index.js +138 -80
  17. package/sidecar/dist/src/capture/installed-package.d.ts +2 -0
  18. package/sidecar/dist/src/capture/installed-package.js +2 -1
  19. package/sidecar/dist/src/capture/outside-root.d.ts +19 -0
  20. package/sidecar/dist/src/capture/outside-root.js +39 -2
  21. package/sidecar/dist/src/capture/self-check.js +3 -13
  22. package/sidecar/dist/src/capture/specifiers.d.ts +14 -0
  23. package/sidecar/dist/src/capture/specifiers.js +25 -0
  24. package/sidecar/dist/src/definition-resolver.d.ts +5 -8
  25. package/sidecar/dist/src/definition-resolver.js +5 -7
  26. package/sidecar/dist/src/function-line-index.d.ts +30 -0
  27. package/sidecar/dist/src/function-line-index.js +162 -0
  28. package/sidecar/dist/src/index.d.ts +5 -0
  29. package/sidecar/dist/src/index.js +62 -9
  30. package/sidecar/dist/src/infer-timing.d.ts +54 -0
  31. package/sidecar/dist/src/infer-timing.js +124 -0
  32. package/sidecar/dist/src/line-index.d.ts +9 -0
  33. package/sidecar/dist/src/line-index.js +26 -0
  34. package/sidecar/dist/src/progress.d.ts +22 -0
  35. package/sidecar/dist/src/progress.js +31 -0
  36. package/sidecar/dist/src/retype.d.ts +4 -2
  37. package/sidecar/dist/src/retype.js +10 -23
  38. package/sidecar/dist/src/type-inferrer.d.ts +238 -12
  39. package/sidecar/dist/src/type-inferrer.js +772 -168
  40. package/sidecar/dist/src/types.d.ts +83 -6
  41. package/sidecar/dist/src/validators.d.ts +54 -16
  42. package/sidecar/dist/src/validators.js +4 -0
  43. package/templates/skills/carrick-reuse.md +6 -5
@@ -0,0 +1,162 @@
1
+ /**
2
+ * The function a line names, from an index built once per source file
3
+ * (carrick#1915).
4
+ *
5
+ * A signature request carries only the line its function starts on, and a
6
+ * signature pass sends one request per unannotated slot: thousands of
7
+ * lookups, several per function and many per file. Answering each by walking
8
+ * the file's every node cost the file's size per slot, and was 81% of the
9
+ * pass on a 2,345-file program. Here the file is walked once, over the
10
+ * compiler's own nodes, and each lookup reads the few entries near its line.
11
+ *
12
+ * The index belongs to the compiler's source file node, not to the file's
13
+ * name: replacing a file's text gives it a new node, so a file rewritten for
14
+ * one reading (the unwidened reading, the retype check) and then restored is
15
+ * indexed again each time, and never answered from positions it no longer has.
16
+ */
17
+ import { SyntaxKind, ts, } from 'ts-morph';
18
+ import { lineIndex } from './line-index.js';
19
+ /** How far, in lines, a function may start from the line that names it. */
20
+ const LINE_TOLERANCE = 2;
21
+ const FUNCTION_KINDS = new Set([
22
+ SyntaxKind.FunctionDeclaration,
23
+ SyntaxKind.ArrowFunction,
24
+ SyntaxKind.FunctionExpression,
25
+ SyntaxKind.MethodDeclaration,
26
+ ]);
27
+ /** The kinds ts-morph's `Node.isStatement` accepts. */
28
+ const STATEMENT_KINDS = new Set([
29
+ SyntaxKind.Block,
30
+ SyntaxKind.BreakStatement,
31
+ SyntaxKind.ClassDeclaration,
32
+ SyntaxKind.ContinueStatement,
33
+ SyntaxKind.DebuggerStatement,
34
+ SyntaxKind.DoStatement,
35
+ SyntaxKind.EmptyStatement,
36
+ SyntaxKind.EnumDeclaration,
37
+ SyntaxKind.ExportAssignment,
38
+ SyntaxKind.ExportDeclaration,
39
+ SyntaxKind.ExpressionStatement,
40
+ SyntaxKind.ForInStatement,
41
+ SyntaxKind.ForOfStatement,
42
+ SyntaxKind.ForStatement,
43
+ SyntaxKind.FunctionDeclaration,
44
+ SyntaxKind.IfStatement,
45
+ SyntaxKind.ImportDeclaration,
46
+ SyntaxKind.ImportEqualsDeclaration,
47
+ SyntaxKind.InterfaceDeclaration,
48
+ SyntaxKind.LabeledStatement,
49
+ SyntaxKind.ModuleBlock,
50
+ SyntaxKind.ModuleDeclaration,
51
+ SyntaxKind.NotEmittedStatement,
52
+ SyntaxKind.ReturnStatement,
53
+ SyntaxKind.SwitchStatement,
54
+ SyntaxKind.ThrowStatement,
55
+ SyntaxKind.TryStatement,
56
+ SyntaxKind.TypeAliasDeclaration,
57
+ SyntaxKind.VariableStatement,
58
+ SyntaxKind.WhileStatement,
59
+ SyntaxKind.WithStatement,
60
+ ]);
61
+ const indexes = new WeakMap();
62
+ /**
63
+ * The function whose declaration starts at, or within `LINE_TOLERANCE` lines
64
+ * of, the given line: the closest, then the smallest, then the first in the
65
+ * file. A function starting after the line is passed over when a statement
66
+ * that opens on the line or after it, on a line before the function's, does
67
+ * not contain the function. Undefined when no function is left.
68
+ *
69
+ * These are `TypeInferrer.findFunctionByLine`'s rules, which says why each is
70
+ * there; this is where they are computed.
71
+ */
72
+ export function functionAtLine(sourceFile, line) {
73
+ if (!Number.isFinite(line))
74
+ return undefined;
75
+ const index = indexOf(sourceFile.compilerNode);
76
+ /** Statements opening inside the forward window. */
77
+ const windowStatements = [];
78
+ for (let at = Math.ceil(line); at <= line + LINE_TOLERANCE; at++) {
79
+ const opening = index.statementsByLine.get(at);
80
+ if (opening)
81
+ windowStatements.push(...opening);
82
+ }
83
+ const separatedFromAnchor = (fn) => windowStatements.some((statement) => statement.line < fn.line && !(statement.start <= fn.start && statement.end >= fn.end));
84
+ let best;
85
+ for (let at = Math.ceil(line - LINE_TOLERANCE); at <= line + LINE_TOLERANCE; at++) {
86
+ for (const fn of index.functionsByLine.get(at) ?? []) {
87
+ if (fn.line > line && separatedFromAnchor(fn))
88
+ continue;
89
+ if (best === undefined || isBetter(fn, best, line))
90
+ best = fn;
91
+ }
92
+ }
93
+ return best ? wrapped(sourceFile, best) : undefined;
94
+ }
95
+ /** Closer to the line, then smaller, then earlier in the file. */
96
+ function isBetter(fn, best, line) {
97
+ const delta = Math.abs(fn.line - line);
98
+ const bestDelta = Math.abs(best.line - line);
99
+ if (delta !== bestDelta)
100
+ return delta < bestDelta;
101
+ const size = fn.end - fn.start;
102
+ const bestSize = best.end - best.start;
103
+ if (size !== bestSize)
104
+ return size < bestSize;
105
+ return fn.order < best.order;
106
+ }
107
+ function indexOf(file) {
108
+ let index = indexes.get(file);
109
+ if (!index) {
110
+ index = buildIndex(file);
111
+ indexes.set(file, index);
112
+ }
113
+ return index;
114
+ }
115
+ function buildIndex(file) {
116
+ // Lines as ts-morph's `getStartLineNumber` counts them, which is what the
117
+ // scanner's line numbers were matched against.
118
+ const lineAt = lineIndex(file.text);
119
+ const functionsByLine = new Map();
120
+ const statementsByLine = new Map();
121
+ let order = 0;
122
+ const visit = (node) => {
123
+ const isFunction = FUNCTION_KINDS.has(node.kind);
124
+ const isStatement = STATEMENT_KINDS.has(node.kind);
125
+ if (isFunction || isStatement) {
126
+ const start = node.getStart(file);
127
+ const extent = { start, end: node.end, line: lineAt(start) };
128
+ if (isFunction) {
129
+ pushAt(functionsByLine, extent.line, { ...extent, node, order: order++ });
130
+ }
131
+ if (isStatement) {
132
+ pushAt(statementsByLine, extent.line, extent);
133
+ }
134
+ }
135
+ ts.forEachChild(node, visit);
136
+ };
137
+ ts.forEachChild(file, visit);
138
+ return { functionsByLine, statementsByLine };
139
+ }
140
+ function pushAt(byLine, line, entry) {
141
+ const entries = byLine.get(line);
142
+ if (entries)
143
+ entries.push(entry);
144
+ else
145
+ byLine.set(line, [entry]);
146
+ }
147
+ /**
148
+ * The ts-morph node for an indexed function. Only this one is wrapped: the
149
+ * deepest node at the function's first character is the function or sits
150
+ * inside it, so its ancestors lead there, and the rest of the file's nodes are
151
+ * never given wrappers they would keep for the life of the project.
152
+ */
153
+ function wrapped(sourceFile, fn) {
154
+ let node = sourceFile.getDescendantAtPos(fn.start);
155
+ while (node && node.compilerNode !== fn.node) {
156
+ node = node.getParent();
157
+ }
158
+ // An invariant, not a case: no shape is known where the climb misses. If
159
+ // one exists, the answer is still the function the index chose.
160
+ node ??= sourceFile.getFirstDescendant((descendant) => descendant.compilerNode === fn.node);
161
+ return node;
162
+ }
@@ -10,5 +10,10 @@
10
10
  * - stdout is ONLY for JSON responses
11
11
  * - stderr is for logging
12
12
  * - Process stays alive between requests (warm standby)
13
+ * - One request at a time. A handler that runs the compiler blocks the event
14
+ * loop until it returns, so a request sent meanwhile waits in the pipe, and
15
+ * nothing can cancel the one that is running. The scanner kills a process
16
+ * that goes silent past its deadline; a long handler stays alive by writing
17
+ * `progress` frames as it finishes units of work (writeProgress).
13
18
  */
14
19
  export {};
@@ -10,6 +10,11 @@
10
10
  * - stdout is ONLY for JSON responses
11
11
  * - stderr is for logging
12
12
  * - Process stays alive between requests (warm standby)
13
+ * - One request at a time. A handler that runs the compiler blocks the event
14
+ * loop until it returns, so a request sent meanwhile waits in the pipe, and
15
+ * nothing can cancel the one that is running. The scanner kills a process
16
+ * that goes silent past its deadline; a long handler stays alive by writing
17
+ * `progress` frames as it finishes units of work (writeProgress).
13
18
  */
14
19
  import * as path from 'node:path';
15
20
  import * as readline from 'node:readline';
@@ -21,6 +26,8 @@ import { DefinitionResolver } from './definition-resolver.js';
21
26
  import { captureStub, findDisqualifyingTopTypes, jsonWireDeclarations, runCheck, } from './capture/index.js';
22
27
  import { Retyper } from './retype.js';
23
28
  import { LibraryClaimsVerifier, httpCheck } from './library-claims.js';
29
+ import { PROGRESS_INTERVAL_MS, atMostEvery } from './progress.js';
30
+ import { mergeInferTimings } from './infer-timing.js';
24
31
  // ===========================================================================
25
32
  // Module-level state
26
33
  // ===========================================================================
@@ -32,6 +39,13 @@ let initTimeMs = null;
32
39
  * the service's files (carrick#1604).
33
40
  */
34
41
  let components = new Map();
42
+ /**
43
+ * Reads a capture stub's own declaration tree and nothing of the init'd
44
+ * project, so it is not one of the project's components: taking it from them
45
+ * would build the service's whole program to answer about the stub
46
+ * (carrick#1927).
47
+ */
48
+ const definitionResolver = new DefinitionResolver();
35
49
  /**
36
50
  * Get the project-backed components, building the project if this is the
37
51
  * first request that needs it.
@@ -61,7 +75,6 @@ function projectComponents(key = '') {
61
75
  built = {
62
76
  typeBundler: new TypeBundler({ project, repoRoot }),
63
77
  typeInferrer,
64
- definitionResolver: new DefinitionResolver({ project }),
65
78
  // Locates calls exactly as `infer` does, so it rewrites the node the
66
79
  // consumer's published type came from (carrick#1491).
67
80
  // The walk is typed against the capture bundle's compiler copy and
@@ -168,7 +181,11 @@ function handleBundle(request) {
168
181
  log(`Bundling ${request.symbols.length} symbol(s)`);
169
182
  // A symbol named by a path is bundled from the program of the project
170
183
  // that owns that file (carrick#1604).
171
- const results = [...byProject(request.symbols, (symbol) => symbol.source_file)].map(([key, symbols]) => projectComponents(key).typeBundler.bundle(symbols));
184
+ const results = [...byProject(request.symbols, (symbol) => symbol.source_file)].map(([key, symbols]) => {
185
+ const { typeBundler } = projectComponents(key);
186
+ writeProgress(request.request_id, 'bundle', 'program ready');
187
+ return typeBundler.bundle(symbols);
188
+ });
172
189
  const result = results.length === 1 ? results[0] : mergeBundles(results);
173
190
  if (!result.success) {
174
191
  return {
@@ -208,6 +225,10 @@ function handleBundle(request) {
208
225
  function handleCaptureV2(request) {
209
226
  try {
210
227
  log(`capture_v2 for service '${request.service_name}' (${request.anchors.length} anchor(s))`);
228
+ // A frame for each stage as the capture reaches it, and within a stage
229
+ // (one report per anchor) at most one per interval.
230
+ let stage;
231
+ const withinStage = atMostEvery(PROGRESS_INTERVAL_MS, (phase, message) => writeProgress(request.request_id, phase, message));
211
232
  const result = captureStub({
212
233
  repoRoot: request.repo_root,
213
234
  serviceName: request.service_name,
@@ -215,6 +236,12 @@ function handleCaptureV2(request) {
215
236
  outDir: request.out_dir,
216
237
  tsconfigPath: request.tsconfig_path,
217
238
  scanRoot: request.scan_root,
239
+ onProgress: (phase, message) => {
240
+ if (phase === stage)
241
+ return withinStage(phase, message);
242
+ stage = phase;
243
+ writeProgress(request.request_id, phase, message);
244
+ },
218
245
  });
219
246
  return {
220
247
  request_id: request.request_id,
@@ -284,7 +311,13 @@ async function handleCheckV2Async(request) {
284
311
  function handleInfer(request) {
285
312
  try {
286
313
  log(`Inferring ${request.requests.length} type(s)`);
287
- const results = [...byProject(request.requests, (item) => item.file_path)].map(([key, items]) => projectComponents(key).typeInferrer.infer(items, request.extraction_config));
314
+ // One count for the request, whichever programs its items span.
315
+ let done = 0;
316
+ const report = atMostEvery(PROGRESS_INTERVAL_MS, () => writeProgress(request.request_id, 'infer', `${done} of ${request.requests.length}`));
317
+ const results = [...byProject(request.requests, (item) => item.file_path)].map(([key, items]) => projectComponents(key).typeInferrer.infer(items, request.extraction_config, () => {
318
+ done += 1;
319
+ report();
320
+ }));
288
321
  const result = results.length === 1
289
322
  ? results[0]
290
323
  : (() => {
@@ -300,6 +333,9 @@ function handleInfer(request) {
300
333
  request_id: request.request_id,
301
334
  status: result.success ? 'success' : 'error',
302
335
  inferred_types: result.inferred_types,
336
+ // Beside the answers and never in them: what the requests cost, for
337
+ // the caller's log (carrick#1985).
338
+ infer_timing: mergeInferTimings(results.map((r) => r.timing)),
303
339
  errors: result.errors,
304
340
  };
305
341
  }
@@ -329,11 +365,19 @@ function handleRetypeCheck(request) {
329
365
  const groups = byProject(request.items.map((item, index) => ({ item, index })), ({ item }) => item.file_path);
330
366
  const deadline = performance.now() + budget;
331
367
  const outcomes = new Array(request.items.length);
368
+ // One count for the request, whichever programs its items span, reported
369
+ // as `infer` reports its batch (carrick#1914, carrick#1945).
370
+ let judged = 0;
371
+ const report = atMostEvery(PROGRESS_INTERVAL_MS, () => writeProgress(request.request_id, 'retype', `${judged} of ${request.items.length}`));
372
+ const onJudged = () => {
373
+ judged += 1;
374
+ report();
375
+ };
332
376
  for (const [key, entries] of groups) {
333
377
  // One budget for the request, whichever programs it spans.
334
378
  const remaining = groups.size === 1 ? budget : Math.max(0, deadline - performance.now());
335
379
  projectComponents(key)
336
- .retyper.run(entries.map(({ item }) => item), remaining)
380
+ .retyper.run(entries.map(({ item }) => item), remaining, onJudged)
337
381
  .forEach((outcome, position) => {
338
382
  outcomes[entries[position].index] = outcome;
339
383
  });
@@ -388,6 +432,8 @@ function handleVerifyLibraryClaims(request) {
388
432
  const started = performance.now();
389
433
  try {
390
434
  const { claimsVerifier } = projectComponents();
435
+ // The verifier's budget starts now, and so does the scanner's wait.
436
+ writeProgress(request.request_id, 'verify_library_claims', 'program ready');
391
437
  const fromDir = path.resolve(projectLoader.getRepoRoot(), request.from_dir);
392
438
  log(`Verifying ${request.checks.length} library claim(s) from ${fromDir}`);
393
439
  const { semantics, modules } = claimsVerifier.run(fromDir, request.checks, request.budget_ms ?? SEMANTICS_BUDGET_MS);
@@ -428,12 +474,13 @@ function handleListLibrarySurface(request) {
428
474
  }
429
475
  /**
430
476
  * Handle the 'resolve_definitions' action - resolve surface aliases from a
431
- * v2 capture stub package's declaration tree.
477
+ * v2 capture stub package's declaration tree. Stateless, as `capture_v2` is:
478
+ * it needs no init and builds no project but the stub's own.
432
479
  */
433
480
  function handleResolveDefinitions(request) {
434
481
  try {
435
482
  log(`Resolving ${request.aliases.length} type alias(es) from ${request.stub_dir}`);
436
- const results = projectComponents().definitionResolver.resolveFromStub(request.stub_dir, request.aliases);
483
+ const results = definitionResolver.resolveFromStub(request.stub_dir, request.aliases);
437
484
  return {
438
485
  request_id: request.request_id,
439
486
  status: 'success',
@@ -530,9 +577,15 @@ function writeResponse(response) {
530
577
  process.stdout.write(json + '\n');
531
578
  }
532
579
  /**
533
- * Write a non-terminal progress/keepalive frame (async install protocol).
534
- * Distinct `status: 'progress'` so clients skip it and wait for the terminal
535
- * success/error frame.
580
+ * Write a non-terminal progress frame. Distinct `status: 'progress'` so
581
+ * clients skip it and wait for the terminal success/error frame; the scanner
582
+ * restarts its deadline for the request on each one (carrick#1914).
583
+ *
584
+ * A synchronous handler calls this between units of its work, with the event
585
+ * loop blocked. The frame still leaves at once: a write this small to a pipe
586
+ * with room in it completes inside the call, and the reader on the other end
587
+ * drains the pipe on its own thread, so there is room. It is written through
588
+ * the same stream as every answer, so a frame can never land inside one.
536
589
  */
537
590
  function writeProgress(requestId, phase, message) {
538
591
  process.stdout.write(JSON.stringify({ request_id: requestId, status: 'progress', phase, message }) + '\n');
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Which requests of an `infer` batch were slow, and at what (carrick#1985).
3
+ *
4
+ * A pass can send thousands of requests and spend most of its time in a few
5
+ * of them. The inferrer times every request; this module turns those timings
6
+ * into what the batch's answer carries: how many there were and what they
7
+ * add up to, the slowest few by name, the request that printed the longest
8
+ * type, and the requests that were the first asked of their file, apart.
9
+ *
10
+ * A request's time is also told apart by what it went on: the compiler call
11
+ * that computes the type, and the print of that type to text. The two have
12
+ * different remedies, and a printed length alone cannot say which one a slow
13
+ * request was: a type of four characters can take seconds to compute, and a
14
+ * type of a million can take half a second to print.
15
+ *
16
+ * It measures and names. It changes no answer and holds no type text.
17
+ */
18
+ import type { InferSlotTiming, InferTiming } from './types.js';
19
+ /**
20
+ * How many of its slowest requests a batch's answer names.
21
+ *
22
+ * Enough for a reader adding batches up to say what the slowest hundredth of
23
+ * a whole pass took: a pass's slowest requests are among its batches'
24
+ * slowest, and the last one a batch names is the most any it left out can
25
+ * have taken.
26
+ */
27
+ export declare const SLOWEST_SLOTS = 25;
28
+ /** How long something that started at `startedMs` has taken by `nowMs`. */
29
+ export declare function elapsedMs(startedMs: number, nowMs: number): number;
30
+ /**
31
+ * The parts of a request's time that are told apart: `type` is the compiler
32
+ * call that computes a type, `print` is the print of a type to text.
33
+ */
34
+ export type TimedPhase = 'type' | 'print';
35
+ /**
36
+ * Run `work`, and count how long it took as `phase`.
37
+ *
38
+ * The count is the process's, not a request's: whoever wants one request's
39
+ * share reads `phaseClock` before the request and after it. One request runs
40
+ * at a time, so the difference is that request's and nobody else's.
41
+ */
42
+ export declare function timedPhase<T>(phase: TimedPhase, work: () => T): T;
43
+ /** What each phase has come to so far, in milliseconds. */
44
+ export declare function phaseClock(): Readonly<Record<TimedPhase, number>>;
45
+ /**
46
+ * What a batch's answer says of the time its requests took. `slots` holds one
47
+ * timing per request, in the order the batch was done with them.
48
+ */
49
+ export declare function inferTiming(slots: readonly InferSlotTiming[]): InferTiming;
50
+ /**
51
+ * One timing for a request several projects answered, each with its own:
52
+ * the counts and the times added up, and the slowest of all of them.
53
+ */
54
+ export declare function mergeInferTimings(parts: readonly InferTiming[]): InferTiming;
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Which requests of an `infer` batch were slow, and at what (carrick#1985).
3
+ *
4
+ * A pass can send thousands of requests and spend most of its time in a few
5
+ * of them. The inferrer times every request; this module turns those timings
6
+ * into what the batch's answer carries: how many there were and what they
7
+ * add up to, the slowest few by name, the request that printed the longest
8
+ * type, and the requests that were the first asked of their file, apart.
9
+ *
10
+ * A request's time is also told apart by what it went on: the compiler call
11
+ * that computes the type, and the print of that type to text. The two have
12
+ * different remedies, and a printed length alone cannot say which one a slow
13
+ * request was: a type of four characters can take seconds to compute, and a
14
+ * type of a million can take half a second to print.
15
+ *
16
+ * It measures and names. It changes no answer and holds no type text.
17
+ */
18
+ /**
19
+ * How many of its slowest requests a batch's answer names.
20
+ *
21
+ * Enough for a reader adding batches up to say what the slowest hundredth of
22
+ * a whole pass took: a pass's slowest requests are among its batches'
23
+ * slowest, and the last one a batch names is the most any it left out can
24
+ * have taken.
25
+ */
26
+ export const SLOWEST_SLOTS = 25;
27
+ /** Milliseconds to the microsecond: sums of these stay readable. */
28
+ const rounded = (ms) => Math.round(ms * 1000) / 1000;
29
+ /** How long something that started at `startedMs` has taken by `nowMs`. */
30
+ export function elapsedMs(startedMs, nowMs) {
31
+ return rounded(nowMs - startedMs);
32
+ }
33
+ /** What this process has spent in each phase since it started, in milliseconds. */
34
+ const spent = { type: 0, print: 0 };
35
+ /**
36
+ * Run `work`, and count how long it took as `phase`.
37
+ *
38
+ * The count is the process's, not a request's: whoever wants one request's
39
+ * share reads `phaseClock` before the request and after it. One request runs
40
+ * at a time, so the difference is that request's and nobody else's.
41
+ */
42
+ export function timedPhase(phase, work) {
43
+ const started = performance.now();
44
+ try {
45
+ return work();
46
+ }
47
+ finally {
48
+ spent[phase] += performance.now() - started;
49
+ }
50
+ }
51
+ /** What each phase has come to so far, in milliseconds. */
52
+ export function phaseClock() {
53
+ return { ...spent };
54
+ }
55
+ /**
56
+ * The `count` slowest of `slots`, slowest first. Requests that took the same
57
+ * time keep the order they are in.
58
+ */
59
+ function slowestOf(slots, count) {
60
+ return [...slots].sort((a, b) => b.ms - a.ms).slice(0, count);
61
+ }
62
+ /**
63
+ * The one of `slots` that printed the longest type, the earliest of those
64
+ * that tie; undefined when none printed anything.
65
+ */
66
+ function longestPrintedOf(slots) {
67
+ let longest;
68
+ for (const slot of slots) {
69
+ if (slot.printed_length > (longest?.printed_length ?? 0))
70
+ longest = slot;
71
+ }
72
+ return longest;
73
+ }
74
+ /**
75
+ * What a batch's answer says of the time its requests took. `slots` holds one
76
+ * timing per request, in the order the batch was done with them.
77
+ */
78
+ export function inferTiming(slots) {
79
+ let slotsMs = 0;
80
+ let typeMs = 0;
81
+ let printMs = 0;
82
+ let firstInFile = 0;
83
+ let firstInFileMs = 0;
84
+ for (const slot of slots) {
85
+ slotsMs += slot.ms;
86
+ typeMs += slot.type_ms;
87
+ printMs += slot.print_ms;
88
+ if (slot.first_in_file) {
89
+ firstInFile += 1;
90
+ firstInFileMs += slot.ms;
91
+ }
92
+ }
93
+ const longest = longestPrintedOf(slots);
94
+ return {
95
+ slots: slots.length,
96
+ slots_ms: rounded(slotsMs),
97
+ type_ms: rounded(typeMs),
98
+ print_ms: rounded(printMs),
99
+ first_in_file_slots: firstInFile,
100
+ first_in_file_ms: rounded(firstInFileMs),
101
+ slowest: slowestOf(slots, SLOWEST_SLOTS),
102
+ ...(longest ? { longest_printed: longest } : {}),
103
+ };
104
+ }
105
+ /**
106
+ * One timing for a request several projects answered, each with its own:
107
+ * the counts and the times added up, and the slowest of all of them.
108
+ */
109
+ export function mergeInferTimings(parts) {
110
+ if (parts.length === 1)
111
+ return parts[0];
112
+ const sum = (read) => parts.reduce((total, part) => total + read(part), 0);
113
+ const longest = longestPrintedOf(parts.flatMap((part) => part.longest_printed ?? []));
114
+ return {
115
+ slots: sum((part) => part.slots),
116
+ slots_ms: rounded(sum((part) => part.slots_ms)),
117
+ type_ms: rounded(sum((part) => part.type_ms)),
118
+ print_ms: rounded(sum((part) => part.print_ms)),
119
+ first_in_file_slots: sum((part) => part.first_in_file_slots),
120
+ first_in_file_ms: rounded(sum((part) => part.first_in_file_ms)),
121
+ slowest: slowestOf(parts.flatMap((part) => part.slowest), SLOWEST_SLOTS),
122
+ ...(longest ? { longest_printed: longest } : {}),
123
+ };
124
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * The 1-based line of a position in a text, counted by line feeds alone: one
3
+ * more than the `\n`s before the position. That is the count ts-morph's
4
+ * `getStartLineNumber` makes. A lone carriage return or a Unicode line
5
+ * separator ends a line for the compiler and not here.
6
+ *
7
+ * The text is read once; each lookup after that is a binary search.
8
+ */
9
+ export declare function lineIndex(text: string): (pos: number) => number;
@@ -0,0 +1,26 @@
1
+ /**
2
+ * The 1-based line of a position in a text, counted by line feeds alone: one
3
+ * more than the `\n`s before the position. That is the count ts-morph's
4
+ * `getStartLineNumber` makes. A lone carriage return or a Unicode line
5
+ * separator ends a line for the compiler and not here.
6
+ *
7
+ * The text is read once; each lookup after that is a binary search.
8
+ */
9
+ export function lineIndex(text) {
10
+ const starts = [0];
11
+ for (let i = 0; i < text.length; i++)
12
+ if (text[i] === '\n')
13
+ starts.push(i + 1);
14
+ return (pos) => {
15
+ let lo = 0;
16
+ let hi = starts.length - 1;
17
+ while (lo < hi) {
18
+ const mid = (lo + hi + 1) >> 1;
19
+ if (starts[mid] <= pos)
20
+ lo = mid;
21
+ else
22
+ hi = mid - 1;
23
+ }
24
+ return lo + 1;
25
+ };
26
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Pacing for the `progress` frames a long request writes (carrick#1914).
3
+ */
4
+ /**
5
+ * The least time between two progress frames of one request.
6
+ *
7
+ * The scanner's deadline for a request is how long the sidecar may stay
8
+ * silent about it, so a frame has to say that a unit of work finished, and
9
+ * has to be far more frequent than that deadline. It does not have to follow
10
+ * every unit: a batch can finish thousands in a second.
11
+ */
12
+ export declare const PROGRESS_INTERVAL_MS = 1500;
13
+ /**
14
+ * `report`, held to one call per `intervalMs`: the first call goes through at
15
+ * once, and a call made sooner than `intervalMs` after the last one that went
16
+ * through is dropped.
17
+ *
18
+ * It is called by the work, between units, and never by a timer. A handler
19
+ * that stops finishing units stops reporting, which is what the reader on the
20
+ * other end is waiting to notice.
21
+ */
22
+ export declare function atMostEvery<Args extends unknown[]>(intervalMs: number, report: (...args: Args) => void, now?: () => number): (...args: Args) => void;
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Pacing for the `progress` frames a long request writes (carrick#1914).
3
+ */
4
+ /**
5
+ * The least time between two progress frames of one request.
6
+ *
7
+ * The scanner's deadline for a request is how long the sidecar may stay
8
+ * silent about it, so a frame has to say that a unit of work finished, and
9
+ * has to be far more frequent than that deadline. It does not have to follow
10
+ * every unit: a batch can finish thousands in a second.
11
+ */
12
+ export const PROGRESS_INTERVAL_MS = 1500;
13
+ /**
14
+ * `report`, held to one call per `intervalMs`: the first call goes through at
15
+ * once, and a call made sooner than `intervalMs` after the last one that went
16
+ * through is dropped.
17
+ *
18
+ * It is called by the work, between units, and never by a timer. A handler
19
+ * that stops finishing units stops reporting, which is what the reader on the
20
+ * other end is waiting to notice.
21
+ */
22
+ export function atMostEvery(intervalMs, report, now = () => performance.now()) {
23
+ let last = Number.NEGATIVE_INFINITY;
24
+ return (...args) => {
25
+ const at = now();
26
+ if (at - last < intervalMs)
27
+ return;
28
+ last = at;
29
+ report(...args);
30
+ };
31
+ }
@@ -53,9 +53,11 @@ export declare class Retyper {
53
53
  * Judge every item, spending at most `budgetMs`. Each item rebuilds the
54
54
  * program at least twice, so a consumer with many calls could otherwise
55
55
  * outrun the caller's read deadline and lose every answer; the items the
56
- * budget does not reach abstain and say so.
56
+ * budget does not reach abstain and say so. `onJudged` is called after each
57
+ * item the budget reached, so the caller can report progress while the
58
+ * event loop is blocked (carrick#1945).
57
59
  */
58
- run(items: RetypeItem[], budgetMs: number): RetypeOutcome[];
60
+ run(items: RetypeItem[], budgetMs: number, onJudged?: () => void): RetypeOutcome[];
59
61
  private runOne;
60
62
  /**
61
63
  * The rewrite that states the producer's UNWIDENED return (carrick#1516),