carrick 0.3.85 → 0.3.87

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.
@@ -18,9 +18,8 @@
18
18
  * function's return type.
19
19
  */
20
20
  import { Node, SyntaxKind, ts, } from 'ts-morph';
21
- import * as fs from 'node:fs';
22
- import * as path from 'node:path';
23
21
  import { validateInferRequestItem } from './validators.js';
22
+ import { isExternalOrigin } from './origin.js';
24
23
  import { expandTypeStructural, } from './type-structural-expander.js';
25
24
  /**
26
25
  * TS/lib globals and primitives that must never be emitted as a deterministic
@@ -152,75 +151,6 @@ const RESPONSE_INIT_MEMBER_NAMES = new Set([
152
151
  'statusText',
153
152
  'headers',
154
153
  ]);
155
- /**
156
- * True when a declaration's source file is runtime/library origin rather than
157
- * user source. Four answers, in order:
158
- *
159
- * 1. The runtime declarations Carrick materialises for a non-Node runtime
160
- * under `.carrick/deno/` (carrick#1017), and the remote (JSR, `https:`)
161
- * modules it copies beside them. Carrick's own artefact layout, not a
162
- * guess about anyone else's.
163
- * 2. A TypeScript default library (`lib.dom.d.ts`, ...), as the program
164
- * classifies it; on a bare checkout the DOM `Response` resolves from here.
165
- * 3. An install under a `node_modules` segment, however it entered the
166
- * program (an import, or a root the loader registered).
167
- * 4. A file the PROGRAM'S RESOLVER marked as an external library import.
168
- * This is the graph-backed answer for a project whose resolution does not
169
- * go through `node_modules` (carrick#1264): Deno serves an npm dependency's
170
- * types from its own cache, a path with no `node_modules` segment, and
171
- * `DenoProject.resolve` hands the compiler `isExternalLibraryImport` from
172
- * the graph, which the compiler records on the file. Nothing crosses the
173
- * capture seam; both programs are built with that host. One exclusion: a
174
- * workspace package reached through a `node_modules` symlink is also
175
- * marked external by the compiler but is the user's own source, so a file
176
- * inside the checkout (the nearest `.git` above the service root) that
177
- * carries no `node_modules` segment stays user source.
178
- *
179
- * Lockstep mirror of `isExternalOrigin` in `capture/machinery.ts` (the capture
180
- * seam forbids sharing a module); `machinery-indicator-mirror.test.ts` guards
181
- * the pair on a real program.
182
- */
183
- export function isExternalOrigin(program, sourceFile, repoRoot) {
184
- const file = sourceFile.fileName.replace(/\\/g, '/');
185
- if (file.includes('/.carrick/deno/')) {
186
- return true;
187
- }
188
- if (program.isSourceFileDefaultLibrary(sourceFile)) {
189
- return true;
190
- }
191
- if (file.includes('/node_modules/')) {
192
- return true;
193
- }
194
- return program.isSourceFileFromExternalLibrary(sourceFile) && !isInsideCheckout(file, repoRoot);
195
- }
196
- const checkoutRoots = new Map();
197
- /** The checkout the service root sits in: the nearest ancestor holding a
198
- * `.git` entry (a directory, or the file a worktree carries), else the service
199
- * root itself. */
200
- function checkoutRootOf(repoRoot) {
201
- const key = path.resolve(repoRoot);
202
- const cached = checkoutRoots.get(key);
203
- if (cached)
204
- return cached;
205
- let dir = key;
206
- let root = key;
207
- for (;;) {
208
- if (fs.existsSync(path.join(dir, '.git'))) {
209
- root = dir;
210
- break;
211
- }
212
- const parent = path.dirname(dir);
213
- if (parent === dir)
214
- break;
215
- dir = parent;
216
- }
217
- checkoutRoots.set(key, root);
218
- return root;
219
- }
220
- function isInsideCheckout(file, repoRoot) {
221
- const root = checkoutRootOf(repoRoot).replace(/\\/g, '/');
222
- return file === root || file.startsWith(root.endsWith('/') ? root : root + '/');
223
- }
224
154
  /**
225
155
  * How far the response-helper recovery (carrick#631) descends through nested
226
156
  * calls looking for the payload argument. `cors(request, json(payload))` needs
@@ -304,6 +234,18 @@ export class TypeInferrer {
304
234
  this.packageOf = options.packageOf;
305
235
  this.repoRoot = options.repoRoot;
306
236
  }
237
+ /**
238
+ * What the structural printer needs to tell the user's own declarations from
239
+ * the runtime's and its packages' — the same program and service root this
240
+ * class asks `isExternalOrigin` about machinery, so the two layers cannot
241
+ * classify one declaration two ways.
242
+ */
243
+ expandOrigin() {
244
+ return {
245
+ program: this.project.getProgram().compilerObject,
246
+ repoRoot: this.repoRoot,
247
+ };
248
+ }
307
249
  /**
308
250
  * Infer types for the given requests
309
251
  *
@@ -2610,7 +2552,7 @@ export class TypeInferrer {
2610
2552
  const fallback = typeNode.getText();
2611
2553
  try {
2612
2554
  const annotationType = this.unwrapPromiseType(typeNode.getType());
2613
- const expanded = expandTypeStructural(annotationType, new Set(), 0, undefined, wire);
2555
+ const expanded = expandTypeStructural(annotationType, this.expandOrigin(), { wire });
2614
2556
  // Only prefer the structural form when expansion actually inlined an
2615
2557
  // object shape; otherwise keep the annotation text (e.g. a bare
2616
2558
  // primitive or a library type the expander leaves by name). A wire
@@ -2618,7 +2560,7 @@ export class TypeInferrer {
2618
2560
  // `Date` annotation sends a string (carrick#1163).
2619
2561
  if (expanded.startsWith('{'))
2620
2562
  return expanded;
2621
- return wire === 'json' && expanded !== expandTypeStructural(annotationType)
2563
+ return wire === 'json' && expanded !== expandTypeStructural(annotationType, this.expandOrigin())
2622
2564
  ? expanded
2623
2565
  : fallback;
2624
2566
  }
@@ -2644,12 +2586,12 @@ export class TypeInferrer {
2644
2586
  */
2645
2587
  expandResolvedTypeStructural(type, fallback, wire = 'declared') {
2646
2588
  try {
2647
- const expanded = expandTypeStructural(type, new Set(), 0, undefined, wire);
2589
+ const expanded = expandTypeStructural(type, this.expandOrigin(), { wire });
2648
2590
  // A wire print that differs from the declared one is the answer even
2649
2591
  // without an inlined object: a bare `Date` payload sends a string.
2650
2592
  if (wire === 'json' &&
2651
2593
  !expanded.includes('{') &&
2652
- expanded !== expandTypeStructural(type)) {
2594
+ expanded !== expandTypeStructural(type, this.expandOrigin())) {
2653
2595
  return expanded;
2654
2596
  }
2655
2597
  // Prefer the expanded form whenever an object got inlined, not only when
@@ -3029,7 +2971,16 @@ export class TypeInferrer {
3029
2971
  * True when an object literal argument is response INIT rather than a body:
3030
2972
  * every property it declares is one the standard `ResponseInit` declares
3031
2973
  * (`status`, `statusText`, `headers`), and it states at least one of them as
3032
- * init really does — a numeric status in the HTTP range, or headers.
2974
+ * init really does — a status in the HTTP range, or headers.
2975
+ *
2976
+ * The status does NOT have to be a literal code. A route that carries its
2977
+ * outcome in a value writes `new Response(body, { status: result.status })`,
2978
+ * where the source fixes no code and `statedStatusCodes` answers
2979
+ * `'variable'` — status-shaped, just not pinned. Requiring a literal there
2980
+ * left the whole init object reading as a body, so an endpoint whose payload
2981
+ * this layer does not publish (a string body) published `{ status: number }`
2982
+ * as its response contract instead: a wrong contract, served, where an
2983
+ * abstention was the honest answer.
3033
2984
  *
3034
2985
  * A payload that merely has a `status` member of its own (`{ status: "ok",
3035
2986
  * service: "ledger" }`) declares members init does not, or states `status` as
@@ -3058,7 +3009,7 @@ export class TypeInferrer {
3058
3009
  statesInit = true;
3059
3010
  continue;
3060
3011
  }
3061
- if (name === 'status' && Array.isArray(this.statedStatusCodes(value))) {
3012
+ if (name === 'status' && this.statedStatusCodes(value) !== undefined) {
3062
3013
  statesInit = true;
3063
3014
  }
3064
3015
  }
@@ -4509,7 +4460,9 @@ export class TypeInferrer {
4509
4460
  const overrides = { types, applied: new Set(), at };
4510
4461
  let text;
4511
4462
  try {
4512
- text = expandTypeStructural(this.unwrapPromiseType(input), new Set(), 0, overrides);
4463
+ text = expandTypeStructural(this.unwrapPromiseType(input), this.expandOrigin(), {
4464
+ overrides,
4465
+ });
4513
4466
  }
4514
4467
  catch {
4515
4468
  return null;
@@ -28,7 +28,7 @@
28
28
  * `type-inferrer.ts` (consumer-side inference), so both paths emit the same
29
29
  * structural form rather than a dangling name.
30
30
  */
31
- import { type Node, type Type } from 'ts-morph';
31
+ import { type Node, type Type, ts } from 'ts-morph';
32
32
  /**
33
33
  * Bound on the structural-expansion recursion. Deep enough for every realistic
34
34
  * request/response shape; a backstop against pathological/recursive types the
@@ -73,17 +73,49 @@ export interface MemberOverrides {
73
73
  * signature, never from a list of type names.
74
74
  */
75
75
  export type WireFormat = 'declared' | 'json';
76
+ /**
77
+ * The program the walked type belongs to, and the service root inside it.
78
+ *
79
+ * Required, not optional, because the walk cannot decide from a type alone
80
+ * whether a declaration is the user's source or something the runtime
81
+ * installed: a path test answers that only where resolution goes through
82
+ * `node_modules`, and a runtime that serves an npm dependency's types out of
83
+ * its own cache leaves no such segment in the path (carrick#1264). The program
84
+ * carries the resolver's own verdict, so it is what `isExternalOrigin` is
85
+ * asked — the same instrument the inference path uses, so the two layers
86
+ * cannot disagree about which types to inline.
87
+ */
88
+ export interface ExpandOrigin {
89
+ readonly program: ts.Program;
90
+ readonly repoRoot: string;
91
+ }
92
+ /** Everything `expandTypeStructural` takes besides the type and its origin. */
93
+ export interface ExpandOptions {
94
+ /** Substitutions at named member positions; see `MemberOverrides`. */
95
+ readonly overrides?: MemberOverrides;
96
+ /** Which representation to print; see `WireFormat`. */
97
+ readonly wire?: WireFormat;
98
+ /**
99
+ * Where in the recursion the walk starts. Production callers never set it —
100
+ * depth is walk state — but the `MAX_EXPANSION_DEPTH` backstop is reachable
101
+ * from a shallow type only by starting the walk at the bound, which is how
102
+ * its union ordering is tested.
103
+ */
104
+ readonly depth?: number;
105
+ }
76
106
  /**
77
107
  * Recursively render a `Type` as fully-inlined structural text.
78
108
  *
79
109
  * Named object/interface types are expanded to their member structure;
80
110
  * primitives, literals, library types (`Date`, `Promise`, tuples, …) and
81
- * functions stay by name. The `seen` set (object type ids on the current
82
- * branch) breaks reference cycles; `depth` is a hard backstop. `overrides`
83
- * substitutes a type at named member positions (`MemberOverrides`); without
84
- * it the print is unchanged. `wire` picks the representation (`WireFormat`).
111
+ * functions stay by name — `origin` is what decides which is which. A cycle
112
+ * set (object type ids on the current branch) breaks reference cycles and
113
+ * `MAX_EXPANSION_DEPTH` is a hard backstop; both are walk state, not caller
114
+ * state. `overrides` substitutes a type at named member positions
115
+ * (`MemberOverrides`); without it the print is unchanged. `wire` picks the
116
+ * representation (`WireFormat`).
85
117
  */
86
- export declare function expandTypeStructural(type: Type, seen?: Set<number>, depth?: number, overrides?: MemberOverrides, wire?: WireFormat): string;
118
+ export declare function expandTypeStructural(type: Type, origin: ExpandOrigin, options?: ExpandOptions): string;
87
119
  /**
88
120
  * The type `JSON.stringify` serialises in place of a value of `type`: the
89
121
  * return type of the value's own `toJSON()`, or `undefined` when it declares
@@ -30,6 +30,7 @@
30
30
  */
31
31
  import { ts } from 'ts-morph';
32
32
  import { canonicalizeUnionsInText, foldBooleanLiterals, } from './type-text-canonicalizer.js';
33
+ import { isExternalOrigin } from './origin.js';
33
34
  /**
34
35
  * Bound on the structural-expansion recursion. Deep enough for every realistic
35
36
  * request/response shape; a backstop against pathological/recursive types the
@@ -62,13 +63,16 @@ function childCursor(cursor, segment) {
62
63
  *
63
64
  * Named object/interface types are expanded to their member structure;
64
65
  * primitives, literals, library types (`Date`, `Promise`, tuples, …) and
65
- * functions stay by name. The `seen` set (object type ids on the current
66
- * branch) breaks reference cycles; `depth` is a hard backstop. `overrides`
67
- * substitutes a type at named member positions (`MemberOverrides`); without
68
- * it the print is unchanged. `wire` picks the representation (`WireFormat`).
66
+ * functions stay by name — `origin` is what decides which is which. A cycle
67
+ * set (object type ids on the current branch) breaks reference cycles and
68
+ * `MAX_EXPANSION_DEPTH` is a hard backstop; both are walk state, not caller
69
+ * state. `overrides` substitutes a type at named member positions
70
+ * (`MemberOverrides`); without it the print is unchanged. `wire` picks the
71
+ * representation (`WireFormat`).
69
72
  */
70
- export function expandTypeStructural(type, seen = new Set(), depth = 0, overrides, wire = 'declared') {
71
- return expandAt(type, seen, depth, overrides ? { overrides, position: '' } : undefined, wire);
73
+ export function expandTypeStructural(type, origin, options = {}) {
74
+ const { overrides, wire = 'declared', depth = 0 } = options;
75
+ return expandAt(type, new Set(), depth, overrides ? { overrides, position: '' } : undefined, wire, origin);
72
76
  }
73
77
  /**
74
78
  * The type `JSON.stringify` serialises in place of a value of `type`: the
@@ -88,7 +92,7 @@ export function jsonWireType(type) {
88
92
  const declaration = member.getValueDeclaration() ?? member.getDeclarations()[0];
89
93
  const memberType = declaration
90
94
  ? member.getTypeAtLocation(declaration)
91
- : member.getDeclaredType();
95
+ : memberTypeWithoutDeclaration(member, type);
92
96
  const signature = memberType.getCallSignatures()[0];
93
97
  if (!signature)
94
98
  return undefined;
@@ -101,7 +105,7 @@ export function jsonWireType(type) {
101
105
  return undefined;
102
106
  return serialised;
103
107
  }
104
- function expandAt(type, seen, depth, at, wire) {
108
+ function expandAt(type, seen, depth, at, wire, origin) {
105
109
  if (depth > MAX_EXPANSION_DEPTH)
106
110
  return backstopText(type);
107
111
  let cursor = at;
@@ -109,7 +113,7 @@ function expandAt(type, seen, depth, at, wire) {
109
113
  const replacement = cursor.overrides.types.get(cursor.position);
110
114
  if (replacement) {
111
115
  cursor.overrides.applied.add(cursor.position);
112
- return expandAt(replacement, seen, depth, undefined, wire);
116
+ return expandAt(replacement, seen, depth, undefined, wire, origin);
113
117
  }
114
118
  // Nothing to substitute below here: print exactly as without overrides.
115
119
  if (!hasOverrideBelow(cursor))
@@ -133,10 +137,10 @@ function expandAt(type, seen, depth, at, wire) {
133
137
  }
134
138
  // Unions / intersections: expand each member, in canonical order.
135
139
  if (type.isUnion()) {
136
- return canonicalMembers(type.getUnionTypes(), seen, depth, cursor, wire).join(' | ');
140
+ return canonicalMembers(type.getUnionTypes(), seen, depth, cursor, wire, origin).join(' | ');
137
141
  }
138
142
  if (type.isIntersection()) {
139
- return canonicalMembers(type.getIntersectionTypes(), seen, depth, cursor, wire).join(' & ');
143
+ return canonicalMembers(type.getIntersectionTypes(), seen, depth, cursor, wire, origin).join(' & ');
140
144
  }
141
145
  // Tuples are array-like but must keep their `[a, b]` shape, not be walked
142
146
  // as objects (which explodes into `Array.prototype`). Handle before arrays.
@@ -147,7 +151,7 @@ function expandAt(type, seen, depth, at, wire) {
147
151
  const element = type.getArrayElementType();
148
152
  if (!element)
149
153
  return namedText(type);
150
- const inner = expandAt(element, seen, depth + 1, childCursor(cursor, '<0>'), wire);
154
+ const inner = expandAt(element, seen, depth + 1, childCursor(cursor, '<0>'), wire, origin);
151
155
  // Parenthesise a union/intersection element so `(A | B)[]` doesn't misparse
152
156
  // as `A | B[]`. Decide from the TYPE, not the string: a single object
153
157
  // literal like `{ a: A | B }` is NOT a union and must not be parenthesised,
@@ -160,12 +164,12 @@ function expandAt(type, seen, depth, at, wire) {
160
164
  if (wire === 'json') {
161
165
  const serialised = jsonWireType(type);
162
166
  if (serialised)
163
- return expandAt(serialised, seen, depth + 1, undefined, wire);
167
+ return expandAt(serialised, seen, depth + 1, undefined, wire, origin);
164
168
  }
165
169
  // Library / built-in types (Date, Promise, RegExp, …): keep by name, unless
166
170
  // a member below has to be substituted — a schema library declares its
167
171
  // inferred object types itself, and they are only walkable, not by-name.
168
- if (!cursor && isLibraryType(type)) {
172
+ if (!cursor && isLibraryType(type, origin)) {
169
173
  return namedText(type);
170
174
  }
171
175
  // Callable/constructable object types (functions): keep by name; their
@@ -182,7 +186,7 @@ function expandAt(type, seen, depth, at, wire) {
182
186
  const props = type.getProperties();
183
187
  if (props.length === 0)
184
188
  return namedText(type);
185
- const parts = props.map((prop) => expandProperty(prop, nextSeen, depth, cursor, wire));
189
+ const parts = props.map((prop) => expandProperty(prop, type, nextSeen, depth, cursor, wire, origin));
186
190
  return `{ ${parts.join('; ')}; }`;
187
191
  }
188
192
  return namedText(type);
@@ -241,8 +245,8 @@ function backstopText(type) {
241
245
  * Ties can only happen between two members that render identically, in which
242
246
  * case the joined output is the same whichever way round they go.
243
247
  */
244
- function canonicalMembers(members, seen, depth, cursor, wire) {
245
- return orderMembers(members, (member) => expandAt(member, seen, depth + 1, cursor, wire));
248
+ function canonicalMembers(members, seen, depth, cursor, wire, origin) {
249
+ return orderMembers(members, (member) => expandAt(member, seen, depth + 1, cursor, wire, origin));
246
250
  }
247
251
  /**
248
252
  * The canonical order itself, over whatever text `render` gives each member.
@@ -263,7 +267,7 @@ function orderMembers(members, render) {
263
267
  return rendered.map((entry) => entry.text);
264
268
  }
265
269
  /** Render a single property as `name[?]: <expanded>`. */
266
- function expandProperty(prop, seen, depth, at, wire) {
270
+ function expandProperty(prop, owner, seen, depth, at, wire, origin) {
267
271
  const optional = (prop.getFlags() & ts.SymbolFlags.Optional) !== 0;
268
272
  const propDecl = prop.getDeclarations()[0];
269
273
  // Render the key from the declaration's name node so quoted/computed keys
@@ -274,7 +278,7 @@ function expandProperty(prop, seen, depth, at, wire) {
274
278
  ? prop.getTypeAtLocation(at.overrides.at)
275
279
  : propDecl
276
280
  ? prop.getTypeAtLocation(propDecl)
277
- : prop.getDeclaredType();
281
+ : memberTypeWithoutDeclaration(prop, owner);
278
282
  // A substituted member takes the override's TYPE but keeps this key's
279
283
  // optionality, and is looked up before the `undefined` strip below so an
280
284
  // override that carries `| undefined` strips like any other optional key.
@@ -295,13 +299,36 @@ function expandProperty(prop, seen, depth, at, wire) {
295
299
  propType = nonUndefined[0];
296
300
  }
297
301
  else if (nonUndefined.length > 1) {
298
- const inner = canonicalMembers(nonUndefined, seen, depth, cursor, wire).join(' | ');
302
+ const inner = canonicalMembers(nonUndefined, seen, depth, cursor, wire, origin).join(' | ');
299
303
  return `${name}?: ${inner}`;
300
304
  }
301
305
  }
302
- const inner = expandAt(propType, seen, depth + 1, cursor, wire);
306
+ const inner = expandAt(propType, seen, depth + 1, cursor, wire, origin);
303
307
  return `${name}${optional ? '?' : ''}: ${inner}`;
304
308
  }
309
+ /**
310
+ * The type of a member the CHECKER synthesised, which has no declaration of
311
+ * its own to be read at (carrick#1433).
312
+ *
313
+ * A mapped type's members — what a query builder's projection, a
314
+ * `GetPayload<…>`-style generic or any homomorphic mapping produces — carry no
315
+ * declaration node. `Symbol.getDeclaredType()` answers `any` for such a symbol
316
+ * (it is the DECLARED type of a type symbol, and a value member declares
317
+ * none), so the printed contract lost every field the compiler had resolved:
318
+ * `{ id: string; createdAt: Date }` printed as `{ id: any; createdAt: any }`
319
+ * and the row was demoted for carrying a top type.
320
+ *
321
+ * The member is read at the owning type's own declaration instead — the mapped
322
+ * type node the checker instantiated. A synthesised member's type does not
323
+ * depend on the location it is read at (only narrowing and `this` do, and it
324
+ * has neither), so this is the instantiated member type; it is the same answer
325
+ * the caller's own node gives. With no declaration anywhere to read at, the
326
+ * declared type is still the only thing left to ask for.
327
+ */
328
+ function memberTypeWithoutDeclaration(prop, owner) {
329
+ const ownerDecl = (owner.getSymbol() ?? owner.getAliasSymbol())?.getDeclarations()?.[0];
330
+ return ownerDecl ? prop.getTypeAtLocation(ownerDecl) : prop.getDeclaredType();
331
+ }
305
332
  /**
306
333
  * The property key as valid TS text. Uses the declaration's name node so a
307
334
  * quoted (`'x-y'`) or computed (`[Symbol.iterator]`) key keeps its syntax;
@@ -320,24 +347,26 @@ function isTuple(type) {
320
347
  return ((target.objectFlags ?? 0) & ts.ObjectFlags.Tuple) !== 0;
321
348
  }
322
349
  /**
323
- * True for types declared in `node_modules` or a TS `lib.*.d.ts` (Date,
324
- * Promise, RegExp, …). These stay by name rather than being inlined.
350
+ * True for types the runtime or an installed package declares (Date, Promise,
351
+ * RegExp, a framework's own types, …). These stay by name rather than being
352
+ * inlined.
353
+ *
354
+ * Asked of the program (`isExternalOrigin`), not of the path: where resolution
355
+ * does not go through `node_modules` — a runtime serving an npm dependency's
356
+ * types from its own cache — a path test recognises nothing, and the walk
357
+ * inlines a library's internals as if they were the user's contract. An
358
+ * interface that extends `Array<T>` then prints as the whole array prototype,
359
+ * whose signatures carry `thisArg?: any`, and the row is demoted for a top
360
+ * type that is not in the contract at all (carrick#1264).
325
361
  */
326
- function isLibraryType(type) {
362
+ function isLibraryType(type, origin) {
327
363
  const symbol = type.getSymbol() ?? type.getAliasSymbol();
328
364
  if (!symbol)
329
365
  return false;
330
366
  const decls = symbol.getDeclarations();
331
367
  if (decls.length === 0)
332
368
  return false;
333
- return decls.some((decl) => {
334
- const sf = decl.getSourceFile();
335
- if (sf.isInNodeModules())
336
- return true;
337
- return (sf.isDeclarationFile() &&
338
- // Normalize separators so a Windows `\\` path still matches lib.*.d.ts.
339
- /(^|\/)lib\.[^/]*\.d\.ts$/.test(sf.getFilePath().replace(/\\/g, '/')));
340
- });
369
+ return decls.some((decl) => isExternalOrigin(origin.program, decl.getSourceFile().compilerNode, origin.repoRoot));
341
370
  }
342
371
  /**
343
372
  * Non-expanded text for a type. Passes `undefined` as the enclosing node so
@@ -53,6 +53,11 @@ Report these numbers before the list, per query where the field is per query:
53
53
  - `hidden_by_threshold` where the answer carries it: how many rows the floor
54
54
  removed, and `best` where it names the closest of them. A count above zero is
55
55
  the case for one more search at a lower `similarity_threshold`;
56
+ - `hidden_by_lexical_floor` where the answer carries it: how many rows matched a
57
+ word of the query but too little of its word weight to rank, and `best` where
58
+ it names the nearest of them, with the weight it reached against the bar it
59
+ had to clear. A lower `similarity_threshold` does not reach these rows. The
60
+ search that does names the identifier or the rare term itself;
56
61
  - `total_without_intent`, which is index-wide: functions carrying no intent
57
62
  text, which the search ranked on their name, signature and body tokens
58
63
  alone, so a query that names one finds it and a query that describes what
@@ -57,10 +57,17 @@ For each consumer service the step above listed:
57
57
  check_compatibility({{SCOPE}}, consumer_service: "<consumer>", producer_service: "<producer>", path: "<path>")
58
58
  ```
59
59
 
60
- Pass `path`. Without it a large producer returns hundreds of rows. Read
61
- `type_verdicts` (`compatible`, `incompatible`, `unresolved`, `not_compared`) and
62
- the `issues` rows for this operation. A `not_compared` pair has no stored
63
- verdict, which is never agreement.
60
+ Pass `path`. Without it a large producer returns hundreds of rows.
61
+
62
+ Read `status` before the buckets: `compatible`, `incompatible`,
63
+ `partially_checked`, or `unresolved` where the check compared nothing, with
64
+ `pairs_compared` and `pairs_uncompared` saying how much of the pair it reached.
65
+ A `status` of `unresolved` is not a pass. It means the answer holds no evidence
66
+ either way, and `type_verdicts` says where the gap is.
67
+
68
+ Then read `type_verdicts` (`compatible`, `incompatible`, `unresolved`,
69
+ `not_compared`) and the `issues` rows for this operation. A `not_compared` pair
70
+ has no stored verdict, which is never agreement.
64
71
 
65
72
  ## 4. A file you have already edited
66
73