carrick 0.3.84 → 0.3.86

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 (47) hide show
  1. package/README.md +15 -0
  2. package/bin/carrick.mjs +89 -0
  3. package/dist/auth/credentials.d.ts +9 -0
  4. package/dist/auth/credentials.js +13 -2
  5. package/dist/auth/credentials.js.map +1 -1
  6. package/dist/auth/read.d.ts +4 -4
  7. package/dist/global-install.d.ts +183 -0
  8. package/dist/global-install.js +393 -0
  9. package/dist/global-install.js.map +1 -0
  10. package/dist/hook/post-edit.js +10 -0
  11. package/dist/hook/post-edit.js.map +1 -1
  12. package/dist/hook/session-start.js +34 -0
  13. package/dist/hook/session-start.js.map +1 -1
  14. package/dist/hook/stop.js +18 -4
  15. package/dist/hook/stop.js.map +1 -1
  16. package/dist/hook/user-prompt.js +16 -4
  17. package/dist/hook/user-prompt.js.map +1 -1
  18. package/dist/init/doctor.d.ts +40 -0
  19. package/dist/init/doctor.js +125 -0
  20. package/dist/init/doctor.js.map +1 -1
  21. package/dist/init/outdated.d.ts +47 -0
  22. package/dist/init/outdated.js +104 -0
  23. package/dist/init/outdated.js.map +1 -1
  24. package/dist/init/projects.d.ts +2 -2
  25. package/dist/init/run.d.ts +9 -0
  26. package/dist/init/run.js +57 -12
  27. package/dist/init/run.js.map +1 -1
  28. package/dist/scan.d.ts +26 -0
  29. package/dist/scan.js +92 -0
  30. package/dist/scan.js.map +1 -1
  31. package/dist/update-check.d.ts +1 -0
  32. package/dist/update-check.js +25 -0
  33. package/dist/update-check.js.map +1 -0
  34. package/dist/update.d.ts +128 -0
  35. package/dist/update.js +398 -0
  36. package/dist/update.js.map +1 -0
  37. package/package.json +7 -7
  38. package/sidecar/dist/src/capture/anchors.js +69 -8
  39. package/sidecar/dist/src/capture/api.d.ts +4 -1
  40. package/sidecar/dist/src/capture/deep-walk.js +4 -1
  41. package/sidecar/dist/src/capture/node-builder.d.ts +28 -1
  42. package/sidecar/dist/src/capture/node-builder.js +89 -3
  43. package/sidecar/dist/src/capture/unresolved.d.ts +7 -0
  44. package/sidecar/dist/src/capture/unresolved.js +1 -1
  45. package/sidecar/dist/src/type-inferrer.d.ts +128 -5
  46. package/sidecar/dist/src/type-inferrer.js +444 -17
  47. package/templates/skills/carrick-census.md +3 -1
@@ -6,9 +6,9 @@
6
6
  */
7
7
  import ts from 'typescript';
8
8
  import * as path from 'node:path';
9
- import { printTypeForDestination, undeclaredNamesIn } from './node-builder.js';
9
+ import { printTypeForDestination, substituteUndeclaredNames, undeclaredNamesIn, } from './node-builder.js';
10
10
  import { typeIsOrContainsMachinery } from './machinery.js';
11
- import { unresolvedAtAnchor } from './unresolved.js';
11
+ import { unresolvedAtAnchor, unresolvedSpecifiersReachableFrom, } from './unresolved.js';
12
12
  /** Repo-root-relative source file -> extensionless specifier from entryDir. */
13
13
  export function entryRelativeSpecifier(entryDir, repoRoot, sourceFile) {
14
14
  const target = path
@@ -54,12 +54,25 @@ export function resolveAnchor(program, request, args) {
54
54
  // (a generated model that was never generated). The stub then self-checks
55
55
  // such a name as an error placeholder, which no walk flags. A name a
56
56
  // sibling symbol anchor imports is resolved by that import.
57
+ // carrick#1377: rewrite what nothing declares to `unknown` in place, so
58
+ // one member typed by a module the checkout does not have stops taking
59
+ // every member around it down with it.
60
+ const rewritten = siblingSpec || !args.placeholder
61
+ ? undefined
62
+ : substituteUndeclaredNamesInText(text, program, args.placeholder);
63
+ const aliasBody = rewritten?.text ?? text;
57
64
  const undeclaredNames = siblingSpec || !args.placeholder
58
65
  ? []
59
- : undeclaredNamesInText(text, program, args.placeholder);
66
+ : undeclaredNamesInText(aliasBody, program, args.placeholder);
67
+ const unresolved = rewritten?.paths.length
68
+ ? {
69
+ paths: rewritten.paths,
70
+ specifiers: unresolvedSpecifiersForLiteral(program, request.source_file, args.repoRoot),
71
+ }
72
+ : undefined;
60
73
  return {
61
74
  request,
62
- aliasText: siblingSpec ? `import('${siblingSpec}').${text}` : text,
75
+ aliasText: siblingSpec ? `import('${siblingSpec}').${text}` : aliasBody,
63
76
  // Literal anchors ARE the legacy-text tier (WP3 wiring of the design's
64
77
  // structural_fallback): hand-produced type text riding the surface.
65
78
  // The self-check still classifies decay; the fidelity metric counts
@@ -67,6 +80,7 @@ export function resolveAnchor(program, request, args) {
67
80
  // ratchetable. Demotions are distinguished by failureReason.
68
81
  serialization: 'structural_fallback',
69
82
  ...(undeclaredNames.length > 0 ? { undeclaredNames } : {}),
83
+ ...(unresolved ? { unresolved } : {}),
70
84
  };
71
85
  }
72
86
  const sourceAbs = path.join(args.repoRoot, request.source_file);
@@ -276,7 +290,12 @@ function finishInferAnchor(program, sourceFile, request, located, placeholder, r
276
290
  if (!printed.text) {
277
291
  return demote(printed.failure ?? 'node builder print failed');
278
292
  }
279
- const unresolved = unresolvedAtAnchor(program, sourceFile, type, located);
293
+ const atAnchor = unresolvedAtAnchor(program, sourceFile, type, located);
294
+ // carrick#1377: a member the print named undeclared and this rewrote to
295
+ // `unknown` is an unresolved position too, whether or not the source type
296
+ // carried the compiler's placeholder at it (a bare name the print reused as
297
+ // written does not). Both lists feed the same labelling.
298
+ const unresolved = mergeUnresolved(atAnchor, printed.substitutedPaths, () => unresolvedSpecifiersReachableFrom(program, sourceFile));
280
299
  return {
281
300
  request,
282
301
  aliasText: printed.text,
@@ -286,13 +305,55 @@ function finishInferAnchor(program, sourceFile, request, located, placeholder, r
286
305
  ...(unresolved ? { unresolved } : {}),
287
306
  };
288
307
  }
308
+ /**
309
+ * Fold substituted member positions into what the source program could not
310
+ * resolve. `specifiers` is a thunk: the reachable-import walk is only worth
311
+ * running for an anchor that actually substituted something.
312
+ */
313
+ function mergeUnresolved(atAnchor, substitutedPaths, specifiers) {
314
+ if (!substitutedPaths || substitutedPaths.length === 0)
315
+ return atAnchor;
316
+ const paths = new Set([...(atAnchor?.paths ?? []), ...substitutedPaths]);
317
+ return {
318
+ paths: [...paths],
319
+ specifiers: atAnchor?.specifiers ?? specifiers(),
320
+ };
321
+ }
322
+ /**
323
+ * The unresolved specifiers a LITERAL anchor's source file reaches, or none
324
+ * when the anchor names no file (its text names a type with nothing behind it
325
+ * and the detail says only that).
326
+ */
327
+ function unresolvedSpecifiersForLiteral(program, sourceFileRel, repoRoot) {
328
+ if (!sourceFileRel)
329
+ return [];
330
+ const sourceFile = program.getSourceFile(path.join(repoRoot, sourceFileRel));
331
+ return sourceFile ? unresolvedSpecifiersReachableFrom(program, sourceFile) : [];
332
+ }
289
333
  /** `undeclaredNamesIn` over type text rather than a built node. */
290
334
  function undeclaredNamesInText(text, program, destination) {
335
+ const parsed = parseLiteralAnchor(text);
336
+ return parsed ? undeclaredNamesIn(parsed, program, destination) : [];
337
+ }
338
+ /** `substituteUndeclaredNames` over type text rather than a built node. */
339
+ function substituteUndeclaredNamesInText(text, program, destination) {
340
+ const parsed = parseLiteralAnchor(text);
341
+ if (!parsed)
342
+ return undefined;
343
+ const rewritten = substituteUndeclaredNames(parsed, program, destination);
344
+ if (rewritten.substitutions.length === 0)
345
+ return undefined;
346
+ const printer = ts.createPrinter({ removeComments: true });
347
+ return {
348
+ text: printer.printNode(ts.EmitHint.Unspecified, rewritten.node, parsed.getSourceFile()),
349
+ paths: rewritten.substitutions.map((entry) => entry.path),
350
+ };
351
+ }
352
+ /** The type node of `type __LiteralAnchor = <text>;`, or undefined. */
353
+ function parseLiteralAnchor(text) {
291
354
  const parsed = ts.createSourceFile('literal-anchor.ts', `type __LiteralAnchor = ${text};`, ts.ScriptTarget.Latest, true);
292
355
  const statement = parsed.statements[0];
293
- if (!statement || !ts.isTypeAliasDeclaration(statement))
294
- return [];
295
- return undeclaredNamesIn(statement.type, program, destination);
356
+ return statement && ts.isTypeAliasDeclaration(statement) ? statement.type : undefined;
296
357
  }
297
358
  /**
298
359
  * True when the anchor carries a LINE and nothing else — no payload span, no
@@ -164,10 +164,13 @@ export type SelfCheckOutcome = 'ok' | 'allowlisted_external' | 'decayed_internal
164
164
  * - `no_request_body`: the located request read is a validated part the
165
165
  * route's validator binds that is not a body (a path parameter, a query), so
166
166
  * the route states no request body contract there (carrick#1166).
167
+ * - `projected_value_only`: every read of a call's result takes a member out
168
+ * of it and none reads the value itself, so the site states a part of a
169
+ * payload rather than the payload a caller receives (carrick#1375).
167
170
  * - `not_recorded`: the position carries a top type and this layer has no
168
171
  * cause for it.
169
172
  */
170
- export type TypeProvenanceReason = 'declared' | 'unresolved_import' | 'budget_exhausted' | 'no_payload_evidence' | 'machinery_envelope' | 'coerced_input' | 'no_success_payload' | 'no_request_body' | 'not_recorded';
173
+ export type TypeProvenanceReason = 'declared' | 'unresolved_import' | 'budget_exhausted' | 'no_payload_evidence' | 'machinery_envelope' | 'coerced_input' | 'no_success_payload' | 'no_request_body' | 'projected_value_only' | 'not_recorded';
171
174
  /**
172
175
  * One `any`/`unknown` finding inside a captured or inferred type, with its
173
176
  * position and its cause. Sorted by `path` wherever a list is emitted, so the
@@ -266,7 +266,10 @@ export function provenanceOf(finding, unresolved) {
266
266
  detail: 'the type is too deep or wide to verify within the capture budget here, so it is reported unverified rather than assumed clean',
267
267
  };
268
268
  }
269
- if (finding.kind === 'any' && unresolved?.paths.includes(finding.path)) {
269
+ // `unknown` reads here as well as `any` (carrick#1377): a reference nothing
270
+ // declares is rewritten to `unknown` at its own position, and a reader told
271
+ // the author declared it that way stops looking where the fix is.
272
+ if (unresolved?.paths.includes(finding.path)) {
270
273
  return {
271
274
  path: finding.path,
272
275
  kind: finding.kind,
@@ -30,9 +30,23 @@ export interface NodeBuilderPrintResult {
30
30
  failure?: string;
31
31
  /**
32
32
  * Names the print refers to that do not resolve at the destination in the
33
- * producer's program (carrick#1165). Present only when there are some.
33
+ * producer's program (carrick#1165). Present only when there are some —
34
+ * which, since carrick#1377 rewrites what it can reach, means a reference
35
+ * the substitution could not replace.
34
36
  */
35
37
  undeclaredNames?: string[];
38
+ /**
39
+ * Member positions the print named something undeclared at, and where that
40
+ * reference now reads `unknown` (carrick#1377). In the walk's own path
41
+ * notation, so a finding at one of them can be labelled as what it is: a
42
+ * module that did not resolve, not a declared top type.
43
+ */
44
+ substitutedPaths?: string[];
45
+ }
46
+ /** One reference replaced by `unknown`, with the member position it sat at. */
47
+ export interface UndeclaredSubstitution {
48
+ name: string;
49
+ path: string;
36
50
  }
37
51
  /**
38
52
  * Print `type` as a type node anchored at `destination` (a declaration inside
@@ -40,6 +54,19 @@ export interface NodeBuilderPrintResult {
40
54
  * any referenced symbol is not plainly accessible from the destination.
41
55
  */
42
56
  export declare function printTypeForDestination(program: ts.Program, type: ts.Type, destination: ts.Node): NodeBuilderPrintResult;
57
+ /**
58
+ * Rewrite every reference in `node` that nothing in the producer's program
59
+ * declares to the `unknown` keyword, and report the member position each sat
60
+ * at (carrick#1377).
61
+ *
62
+ * The positions use the same notation as the capture's deep walk — `sub`,
63
+ * `items<0>.meta`, `[index]`, `()` for a callable return — so a path found
64
+ * here names the same member a self-check finding at that path names.
65
+ */
66
+ export declare function substituteUndeclaredNames(node: ts.TypeNode, program: ts.Program, destination: ts.Node): {
67
+ node: ts.TypeNode;
68
+ substitutions: UndeclaredSubstitution[];
69
+ };
43
70
  /**
44
71
  * Bare names in a printed type node that nothing in the producer's program
45
72
  * declares (carrick#1165).
@@ -117,10 +117,96 @@ export function printTypeForDestination(program, type, destination) {
117
117
  failure: `symbols not accessible from the surface entry: ${[...new Set(inaccessible)].join(', ')}`,
118
118
  };
119
119
  }
120
+ // carrick#1377: a reference nothing declares makes the WHOLE answer
121
+ // unpublishable, so one member typed by a package the checkout does not
122
+ // have used to discard every member around it. Replace what it names with
123
+ // `unknown` in place and print that; the rest of the shape survives, and
124
+ // the positions are reported so the finding at each can be labelled as a
125
+ // module that did not resolve rather than a top type the author declared.
126
+ const substituted = substituteUndeclaredNames(node, program, destination);
120
127
  const printer = ts.createPrinter({ removeComments: true });
121
- const text = printer.printNode(ts.EmitHint.Unspecified, node, destination.getSourceFile());
122
- const undeclaredNames = undeclaredNamesIn(node, program, destination);
123
- return { text, inaccessible, ...(undeclaredNames.length > 0 ? { undeclaredNames } : {}) };
128
+ const text = printer.printNode(ts.EmitHint.Unspecified, substituted.node, destination.getSourceFile());
129
+ // Asked of the REWRITTEN node: the field says what the printed answer
130
+ // names, so anything the substitution could not reach still fills it and
131
+ // still refuses publication.
132
+ const undeclaredNames = undeclaredNamesIn(substituted.node, program, destination);
133
+ const substitutedPaths = substituted.substitutions.map((entry) => entry.path);
134
+ return {
135
+ text,
136
+ inaccessible,
137
+ ...(undeclaredNames.length > 0 ? { undeclaredNames } : {}),
138
+ ...(substitutedPaths.length > 0 ? { substitutedPaths } : {}),
139
+ };
140
+ }
141
+ /**
142
+ * Rewrite every reference in `node` that nothing in the producer's program
143
+ * declares to the `unknown` keyword, and report the member position each sat
144
+ * at (carrick#1377).
145
+ *
146
+ * The positions use the same notation as the capture's deep walk — `sub`,
147
+ * `items<0>.meta`, `[index]`, `()` for a callable return — so a path found
148
+ * here names the same member a self-check finding at that path names.
149
+ */
150
+ export function substituteUndeclaredNames(node, program, destination) {
151
+ const undeclared = new Set(undeclaredNamesIn(node, program, destination));
152
+ if (undeclared.size === 0) {
153
+ return { node, substitutions: [] };
154
+ }
155
+ const substitutions = [];
156
+ const leftmost = (name) => ts.isIdentifier(name) ? name : leftmost(name.left);
157
+ // The path is carried down the visit rather than reconstructed, because a
158
+ // rewritten node has no parent to walk back up from.
159
+ const rewrite = (current, path) => {
160
+ if ((ts.isTypeReferenceNode(current) && undeclared.has(leftmost(current.typeName).text)) ||
161
+ (ts.isTypeQueryNode(current) && undeclared.has(leftmost(current.exprName).text))) {
162
+ substitutions.push({
163
+ name: ts.isTypeReferenceNode(current)
164
+ ? leftmost(current.typeName).text
165
+ : leftmost(current.exprName).text,
166
+ path: path === '' ? '<root>' : path,
167
+ });
168
+ return ts.factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword);
169
+ }
170
+ return ts.visitEachChild(current, (child) => rewrite(child, childPath(current, child, path)),
171
+ /* context */ undefined);
172
+ };
173
+ const rewritten = rewrite(node, '');
174
+ return { node: rewritten, substitutions };
175
+ }
176
+ /** The deep walk's path for `child` inside `parent`, extending `path`. */
177
+ function childPath(parent, child, path) {
178
+ if (ts.isPropertySignature(parent) && parent.type === child) {
179
+ const name = ts.isIdentifier(parent.name) || ts.isStringLiteral(parent.name)
180
+ ? parent.name.text
181
+ : parent.name.getText?.() ?? '';
182
+ return path === '' ? name : `${path}.${name}`;
183
+ }
184
+ if (ts.isArrayTypeNode(parent) && parent.elementType === child) {
185
+ return `${path}<0>`;
186
+ }
187
+ if (ts.isIndexSignatureDeclaration(parent) && parent.type === child) {
188
+ return `${path}[index]`;
189
+ }
190
+ if ((ts.isFunctionTypeNode(parent) ||
191
+ ts.isMethodSignature(parent) ||
192
+ ts.isCallSignatureDeclaration(parent)) &&
193
+ parent.type === child) {
194
+ return `${path}()`;
195
+ }
196
+ if (ts.isTypeReferenceNode(parent) && parent.typeArguments) {
197
+ const index = parent.typeArguments.indexOf(child);
198
+ if (index >= 0)
199
+ return `${path}<${index}>`;
200
+ }
201
+ if (ts.isTupleTypeNode(parent)) {
202
+ const index = parent.elements.indexOf(child);
203
+ if (index >= 0)
204
+ return `${path}<${index}>`;
205
+ }
206
+ // A union or intersection member sits at its parent's position, as the deep
207
+ // walk records it; everything else (a type literal's members, a parenthesis)
208
+ // keeps the path it was reached with.
209
+ return path;
124
210
  }
125
211
  /**
126
212
  * Bare names in a printed type node that nothing in the producer's program
@@ -26,3 +26,10 @@ import { type UnresolvedAtAnchor } from './deep-walk.js';
26
26
  * depth prints `import('./m').Row[]`, whose members sit under `<0>`.
27
27
  */
28
28
  export declare function unresolvedAtAnchor(program: ts.Program, sourceFile: ts.SourceFile, type: ts.Type, location: ts.Node, pathPrefix?: string): UnresolvedAtAnchor | undefined;
29
+ /**
30
+ * Module specifiers, as written, that do not resolve from `sourceFile` or from
31
+ * any source module it imports, breadth-first so the nearest come first, with
32
+ * relative specifiers ahead of package names. Installed packages and the
33
+ * default library are not descended into.
34
+ */
35
+ export declare function unresolvedSpecifiersReachableFrom(program: ts.Program, sourceFile: ts.SourceFile): string[];
@@ -50,7 +50,7 @@ function prefixPath(prefix, path) {
50
50
  * relative specifiers ahead of package names. Installed packages and the
51
51
  * default library are not descended into.
52
52
  */
53
- function unresolvedSpecifiersReachableFrom(program, sourceFile) {
53
+ export function unresolvedSpecifiersReachableFrom(program, sourceFile) {
54
54
  let cache = reachableCache.get(program);
55
55
  if (!cache) {
56
56
  cache = new Map();
@@ -206,17 +206,131 @@ export declare class TypeInferrer {
206
206
  */
207
207
  private isResponseSend;
208
208
  /**
209
- * The send `node` is an argument of, looking through the wrappers that do
209
+ * The call `node` is an ARGUMENT of, looking through the wrappers that do
210
210
  * not change a payload (parentheses, `as`, `satisfies`, `!`, `await`) and a
211
- * `JSON.stringify` around the body. `undefined` when the parent call is not
212
- * a send or `node` is its callee.
211
+ * `JSON.stringify` around the body. `undefined` when `accept` rejects that
212
+ * call or `node` is its callee.
213
+ *
214
+ * Two readings use it: a value handed to a response send is the payload that
215
+ * send transmits, and a body read handed to a call that states what it
216
+ * returns is a better statement of that body than the read (carrick#1382).
213
217
  */
214
- private sendReceivingArgument;
218
+ private receivingCallOf;
215
219
  private inferCallResult;
216
220
  private inferVariable;
217
221
  private inferExpression;
218
222
  private inferRequestBody;
219
223
  private resolveCallResultTerminalNode;
224
+ /**
225
+ * True when a member read on the call's result resolves to one of that
226
+ * result's own TYPE ARGUMENTS — the source is unwrapping a generic envelope
227
+ * by hand (`state.data` off a `ResourceState<Envelope>`), and the payload it
228
+ * carries is the instantiation, not the envelope (carrick#1375).
229
+ *
230
+ * The generic is what tells the two apart. A call that answers its payload
231
+ * directly is read member by member too, and its declared result IS the
232
+ * contract; abstaining there would throw away the type the request boundary
233
+ * states, which a replay over a real repo's consumer rows showed on a
234
+ * `{ ok: true } | { ok: false; reason: string }` result read as `sent.ok`.
235
+ */
236
+ private projectionReadsGenericPayload;
237
+ /**
238
+ * The payload a RESULT CARRIER carries, or `undefined` when `type` is not
239
+ * one or its success side cannot be told from its failure side
240
+ * (carrick#1376).
241
+ *
242
+ * A carrier is recognised by its shape, never by a name: a union of object
243
+ * branches, instantiated with two or more type arguments, at least one of
244
+ * which a branch holds as a member. `Result<T, E>`, `Either<L, R>` and a
245
+ * hand-rolled `{ ok: true; value: T } | { ok: false; error: E }` are all the
246
+ * same shape, and a promise-like around one is peeled first through the
247
+ * language's own await protocol. A single generic object — a resource state,
248
+ * a query result — is NOT a union and is left to carrick#1375, which
249
+ * abstains on it so a sibling site can answer.
250
+ *
251
+ * Which argument is the payload is decided twice over, and never guessed:
252
+ *
253
+ * 1. the platform's error shape. Exactly one argument that is not
254
+ * error-shaped, beside at least one that is, is the success side.
255
+ * 2. what the source reads. Where every argument looks alike — `Pair<A,
256
+ * string>` — a member read of the carrier that resolves to exactly one
257
+ * of the arguments names the side this call site takes.
258
+ *
259
+ * Where neither decides, the carrier keeps its own answer and the limit is
260
+ * logged: a coin flip published as a contract is worse than an envelope a
261
+ * reader can see is an envelope.
262
+ */
263
+ private resultCarrierPayload;
264
+ /**
265
+ * `Future<T>` -> `T` for a promise-like of the source's own making, read off
266
+ * the await protocol rather than a name: a `then` whose first parameter is a
267
+ * callback, whose own first parameter is the value awaiting it yields.
268
+ * `Promise` and `PromiseLike` are peeled by `unwrapPromiseType` before this.
269
+ */
270
+ private unwrapThenableType;
271
+ /**
272
+ * The platform's error shape, in full: `name` and `message` strings AND a
273
+ * `stack`, which is what the `Error` interface declares and every subclass
274
+ * of it inherits.
275
+ *
276
+ * `stack` is what makes the test a test. A name and a message alone are a
277
+ * shape a PAYLOAD can have — a contact form declares both — and reading such
278
+ * a payload as the failure side would publish the other argument, which is
279
+ * the concrete-but-wrong answer this whole rule exists to avoid. A union is
280
+ * error-shaped when every member of it is.
281
+ */
282
+ private isErrorShaped;
283
+ /**
284
+ * The member read that takes `identifier` as its RECEIVER — `query` in
285
+ * `query.data`, `envelope` in `envelope.list[0]` — or `undefined` when the
286
+ * identifier names the value itself.
287
+ *
288
+ * A member CALL is not a projection: `res.text()` yields a body rather than
289
+ * a part of one, and what it returns stays the walk's business. The
290
+ * zero-argument json body read has its own branch and is taken before this
291
+ * is asked.
292
+ */
293
+ private projectionOnReceiver;
294
+ /**
295
+ * The call that CONSUMES this json body read and states what the body is —
296
+ * `parseEnvelope(await response.json())` — or `undefined` when nothing
297
+ * downstream of the read says more about it than the read itself does
298
+ * (carrick#1382).
299
+ *
300
+ * Three conditions, all shapes of the language rather than names:
301
+ *
302
+ * - the read reaches the call as an ARGUMENT, through the wrappers that do
303
+ * not change a value (`await`, parentheses, `as`, `satisfies`, `!`). A
304
+ * cast with no call around it therefore keeps the read as the terminal,
305
+ * so `(await res.json()) as Entry` is still read off the read itself;
306
+ * - the call's own result is BOUND — declared into a variable, returned, or
307
+ * assigned — so a call the source made for its side effect
308
+ * (`store(await res.json())`) states nothing about the payload;
309
+ * - that result is an OBJECT shape. A validator answering `boolean` or a
310
+ * serialiser answering `string` describes what the caller did with the
311
+ * body, not what the body is, and publishing it would be a
312
+ * concrete-but-wrong contract where the honest `any` of the read is
313
+ * merely unresolved.
314
+ */
315
+ private statedPayloadAroundBodyRead;
316
+ /**
317
+ * The source keeps this call's result: it initializes a declaration, is
318
+ * returned, is assigned, or is an arrow's expression body. A result that is
319
+ * kept is one the source has a use for; a discarded one is a side effect.
320
+ */
321
+ private callResultIsBound;
322
+ /**
323
+ * A shape a JSON body can be: an object, an array, or a union of them.
324
+ * Top types, primitives, `void` and callables are not.
325
+ */
326
+ private isObjectShape;
327
+ /**
328
+ * Every use of a tracked name inside `expr` reads a member out of the
329
+ * tracked value, so the expression's type describes a PART of the payload.
330
+ * False when the expression uses no tracked name at all, so a caller can
331
+ * read it as "this is a projection" rather than "this is not a use".
332
+ */
333
+ private usesNamesOnlyByProjection;
220
334
  private extractBindingFromCall;
221
335
  private extractBindingNames;
222
336
  private getPrimaryBindingNode;
@@ -594,7 +708,16 @@ export declare class TypeInferrer {
594
708
  * True when an object literal argument is response INIT rather than a body:
595
709
  * every property it declares is one the standard `ResponseInit` declares
596
710
  * (`status`, `statusText`, `headers`), and it states at least one of them as
597
- * init really does — a numeric status in the HTTP range, or headers.
711
+ * init really does — a status in the HTTP range, or headers.
712
+ *
713
+ * The status does NOT have to be a literal code. A route that carries its
714
+ * outcome in a value writes `new Response(body, { status: result.status })`,
715
+ * where the source fixes no code and `statedStatusCodes` answers
716
+ * `'variable'` — status-shaped, just not pinned. Requiring a literal there
717
+ * left the whole init object reading as a body, so an endpoint whose payload
718
+ * this layer does not publish (a string body) published `{ status: number }`
719
+ * as its response contract instead: a wrong contract, served, where an
720
+ * abstention was the honest answer.
598
721
  *
599
722
  * A payload that merely has a `status` member of its own (`{ status: "ok",
600
723
  * service: "ledger" }`) declares members init does not, or states `status` as