@suss/adapter-typescript 0.19.0 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -1,6 +1,7 @@
1
- import { SourceFile, Node, FunctionDeclaration, FunctionExpression, ArrowFunction, MethodDeclaration, CallExpression, Project, Symbol, Expression, NewExpression, TaggedTemplateExpression, PropertyAccessExpression } from 'ts-morph';
2
- import { PackDeclarations, TransparentWrapper, RawCondition, DiscoveryPattern, TerminalPattern, InputMappingPattern, LanguageAdapter, PatternPack, ExtractorOptions, InvocationRecognizer, AccessRecognizer, RawCodeStructure, RawTerminal, RawBranch, EffectArg, RawDeclaredContract, ContractPattern, AstCapableOps, CallOps, ResponsePropertyMapping } from '@suss/extractor';
1
+ import { SourceFile, Node, FunctionDeclaration, FunctionExpression, ArrowFunction, MethodDeclaration, CallExpression, PropertyAccessExpression, TaggedTemplateExpression, Project, ImportDeclaration, Symbol, Expression, NewExpression, Identifier } from 'ts-morph';
2
+ import { PackDeclarations, TransparentWrapper, RawCondition, DiscoveryPattern, TerminalPattern, InputMappingPattern, EffectArg, CallOps, LanguageAdapter, PatternPack, ExtractorOptions, InvocationRecognizer, AccessRecognizer, RawCodeStructure, RawTerminal, RawBranch, configuredCallOption, RawDeclaredContract, ContractPattern, AstCapableOps, ResponsePropertyMapping } from '@suss/extractor';
3
3
  import { BehavioralSummary, MessageBusSemantics, DeployableUnit, BoundaryBinding, Predicate, TypeShape, ValueRef } from '@suss/behavioral-ir';
4
+ import { z } from 'zod';
4
5
  import { Proof, Atom } from '@suss/datalog';
5
6
  import { ResolutionExplanation } from '@suss/resolution';
6
7
 
@@ -140,6 +141,14 @@ interface PackFunnel {
140
141
  * while this is non-empty.
141
142
  */
142
143
  failures: PackFailure[];
144
+ /**
145
+ * Registration helpers this pack's config asked for that no call in
146
+ * the run matched. The helper belongs to the project, so a spelling
147
+ * that matches nothing is a config mistake with no other symptom: the
148
+ * routes go missing and every count reads the same as a project
149
+ * without them.
150
+ */
151
+ helpersUnmatched: string[];
143
152
  /**
144
153
  * What the pack wrote as data rather than as code, or null for a pack
145
154
  * written as a hand-rolled walk. This is the one thing in the funnel
@@ -171,6 +180,13 @@ interface ExtractionReport {
171
180
  * nothing. Null when the run produced summaries.
172
181
  */
173
182
  emptyStage: EmptyStage | null;
183
+ /**
184
+ * Reassigned names the run stated nothing for, because control flow
185
+ * decides which write a reader sees. Each is a value resolution the
186
+ * facts decline; the count across a large corpus says whether scoped
187
+ * reaching definitions is worth writing.
188
+ */
189
+ reassignedNamesUnstated: number;
174
190
  }
175
191
  type EmptyStage = "tsconfig" | "gateResolution" | "candidateFiles" | "discovery" | "assembly";
176
192
  /**
@@ -238,6 +254,15 @@ declare class ResolutionStore {
238
254
  private readonly fullyExtracted;
239
255
  private readonly seededValues;
240
256
  private readonly importedNames;
257
+ private readonly writtenValues;
258
+ private readonly importOrigins;
259
+ private readonly subjectConstructions;
260
+ /**
261
+ * A file's table depends only on its re-export closure, which
262
+ * `collectExports` extracts whole before answering, so later
263
+ * extraction elsewhere cannot change an entry.
264
+ */
265
+ private readonly exportTables;
241
266
  /** Files the most recent wave walk entered, for the memo to keep. */
242
267
  private lastQueryWalked;
243
268
  private readonly declarations;
@@ -273,6 +298,36 @@ declare class ResolutionStore {
273
298
  * GraphQL document kept in a constant in another file.
274
299
  */
275
300
  resolveWrittenValue(value: Node): Node | null;
301
+ /**
302
+ * The construction behind a registration subject: the call or `new`
303
+ * this value is written as, kept only when its callee comes from
304
+ * `importModule` under `importName`. A value written as two
305
+ * different things comes back null, since keying a route on the
306
+ * wrong app is worse than not keying it.
307
+ */
308
+ subjectConstructionOf(value: Node, importModule: string, importName: string): Node | null;
309
+ /**
310
+ * How many constructions this value was written as, which is what
311
+ * `subjectConstructionOf` weighed before it declined. Ask it after
312
+ * that call, so the walk it needs has already been paid for.
313
+ */
314
+ subjectCandidateCountOf(value: Node, importModule: string, importName: string): number;
315
+ /**
316
+ * The single-answer policy over the written-value walk, then the
317
+ * origin check on the one candidate. A candidate failing the check
318
+ * is settled refusal rather than a reason to widen: its own file's
319
+ * import facts are already in, and more files only add candidates.
320
+ */
321
+ private lookupSubjectConstruction;
322
+ /**
323
+ * Whether the construction's callee was imported from this module
324
+ * under this name. A default import records its name as "default",
325
+ * and a pack declares a default export by the local name people
326
+ * give it, so "default" counts when the callee is spelled with the
327
+ * declared name.
328
+ */
329
+ private constructionComesFrom;
330
+ private calleeNameOf;
276
331
  /**
277
332
  * Which names from `modules` this value comes down to, either by being
278
333
  * one of them under a local name or by calling into one. Getting
@@ -280,6 +335,36 @@ declare class ResolutionStore {
280
335
  * decorators.
281
336
  */
282
337
  importedNamesOf(value: Node, modules: string[]): string[];
338
+ /**
339
+ * Which module export this value comes down to, with what made it
340
+ * included: a value written as `createClient()` is made from the
341
+ * module exporting `createClient`, and a member destructured off
342
+ * that result is one path segment further. The module half is the
343
+ * specifier as the source wrote it, so a subpath import keeps its
344
+ * subpath.
345
+ */
346
+ importOriginsOf(value: Node, modules: string[]): Array<{
347
+ module: string;
348
+ path: string[];
349
+ }>;
350
+ /**
351
+ * The batched form: one demand set, one derivation, one widening
352
+ * walk shared by every value. A discovery pass asks about every
353
+ * callee in a file, and per-value queries would re-pay demand
354
+ * clearing and re-derivation once per call site.
355
+ */
356
+ importOriginsOfMany(values: readonly Node[], modules: string[]): Map<Node, Array<{
357
+ module: string;
358
+ path: string[];
359
+ }>>;
360
+ /**
361
+ * The node a memo shares across references: a plain name's
362
+ * declaration, so a hundred call sites of one import pay one walk.
363
+ * Anything else keys as itself, since `a.foo` and `b.foo` share a
364
+ * declared `foo` without sharing a value.
365
+ */
366
+ private sharedDeclarationFor;
367
+ private lookupImportOrigins;
283
368
  /**
284
369
  * Null tells the walk to keep widening. A value that landed in a
285
370
  * package returns empty instead, because a library's own body is not
@@ -310,12 +395,39 @@ declare class ResolutionStore {
310
395
  * construction makes an instance of. The README says why.
311
396
  */
312
397
  private lookupWritten;
398
+ /**
399
+ * Read these files' facts now, before anything asks a question.
400
+ *
401
+ * A query widens along the imports of the file it starts in, which
402
+ * is where a value's definition is. What a caller passed is the other
403
+ * direction, so a question about a parameter is answered only by
404
+ * files the walk never reaches on its own.
405
+ */
406
+ extractFiles(sourceFiles: Iterable<SourceFile>): void;
313
407
  /**
314
408
  * Which of these files reach any of these packages, following
315
409
  * project-local re-export chains. A barrel package that re-exports an
316
410
  * SDK defeats a per-file import check and does not defeat this.
317
411
  */
318
412
  filesImportingTransitively(fileSets: ReadonlyArray<FileSetQuery>): ReadonlyArray<ReadonlySet<SourceFile>>;
413
+ /**
414
+ * The calls behind a receiver that `matches` accepts, walked out in
415
+ * waves the way every question is. The wave ends as soon as one file
416
+ * yields a match, and the caller applies the single-answer policy to
417
+ * the set; the resolution README's anchor section says why the rules
418
+ * cannot rank a nearer call above a farther one.
419
+ */
420
+ anchorCallsOf(value: Node, matches: (call: Node) => boolean): Node[];
421
+ /**
422
+ * Every name a module exports and the values behind each, with
423
+ * re-export chains flattened by the rules. The frontier follows
424
+ * re-export targets only, so a barrel of barrels extracts its own
425
+ * chain and nothing beside it, to any depth.
426
+ */
427
+ exportsOf(sourceFile: SourceFile): Map<string, Node[]>;
428
+ private collectExports;
429
+ /** Module keys this file re-exports from, read off its own facts. */
430
+ private reExportTargetsOf;
319
431
  /**
320
432
  * Null keeps the wave walk widening; any candidate at all ends it,
321
433
  * since more files can only add candidates, never take one away. The
@@ -342,6 +454,44 @@ type FunctionRoot = FunctionDeclaration | FunctionExpression | ArrowFunction | M
342
454
  */
343
455
  declare function collectAncestorBranches(terminalNode: Node, functionRoot: FunctionRoot): RawCondition[];
344
456
 
457
+ /**
458
+ * unfollowedCall.ts: naming the calls the reachable closure stops at.
459
+ *
460
+ * The closure follows a call by resolving its callee to a function with a
461
+ * body. When that fails the edge is dropped, and a unit whose body is full
462
+ * of dropped edges produces the same empty summary as a unit that does
463
+ * nothing. This module says which kind of stop a call site is, and which
464
+ * kinds are worth leaving a gap for.
465
+ *
466
+ * The README beside this file explains where that line is drawn, and why
467
+ * a call into a dependency stays on the other side of it.
468
+ */
469
+
470
+ /**
471
+ * Why the walk stopped.
472
+ *
473
+ * `noBody` states a shape and nothing else: a method on an interface, an
474
+ * abstract method, an ambient declaration. `unsettledValue` is declared
475
+ * as something other than a function, with something in it that could
476
+ * not be read. `multipleSources` reaches two different functions, so no
477
+ * single body can be followed. `outsideRun` is a declaration in a
478
+ * dependency, whose source this run never read. `noDeclaration` is a
479
+ * callee nothing declares, which is what a call on an untyped value
480
+ * comes to. `callerSupplied` is a parameter of the function being
481
+ * scanned, so the call runs whatever its caller handed in.
482
+ * `multipleReceivers` is a registration whose receiver comes down to
483
+ * more than one thing, so nothing says which one it registers on.
484
+ */
485
+ type UnfollowedReason = "noBody" | "unsettledValue" | "multipleSources" | "outsideRun" | "noDeclaration" | "callerSupplied" | "multipleReceivers";
486
+ /** One call the walk met and could not follow. */
487
+ interface UnfollowedCall {
488
+ /** The callee as the source writes it, `this.dao.getEditions` say. */
489
+ readonly callee: string;
490
+ readonly reason: UnfollowedReason;
491
+ /** How many things the walk reached where it needed one. */
492
+ readonly candidates?: number;
493
+ }
494
+
345
495
  interface ClientCallSite {
346
496
  callExpression: CallExpression;
347
497
  /** Method name on the client object (e.g. "getUser"), null for bare calls like fetch() */
@@ -464,6 +614,11 @@ interface DiscoveredUnit {
464
614
  packageName: string;
465
615
  exportPath: string[];
466
616
  };
617
+ /** A callable identity the pack stated: module and export name. */
618
+ functionCallInfo?: {
619
+ module: string;
620
+ exportName: string;
621
+ };
467
622
  /**
468
623
  * Populated by `decoratedRoute` discovery (NestJS-style REST
469
624
  * controllers). The adapter uses it to build a `rest` binding
@@ -488,6 +643,19 @@ interface DiscoveredUnit {
488
643
  /** Null when the pack knows the wire but not the channel on it. */
489
644
  channel: string | null;
490
645
  };
646
+ /**
647
+ * Populated by a pack's `discoverUnits` callback for a deployed unit
648
+ * nothing else routes to, which is reached by being invoked by name.
649
+ * The adapter builds a `unit-invocation` binding from it, which pairs
650
+ * with the calls that name the same unit.
651
+ */
652
+ invocationInfo?: DeployableUnit;
653
+ /**
654
+ * Identity of the routable this unit was registered on, when a
655
+ * registration call put it there. What the wrapper index is keyed on,
656
+ * since a wrapper is registered on the routable and not on the unit.
657
+ */
658
+ registrationSubjectId?: string;
491
659
  /** The thing that gets deployed and runs this unit, when known. */
492
660
  deployableUnit?: DeployableUnit;
493
661
  /**
@@ -505,6 +673,12 @@ interface DiscoveredUnit {
505
673
  * summary as an `unreadOutcome` gap.
506
674
  */
507
675
  unreadBinding?: string;
676
+ /**
677
+ * A call the handler met and could not follow, which reaches the
678
+ * summary as an `unfollowedCall` gap beside the ones the closure
679
+ * walk records.
680
+ */
681
+ unfollowed?: UnfollowedCall;
508
682
  }
509
683
 
510
684
  /**
@@ -529,12 +703,19 @@ interface MountPrefixIndex {
529
703
  */
530
704
  prefixForId?(childId: string): string;
531
705
  }
706
+ /**
707
+ * What the project helper index says it has already expanded, which the
708
+ * walk consults so one registration does not become two summaries.
709
+ */
710
+ interface ExpandedRegistrations {
711
+ expandedAtCallSites(helper: Node, method: string): boolean;
712
+ }
532
713
 
533
714
  /**
534
715
  * Discover code units in `sourceFile` by running all patterns.
535
716
  * Deduplicates entries with the same function node and kind.
536
717
  */
537
- declare function discoverUnits(sourceFile: SourceFile, patterns: DiscoveryPattern[], resolution?: ResolutionStore, mountPrefixes?: MountPrefixIndex): DiscoveredUnit[];
718
+ declare function discoverUnits(sourceFile: SourceFile, patterns: DiscoveryPattern[], resolution?: ResolutionStore, mountPrefixes?: MountPrefixIndex, expandedElsewhere?: ExpandedRegistrations): DiscoveredUnit[];
538
719
 
539
720
  interface TimingReport {
540
721
  totalMs: number;
@@ -560,6 +741,127 @@ interface Timer {
560
741
  */
561
742
  type DescentBarriers = ReadonlySet<Node>;
562
743
 
744
+ /**
745
+ * Context handed to TypeScript-adapter access recognizers: sister
746
+ * to TsInvocationRecognizerContext but for property-access nodes.
747
+ * No `extractArgs` since property accesses don't take arguments.
748
+ */
749
+ interface TsAccessRecognizerContext {
750
+ /** The property-access expression itself. */
751
+ access: Node;
752
+ /** Source file the access lives in. */
753
+ sourceFile: SourceFile;
754
+ /**
755
+ * One-hop lookup from a written name to the value it was bound to,
756
+ * from the run's facts (#300). Gives null when the run has no store,
757
+ * and the recognizer's own pattern match runs on the raw node.
758
+ */
759
+ resolveWrittenValue: (value: Node) => Node | null;
760
+ /**
761
+ * What a declared pack asks about this node, when the node is a call
762
+ * or a tagged template. This walk visits property accesses as well,
763
+ * and there is no call there to ask about, so a declared pack
764
+ * standing on one is handed nothing and matches nothing.
765
+ */
766
+ ops?: CallOps;
767
+ }
768
+ /**
769
+ * Context handed to TypeScript-adapter recognizers. Recognizers in
770
+ * `@suss/framework-prisma` (and other packs) receive this and use
771
+ * the type-checker / source-file primitives to decide whether the
772
+ * call site matches their semantics. `extractArgs` reuses the same
773
+ * `EffectArg` builder the adapter uses for `invocation` effects, so
774
+ * recognizers don't have to re-implement literal/object/identifier
775
+ * shape extraction.
776
+ */
777
+ interface TsInvocationRecognizerContext {
778
+ /** The call expression itself (also passed as the first arg). */
779
+ call: CallExpression;
780
+ /** Source file the call lives in. Useful for import resolution. */
781
+ sourceFile: SourceFile;
782
+ /**
783
+ * Convert the call's arguments to `EffectArg[]` using the same
784
+ * literal / object / identifier extraction the adapter applies to
785
+ * `invocation` effects. Recognizers call this when they want
786
+ * field-level shape (e.g. Prisma's `select: { id: true }`).
787
+ */
788
+ extractArgs(): EffectArg[];
789
+ /**
790
+ * Whether `identifier` was introduced by an import of
791
+ * `expectedModule`. Recognizers use this to tell a library's export
792
+ * apart from a local class that happens to share its name.
793
+ *
794
+ * Named, default, and namespace imports all match. So does an import
795
+ * through a project-local barrel that re-exports the module: the
796
+ * aliased symbol is still in the package that declared it.
797
+ */
798
+ isImportedFrom(identifier: Node, expectedModule: string): boolean;
799
+ /**
800
+ * The expression `value` is written as, followed through const
801
+ * bindings, imports, and re-export barrels. A recognizer asks this
802
+ * before claiming the source does not name something:
803
+ *
804
+ * const url = "https://sqs/.../orders";
805
+ * client.send(new SendMessageCommand({ QueueUrl: url }));
806
+ *
807
+ * Resolving `url` gives back the string literal, and the recognizer's
808
+ * own pattern match runs on that. Null when the value has no written
809
+ * form (a parameter, a call result), which is exactly when null on the
810
+ * binding is correct.
811
+ */
812
+ resolveWrittenValue(value: Node): Node | null;
813
+ /**
814
+ * What a declared pack asks about this call, in the vocabulary
815
+ * `@suss/extractor` defines. A pack written as a chain of data links
816
+ * reads only this; the members above are what a pack written as code
817
+ * reaches for.
818
+ */
819
+ ops: CallOps;
820
+ }
821
+ /**
822
+ * Everything a recognizer may ask about one call site.
823
+ *
824
+ * A pack's tests build it here too, so a pack that passes them has run
825
+ * against the context extraction gives it.
826
+ */
827
+ declare function invocationContextFor(call: CallExpression, resolveWrittenValue?: (value: Node) => Node | null, originatesFrom?: OriginatesFrom, anchorCallsOf?: AnchorCallsOf): TsInvocationRecognizerContext;
828
+ /**
829
+ * Whether a value's chain reaches the module, asked of the store. The
830
+ * adapter binds it; a context built bare falls back to the checker's
831
+ * alias resolution.
832
+ */
833
+ type OriginatesFrom = (value: Node, module: string) => boolean;
834
+ /** The calls behind a value that a caller's own check accepts, from the store. */
835
+ type AnchorCallsOf = (value: Node, matches: (call: Node) => boolean) => Node[];
836
+ declare function isImportedFrom(identifier: Node, expectedModule: string, originatesFrom?: OriginatesFrom): boolean;
837
+ /**
838
+ * Whether `expectedModule` declares the method a call goes to. A pack
839
+ * asks this when the receiver came from somewhere the source does not
840
+ * spell out: `const redis = await this.getClient()` says nothing about
841
+ * ioredis, and the type of `redis.get` says everything.
842
+ *
843
+ * A client cached on an untyped global, `global.x || new Redis()`,
844
+ * types as `any`, so the method has no symbol. With `resolve` supplied,
845
+ * what the receiver was written as still says which library: the value
846
+ * comes down to a construction or a factory call of something imported
847
+ * from it.
848
+ */
849
+ declare function methodDeclaredIn(callee: Node, expectedModule: string, resolve?: (value: Node) => Node | null, originatesFrom?: OriginatesFrom): boolean;
850
+ /** The nodes this walk stops at, which is every shape a pack can guard. */
851
+ type Accessed = PropertyAccessExpression | CallExpression | TaggedTemplateExpression;
852
+ /**
853
+ * What one node is handed. The ops are built on the first read rather
854
+ * than up front, the way the invocation walk builds them: most nodes
855
+ * reach no declared pack, and a pack written as code never asks.
856
+ *
857
+ * A pack's tests build it here too, so a pack that passes them has run
858
+ * against the context extraction gives it.
859
+ */
860
+ declare function accessContextFor(node: Accessed, sourceFile: SourceFile, resolveWrittenValue?: (value: Node) => Node | null, originatesFrom?: OriginatesFrom, anchorCallsOf?: AnchorCallsOf): TsAccessRecognizerContext;
861
+
862
+ /** Resolves a helper callee to its declaration; the adapter binds the store's ask. */
863
+ type ResolveCallee = (callee: Node) => Node | null;
864
+
563
865
  /**
564
866
  * The whole extraction, from a tsconfig to a set of behavioral
565
867
  * summaries. `createTypeScriptAdapter` is what a caller uses;
@@ -575,7 +877,7 @@ type DescentBarriers = ReadonlySet<Node>;
575
877
  * framework or a runtime belongs to a pack.
576
878
  */
577
879
 
578
- declare function extractCodeStructure(unit: DiscoveredUnit, pack: PatternPack, invocationRecognizers?: InvocationRecognizer[], accessRecognizers?: AccessRecognizer[], barriers?: DescentBarriers, resolveWrittenValue?: (value: Node) => Node | null): RawCodeStructure;
880
+ declare function extractCodeStructure(unit: DiscoveredUnit, pack: PatternPack, invocationRecognizers?: InvocationRecognizer[], accessRecognizers?: AccessRecognizer[], barriers?: DescentBarriers, resolveWrittenValue?: (value: Node) => Node | null, originatesFrom?: OriginatesFrom, anchorCallsOf?: AnchorCallsOf, resolveCallee?: ResolveCallee): RawCodeStructure;
579
881
  interface TypeScriptAdapterConfig {
580
882
  tsConfigFilePath?: string;
581
883
  /**
@@ -639,7 +941,7 @@ interface FoundTerminal {
639
941
  * discovered. Inside a nested function only escaping terminals are
640
942
  * matched: see `NESTED_ESCAPING_MATCH_TYPES`.
641
943
  */
642
- declare function findTerminals(func: FunctionRoot, patterns: TerminalPattern[], barriers?: DescentBarriers, resolveWrittenValue?: (value: Node) => Node | null): FoundTerminal[];
944
+ declare function findTerminals(func: FunctionRoot, patterns: TerminalPattern[], barriers?: DescentBarriers, resolveWrittenValue?: (value: Node) => Node | null, originatesFrom?: OriginatesFrom, resolveCallee?: ResolveCallee): FoundTerminal[];
643
945
 
644
946
  /**
645
947
  * Turns one function into the list of branches a summary is built from.
@@ -668,7 +970,7 @@ interface RawBranchResult {
668
970
  * or when every condition on it came from an early return or throw,
669
971
  * since those are the paths that run when no guard fired.
670
972
  */
671
- declare function extractRawBranches(func: FunctionRoot, terminalPatterns: TerminalPattern[], invocationRecognizers?: InvocationRecognizer[], accessRecognizers?: AccessRecognizer[], barriers?: DescentBarriers, resolveWrittenValue?: (value: Node) => Node | null): RawBranchResult;
973
+ declare function extractRawBranches(func: FunctionRoot, terminalPatterns: TerminalPattern[], invocationRecognizers?: InvocationRecognizer[], accessRecognizers?: AccessRecognizer[], barriers?: DescentBarriers, resolveWrittenValue?: (value: Node) => Node | null, originatesFrom?: OriginatesFrom, anchorCallsOf?: AnchorCallsOf, resolveCallee?: ResolveCallee): RawBranchResult;
672
974
 
673
975
  /**
674
976
  * A Project covering `rootDir`, for a codebase with no tsconfig to read.
@@ -698,28 +1000,12 @@ declare function findNearestTsconfig(startDir: string): string | null;
698
1000
  */
699
1001
 
700
1002
  /**
701
- * A call the project has declared to be its own dispatcher.
702
- *
703
- * `receiver` is the type name rather than the variable: a service keeps
704
- * its dispatcher in a field, a closure or a constructor parameter, and
705
- * the type is the only thing stable across all three.
1003
+ * A call the project has declared to be its own dispatcher. The
1004
+ * declaration a project config is parsed against lives with the other
1005
+ * shared pack options, so this reader and the packs that take it under
1006
+ * `producers` describe one shape.
706
1007
  */
707
- interface ConfiguredCallSpec {
708
- /** Module that declares the receiver's type. */
709
- module: string;
710
- /** Type name of the receiver, as exported from that module. */
711
- receiver: string;
712
- /** Method that performs the send. */
713
- method: string;
714
- /** Argument index carrying the subject. */
715
- subjectArg: number;
716
- /**
717
- * Which argument the message body is. Left out when the method has no
718
- * single body argument, as a batch method taking a list of entries
719
- * does, and then no body is reported.
720
- */
721
- bodyArg?: number;
722
- }
1008
+ type ConfiguredCallSpec = z.infer<typeof configuredCallOption>;
723
1009
  interface ConfiguredCallRead {
724
1010
  /** The subject the call sends to, always a literal string. */
725
1011
  subject: string;
@@ -763,7 +1049,7 @@ interface ContractReadResult {
763
1049
  * Returns null if the handler wasn't registered with a contract or the contract
764
1050
  * can't be resolved.
765
1051
  */
766
- declare function readContract(unit: DiscoveredUnit, pattern: ContractPattern, framework: string): ContractReadResult | null;
1052
+ declare function readContract(unit: DiscoveredUnit, pattern: ContractPattern, framework: string, resolution?: ResolutionStore): ContractReadResult | null;
767
1053
  /**
768
1054
  * Resolve the contract for a consumer call site.
769
1055
  *
@@ -771,7 +1057,90 @@ declare function readContract(unit: DiscoveredUnit, pattern: ContractPattern, fr
771
1057
  * call to find the contract, then extracts the endpoint definition for the
772
1058
  * matched method name (e.g. "getUser").
773
1059
  */
774
- declare function readContractForClientCall(callExpression: Node, methodName: string, pattern: ContractPattern, framework: string): ContractReadResult | null;
1060
+ declare function readContractForClientCall(callExpression: Node, methodName: string, pattern: ContractPattern, framework: string, resolution?: ResolutionStore): ContractReadResult | null;
1061
+
1062
+ /**
1063
+ * helperIndex.ts: reading the project's own helpers once, before any
1064
+ * file is walked.
1065
+ *
1066
+ * A pack that declares `projectHelpers` gets every helper the project
1067
+ * wrote in front of its library, each read in terms of its own
1068
+ * parameters, and gives back the patterns and recognizers it would
1069
+ * otherwise have taken as config. Those join the pack for the rest of
1070
+ * the run, so the match happens at the call site, where the arguments
1071
+ * are literal.
1072
+ *
1073
+ * Built alongside the mount and wrapper indexes, over the same files.
1074
+ * The discovery README has the ordering and the cost.
1075
+ */
1076
+
1077
+ interface ProjectHelperIndex {
1078
+ /** Discovery patterns `packName` contributed after reading the project. */
1079
+ patternsFor(packName: string): readonly DiscoveryPattern[];
1080
+ /**
1081
+ * Recognizers the packs contributed. They match calls to functions
1082
+ * this project declares, so they run wherever a call can appear
1083
+ * rather than only where the pack's own import gate reaches.
1084
+ */
1085
+ contributedRecognizers(): readonly InvocationRecognizer[];
1086
+ /**
1087
+ * The helper files that shaped what `file` extracts to, so the
1088
+ * per-file cache invalidates `file` when one of them is edited.
1089
+ */
1090
+ helperFilesFor(file: SourceFile): readonly string[];
1091
+ /**
1092
+ * Whether a registration written inside `helper` with `method` is one
1093
+ * the call sites already expand. Reading it here as well would give
1094
+ * one registration two summaries, and which of the two a run kept
1095
+ * would come down to the order the files were walked in.
1096
+ */
1097
+ expandedAtCallSites(helper: Node, method: string): boolean;
1098
+ }
1099
+ declare function buildProjectHelperIndex(sourceFiles: ReadonlyArray<SourceFile>, packs: ReadonlyArray<PatternPack>, packsByFile: ReadonlyMap<SourceFile, readonly PatternPack[]>, resolution: ResolutionStore): ProjectHelperIndex;
1100
+
1101
+ /**
1102
+ * The one import scan.
1103
+ *
1104
+ * Fifteen copies of "which names did this file import from module X"
1105
+ * grew across discovery handlers and packs, and two of them disagreed
1106
+ * about aliases (#674). Every question about a file's imports of a
1107
+ * module is asked here: which declarations name it, what the named
1108
+ * imports are called locally, and which local spellings the default
1109
+ * and namespace imports go by. Matching semantics are explicit
1110
+ * options, so a caller that accepts subpath specifiers or resolves
1111
+ * path-shaped specifiers through the store says so, and one that
1112
+ * matches the specifier verbatim stays verbatim.
1113
+ */
1114
+
1115
+ interface ImportScanOptions {
1116
+ /**
1117
+ * Accept `module/subpath` specifiers beside the exact module, the
1118
+ * way `react-dom/client` is still react-dom's surface.
1119
+ */
1120
+ subpaths?: boolean;
1121
+ /** Resolve path-shaped specifiers through the store's module graph. */
1122
+ resolution?: ResolutionStore;
1123
+ }
1124
+ /** One named import: how the file spells it, and what it is. */
1125
+ interface NamedImport {
1126
+ /** The spelling this file binds, the alias when there is one. */
1127
+ local: string;
1128
+ /** The name the module exports it as. */
1129
+ canonical: string;
1130
+ /** The specifier as written, for a caller that reads sub-paths. */
1131
+ specifier: string;
1132
+ }
1133
+ /** Every named import of these modules, aliases resolved. */
1134
+ declare function namedImportsOf(sourceFile: SourceFile, modules: readonly string[], options?: ImportScanOptions): NamedImport[];
1135
+ /**
1136
+ * Local spelling to canonical exported name, for every named import
1137
+ * of these modules. `import { Get as HttpGet }` yields HttpGet -> Get.
1138
+ */
1139
+ declare function importedNamesOf(sourceFile: SourceFile, modules: readonly string[], options?: ImportScanOptions): Map<string, string>;
1140
+ /** The local spellings of default and namespace imports of these modules. */
1141
+ declare function importedRootsOf(sourceFile: SourceFile, modules: readonly string[], options?: ImportScanOptions): Set<string>;
1142
+ /** Every import declaration in the file that names one of `modules`. */
1143
+ declare function importDeclarationsOf(sourceFile: SourceFile, modules: readonly string[], options?: ImportScanOptions): ImportDeclaration[];
775
1144
 
776
1145
  interface TsDiscoveryContext {
777
1146
  /** Full filesystem path of the source file. Useful for excluding
@@ -794,32 +1163,6 @@ interface TsDiscoveryContext {
794
1163
  func: FunctionRoot;
795
1164
  isDefault: boolean;
796
1165
  }>;
797
- /**
798
- * For an export the project builds by calling a factory, the string
799
- * under a property of that call's config object:
800
- * `export const handler = makeWidgetHandler({ subject: "a.b" }, ...)`
801
- * with `{ property: "subject" }` gives back `"a.b"`.
802
- *
803
- * The caller does not have to say which function was called or which
804
- * argument the config was in. Every object argument is read. Giving the
805
- * callee or fixing the argument position narrows that down, for a
806
- * project whose factories would otherwise collide.
807
- *
808
- * Two arguments with the property set to different values give back
809
- * null, since nothing says which one was meant.
810
- *
811
- * The config argument is usually an object literal at the call site;
812
- * a variable or import is followed to the literal it resolves to.
813
- * `as const` and parentheses around the property value are peeled.
814
- * Anything but a string literal underneath (a computed subject, a
815
- * template, a call) gives back null, and the caller attaches nothing
816
- * rather than guessing.
817
- */
818
- exportedCallConfigString(sourceFile: SourceFile, exportName: string, spec: {
819
- callees?: string[];
820
- argIndex?: number;
821
- property: string;
822
- }): string | null;
823
1166
  /**
824
1167
  * Walk a function's body for return statements whose value is a
825
1168
  * JSX element / fragment / self-closing tag. Returns true on the
@@ -899,14 +1242,11 @@ declare class WhySession {
899
1242
  private displayPath;
900
1243
  }
901
1244
 
902
- type ExportedDeclarationMap = ReturnType<SourceFile["getExportedDeclarations"]>;
903
1245
  /**
904
- * What a module exports, by exported name. Cached, because ts-morph
905
- * rebuilds the map from scratch on every `getExportedDeclarations` call
906
- * and callee resolution asks the same file the same thing once per
907
- * import site.
1246
+ * What a module exports, by exported name. The store's rules flatten
1247
+ * re-export chains of any length, and the store memoizes per file.
908
1248
  */
909
- declare function exportedDeclarationsOf(sourceFile: SourceFile): ExportedDeclarationMap;
1249
+ declare function exportedDeclarationsOf(sourceFile: SourceFile, resolution: ResolutionStore): Map<string, Node[]>;
910
1250
  /**
911
1251
  * `Symbol.getAliasedSymbol`, with the re-export chains warmed first so
912
1252
  * that an import sitting at the top of a deep chain still resolves
@@ -1031,117 +1371,31 @@ type Called = CallExpression | NewExpression | TaggedTemplateExpression;
1031
1371
  /** One-hop lookup from a written name to the value it was bound to. */
1032
1372
  type Resolve = (value: Node) => Node | null;
1033
1373
  /** What a declared pack can ask about one TypeScript call. */
1034
- declare function callOpsFor(call: Called, resolveWrittenValue?: Resolve): AstCapableOps;
1374
+ declare function callOpsFor(call: Called, resolveWrittenValue?: Resolve, originatesFrom?: OriginatesFrom, anchorCallsOf?: AnchorCallsOf): AstCapableOps;
1035
1375
 
1036
1376
  /**
1037
- * Context handed to TypeScript-adapter access recognizers: sister
1038
- * to TsInvocationRecognizerContext but for property-access nodes.
1039
- * No `extractArgs` since property accesses don't take arguments.
1040
- */
1041
- interface TsAccessRecognizerContext {
1042
- /** The property-access expression itself. */
1043
- access: Node;
1044
- /** Source file the access lives in. */
1045
- sourceFile: SourceFile;
1046
- /**
1047
- * One-hop lookup from a written name to the value it was bound to,
1048
- * from the run's facts (#300). Gives null when the run has no store,
1049
- * and the recognizer's own pattern match runs on the raw node.
1050
- */
1051
- resolveWrittenValue: (value: Node) => Node | null;
1052
- /**
1053
- * What a declared pack asks about this node, when the node is a call
1054
- * or a tagged template. This walk visits property accesses as well,
1055
- * and there is no call there to ask about, so a declared pack
1056
- * standing on one is handed nothing and matches nothing.
1057
- */
1058
- ops?: CallOps;
1059
- }
1060
- /**
1061
- * Context handed to TypeScript-adapter recognizers. Recognizers in
1062
- * `@suss/framework-prisma` (and other packs) receive this and use
1063
- * the type-checker / source-file primitives to decide whether the
1064
- * call site matches their semantics. `extractArgs` reuses the same
1065
- * `EffectArg` builder the adapter uses for `invocation` effects, so
1066
- * recognizers don't have to re-implement literal/object/identifier
1067
- * shape extraction.
1068
- */
1069
- interface TsInvocationRecognizerContext {
1070
- /** The call expression itself (also passed as the first arg). */
1071
- call: CallExpression;
1072
- /** Source file the call lives in. Useful for import resolution. */
1073
- sourceFile: SourceFile;
1074
- /**
1075
- * Convert the call's arguments to `EffectArg[]` using the same
1076
- * literal / object / identifier extraction the adapter applies to
1077
- * `invocation` effects. Recognizers call this when they want
1078
- * field-level shape (e.g. Prisma's `select: { id: true }`).
1079
- */
1080
- extractArgs(): EffectArg[];
1081
- /**
1082
- * Whether `identifier` was introduced by an import of
1083
- * `expectedModule`. Recognizers use this to tell a library's export
1084
- * apart from a local class that happens to share its name.
1085
- *
1086
- * Named, default, and namespace imports all match. So does an import
1087
- * through a project-local barrel that re-exports the module: the
1088
- * aliased symbol is still in the package that declared it.
1089
- */
1090
- isImportedFrom(identifier: Node, expectedModule: string): boolean;
1091
- /**
1092
- * The expression `value` is written as, followed through const
1093
- * bindings, imports, and re-export barrels. A recognizer asks this
1094
- * before claiming the source does not name something:
1095
- *
1096
- * const url = "https://sqs/.../orders";
1097
- * client.send(new SendMessageCommand({ QueueUrl: url }));
1098
- *
1099
- * Resolving `url` gives back the string literal, and the recognizer's
1100
- * own pattern match runs on that. Null when the value has no written
1101
- * form (a parameter, a call result), which is exactly when null on the
1102
- * binding is correct.
1103
- */
1104
- resolveWrittenValue(value: Node): Node | null;
1105
- /**
1106
- * What a declared pack asks about this call, in the vocabulary
1107
- * `@suss/extractor` defines. A pack written as a chain of data links
1108
- * reads only this; the members above are what a pack written as code
1109
- * reaches for.
1110
- */
1111
- ops: CallOps;
1112
- }
1113
- /**
1114
- * Everything a recognizer may ask about one call site.
1377
+ * The one resolver from a reference to the function behind it.
1115
1378
  *
1116
- * A pack's tests build it here too, so a pack that passes them has run
1117
- * against the context extraction gives it.
1379
+ * Four copies of "follow an identifier to its function declaration"
1380
+ * grew across the closure, the render-tree builder, the react pack,
1381
+ * and the parameter-read walk, three of them lossy (#674). The walk
1382
+ * lives here: alias-following through imports, shorthand properties
1383
+ * resolved to the binding they read, overload signatures routed to
1384
+ * the declaration with the body, and external code refused. Callers
1385
+ * that need only the file and name take `functionTargetOf`; the
1386
+ * closure takes `resolveDecl` for the function root itself.
1118
1387
  */
1119
- declare function invocationContextFor(call: CallExpression, resolveWrittenValue?: (value: Node) => Node | null): TsInvocationRecognizerContext;
1120
- declare function isImportedFrom(identifier: Node, expectedModule: string): boolean;
1121
- /**
1122
- * Whether `expectedModule` declares the method a call goes to. A pack
1123
- * asks this when the receiver came from somewhere the source does not
1124
- * spell out: `const redis = await this.getClient()` says nothing about
1125
- * ioredis, and the type of `redis.get` says everything.
1126
- *
1127
- * A client cached on an untyped global, `global.x || new Redis()`,
1128
- * types as `any`, so the method has no symbol. With `resolve` supplied,
1129
- * what the receiver was written as still says which library: the value
1130
- * comes down to a construction or a factory call of something imported
1131
- * from it.
1132
- */
1133
- declare function methodDeclaredIn(callee: Node, expectedModule: string, resolve?: (value: Node) => Node | null): boolean;
1134
- /** The nodes this walk stops at, which is every shape a pack can guard. */
1135
- type Accessed = PropertyAccessExpression | CallExpression | TaggedTemplateExpression;
1388
+
1136
1389
  /**
1137
- * What one node is handed. The ops are built on the first read rather
1138
- * than up front, the way the invocation walk builds them: most nodes
1139
- * reach no declared pack, and a pack written as code never asks.
1140
- *
1141
- * A pack's tests build it here too, so a pack that passes them has run
1142
- * against the context extraction gives it.
1390
+ * The function an identifier refers to, as its declaration's file and
1391
+ * name, for joins that never walk the body. Null when the reference
1392
+ * does not land on a project function.
1143
1393
  */
1144
- declare function accessContextFor(node: Accessed, sourceFile: SourceFile, resolveWrittenValue?: (value: Node) => Node | null): TsAccessRecognizerContext;
1394
+ declare function functionTargetOf(node: Identifier): {
1395
+ func: FunctionRoot;
1396
+ file: string;
1397
+ name: string;
1398
+ } | null;
1145
1399
 
1146
1400
  /**
1147
1401
  * What a name-valued expression says, as fixed text with a hole for
@@ -1192,6 +1446,33 @@ declare function collectClientFieldAccesses(callExpr: CallExpression, func: Node
1192
1446
  end: number;
1193
1447
  }>, responseSemantics?: ResponsePropertyMapping[]): BranchFieldAccesses[];
1194
1448
 
1449
+ /**
1450
+ * Evidence for drafting a dependency stub: every call the project
1451
+ * makes into a named package, grouped by the export it reaches, with
1452
+ * the argument shapes observed at each call site. `suss infer stub`
1453
+ * turns this into a stub skeleton for an author to fill in. The
1454
+ * package's own code is never read, only the project's calls into it,
1455
+ * so this works for packages whose source no adapter can parse.
1456
+ */
1457
+
1458
+ interface ObservedStubCall {
1459
+ readonly file: string;
1460
+ readonly line: number;
1461
+ readonly args: EffectArg[];
1462
+ }
1463
+ interface StubCallEvidence {
1464
+ readonly exportPath: string[];
1465
+ readonly calls: ObservedStubCall[];
1466
+ }
1467
+ declare function stubEvidenceIn(project: Project, packageName: string, root: string): StubCallEvidence[];
1468
+ interface StubEvidenceOptions {
1469
+ packageName: string;
1470
+ tsConfigFilePath?: string;
1471
+ /** Read when no tsconfig is given. */
1472
+ directory?: string;
1473
+ }
1474
+ declare function stubEvidence(options: StubEvidenceOptions): StubCallEvidence[];
1475
+
1195
1476
  /**
1196
1477
  * Resolve a ts-morph Expression node to a structured ValueRef.
1197
1478
  * Uses only expr.getSymbol()?.getDeclarations()[0] for symbol lookup ,
@@ -1330,4 +1611,4 @@ declare function computeAdapterPacksDigest(packVersions: ReadonlyArray<{
1330
1611
  version?: string;
1331
1612
  }>): string;
1332
1613
 
1333
- export { ADAPTER_VERSION, type Accessed, type CacheDiagnostic, type CacheLookup, type ClientCallSite, type ConfiguredCallContext, type ConfiguredCallRead, type ConfiguredCallSpec, type ContractReadResult, type DiscoveredUnit, type EmptyStage, type ExplainCallableOptions, type ExplainStats, type ExplainedResolution, type ExtractionReport, type FoundTerminal, type FunctionRoot, type HealthCheck, type HealthViolation, type PackFunnel, type PackGradient, type ReadNameOptions, ResolutionStore, type Timer, type TimingReport, type TsDiscoveryContext, type TsInvocationRecognizerContext, type TsJsxAttributeLocation, type TsSubUnitContext, type TypeScriptAdapter, type TypeScriptAdapterConfig, type ValueLocation, type WhyExplained, WhySession, type WhySessionOptions, accessContextFor, callOpsFor, collectAncestorBranches, collectClientFieldAccesses, computeAdapterPacksDigest, computeContentHash, createProjectWithoutTsconfig, createTsDiscoveryContext, createTsSubUnitContext, createTypeScriptAdapter, discoverUnits, evaluatePackHealth, exportedDeclarationsOf, extractCodeStructure, extractRawBranches, findNearestTsconfig, findTerminals, formatPackHealth, invocationContextFor, isImportedFrom, methodDeclaredIn, parseConditionExpression, readConfiguredCall, readContract, readContractForClientCall, readName, resolveAliasedSymbol, resolveSubject, rootIdentifier, workspaceRootFor };
1614
+ export { ADAPTER_VERSION, type Accessed, type CacheDiagnostic, type CacheLookup, type ClientCallSite, type ConfiguredCallContext, type ConfiguredCallRead, type ConfiguredCallSpec, type ContractReadResult, type DiscoveredUnit, type EmptyStage, type ExplainCallableOptions, type ExplainStats, type ExplainedResolution, type ExtractionReport, type FoundTerminal, type FunctionRoot, type HealthCheck, type HealthViolation, type ImportScanOptions, type ObservedStubCall, type PackFunnel, type PackGradient, type ReadNameOptions, ResolutionStore, type StubCallEvidence, type StubEvidenceOptions, type Timer, type TimingReport, type TsDiscoveryContext, type TsInvocationRecognizerContext, type TsJsxAttributeLocation, type TsSubUnitContext, type TypeScriptAdapter, type TypeScriptAdapterConfig, type ValueLocation, type WhyExplained, WhySession, type WhySessionOptions, accessContextFor, buildProjectHelperIndex, callOpsFor, collectAncestorBranches, collectClientFieldAccesses, computeAdapterPacksDigest, computeContentHash, createProjectWithoutTsconfig, createTsDiscoveryContext, createTsSubUnitContext, createTypeScriptAdapter, discoverUnits, evaluatePackHealth, exportedDeclarationsOf, extractCodeStructure, extractRawBranches, findNearestTsconfig, findTerminals, formatPackHealth, functionTargetOf, importDeclarationsOf, importedNamesOf, importedRootsOf, invocationContextFor, isImportedFrom, methodDeclaredIn, namedImportsOf, parseConditionExpression, readConfiguredCall, readContract, readContractForClientCall, readName, resolveAliasedSymbol, resolveSubject, rootIdentifier, stubEvidence, stubEvidenceIn, workspaceRootFor };