@suss/extractor 0.0.2 → 0.1.1

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/README.md CHANGED
@@ -4,7 +4,7 @@ Assembly engine that turns raw language-adapter output into a `BehavioralSummary
4
4
 
5
5
  ## What this package is
6
6
 
7
- `@suss/extractor` is the core assembly layer of the suss pipeline. Language adapters (such as `@suss/adapter-typescript`) parse source code and produce a `RawCodeStructure` — a normalized, adapter-specific intermediate form. The extractor's `assembleSummary` function converts that structure into the final `BehavioralSummary` IR, handling condition polarity, terminal mapping, gap detection, confidence assessment, and `expectedInput` pass-through for client field tracking. It also exports the `RawCodeStructure` type, the `PatternPack` interface, and all related raw types so adapters can share a common contract.
7
+ `@suss/extractor` is the core assembly layer of the suss pipeline. Language adapters (such as `@suss/adapter-typescript`) parse source code and produce a `RawCodeStructure`, a normalized, adapter-specific intermediate form. The extractor's `assembleSummary` function converts that structure into the final `BehavioralSummary` IR, handling condition polarity, terminal mapping, gap detection, confidence assessment, and `expectedInput` pass-through for client field tracking. It also exports the `RawCodeStructure` type, the `PatternPack` interface, and all related raw types so adapters can share a common contract.
8
8
 
9
9
  ## Where it sits in suss
10
10
 
package/dist/index.cjs CHANGED
@@ -95,7 +95,8 @@ function assembleSummary(raw, options = DEFAULT_OPTIONS) {
95
95
  identity: {
96
96
  name: raw.identity.name,
97
97
  exportPath: raw.identity.exportPath,
98
- boundaryBinding: raw.boundaryBinding ?? null
98
+ boundaryBinding: raw.boundaryBinding ?? null,
99
+ ...raw.deployableUnit !== void 0 ? { deployableUnit: raw.deployableUnit } : {}
99
100
  },
100
101
  inputs,
101
102
  transitions,
@@ -150,6 +151,34 @@ function detectGaps(raw, transitions, options) {
150
151
  return [];
151
152
  }
152
153
  const gaps = [];
154
+ if (raw.unmatchedReturns !== void 0 && raw.unmatchedReturns > 0) {
155
+ const count = raw.unmatchedReturns;
156
+ gaps.push({
157
+ // Not a contract violation: the handler may be answering
158
+ // correctly, in a shape nobody taught the pack.
159
+ type: "unreadOutcome",
160
+ conditions: [],
161
+ consequence: "unknown",
162
+ description: count === 1 ? "One return in this function matches none of the terminal shapes this pack looks for, so what it produces is not described here" : `${count} returns in this function match none of the terminal shapes this pack looks for, so what they produce is not described here`
163
+ });
164
+ }
165
+ if (raw.unreadBinding !== void 0) {
166
+ gaps.push({
167
+ type: "unreadOutcome",
168
+ conditions: [],
169
+ consequence: "unknown",
170
+ description: raw.unreadBinding
171
+ });
172
+ }
173
+ const unreadBody = describeUnreadBody(raw);
174
+ if (unreadBody !== null) {
175
+ gaps.push({
176
+ type: "unreadOutcome",
177
+ conditions: [],
178
+ consequence: "unknown",
179
+ description: unreadBody
180
+ });
181
+ }
153
182
  if (raw.declaredContract) {
154
183
  const producedStatuses = new Set(
155
184
  transitions.flatMap((t) => {
@@ -189,7 +218,34 @@ function detectGaps(raw, transitions, options) {
189
218
  }
190
219
  return gaps;
191
220
  }
221
+ function bodyWentUnread(raw) {
222
+ if (raw.bodyContent === "absent" || raw.bodyContent === "elsewhere") {
223
+ return true;
224
+ }
225
+ return raw.bodyContent === "statements" && raw.branches.length === 0;
226
+ }
227
+ function describeUnreadBody(raw) {
228
+ if (!bodyWentUnread(raw)) {
229
+ return null;
230
+ }
231
+ if (raw.bodyContent === "absent") {
232
+ return "This unit is a declaration with no body behind it, so nothing about what it does was read here";
233
+ }
234
+ if (raw.bodyContent === "elsewhere") {
235
+ return "The handler on this boundary comes from outside the code that registers it, so nothing about what it does was read here";
236
+ }
237
+ if ((raw.unmatchedReturns ?? 0) > 0) {
238
+ return null;
239
+ }
240
+ return "Nothing this unit's body does matches a shape this pack looks for, so what it does is not described here";
241
+ }
192
242
  function assessConfidence(raw) {
243
+ if ((raw.unmatchedReturns ?? 0) > 0) {
244
+ return { source: "inferred_static", level: "low" };
245
+ }
246
+ if (bodyWentUnread(raw)) {
247
+ return { source: "inferred_static", level: "low" };
248
+ }
193
249
  let total = 0;
194
250
  let opaque = 0;
195
251
  for (const branch of raw.branches) {
package/dist/index.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { Effect, Predicate, TypeShape, RenderNode, CodeUnitKind, BoundaryBinding, BehavioralSummary, ConfidenceInfo, Transition, Gap, Input, Output } from '@suss/behavioral-ir';
1
+ import { Effect, MessageBusSemantics, DeployableUnit, Predicate, TypeShape, RenderNode, CodeUnitKind, BoundaryBinding, BehavioralSummary, ConfidenceInfo, Transition, Gap, Input, Output } from '@suss/behavioral-ir';
2
2
 
3
3
  type DiscoveryMatch = {
4
4
  type: "namedExport";
@@ -211,30 +211,42 @@ type DiscoveryMatch = {
211
211
  * The adapter populates `DiscoveredUnit.resolverInfo` so the
212
212
  * produced binding carries `graphql-resolver(typeName, fieldName)`.
213
213
  * `typeName` resolves from the class decorator's first argument
214
- * (`@Resolver(() => User)` → `"User"`); when the class decorator
215
- * has no argument it defaults to the operation kind (`"Query"`,
216
- * `"Mutation"`, `"Subscription"`). `fieldName` reads the method
217
- * decorator's `{ name }` option override when present, otherwise
218
- * the method name.
214
+ * (`@Resolver(() => User)` → `"User"`), and from
215
+ * `methodDecoratorTypeMap` when the class decorator has no
216
+ * argument. `fieldName` reads the method decorator's `{ name }`
217
+ * option override when present, otherwise the method name.
219
218
  */
220
219
  type: "decoratedMethod";
221
220
  /**
222
221
  * The module a pack-recognised decorator must be imported from
223
222
  * for discovery to fire. Codebases sometimes wrap framework
224
- * decorators in their own re-exports (NestJS apps frequently
225
- * declare `MetadataResolver` / `CoreResolver` factories that
226
- * compose `@Resolver()` with extra metadata). When that happens,
227
- * the import-module gate would miss them; pass an array of
228
- * accepted modules and any one match suffices.
223
+ * decorators in their own re-exports, composing the framework's
224
+ * decorator with extra metadata. When that happens, the
225
+ * import-module gate would miss them; pass an array of accepted
226
+ * modules and any one match suffices.
229
227
  */
230
228
  importModule: string | string[];
231
229
  /**
232
230
  * Class decorator names to recognise. The first entry that
233
231
  * appears on a class wins for typeName extraction (the rest are
234
- * fallbacks for codebases with multiple wrapper styles).
232
+ * fallbacks for codebases with multiple wrapper styles). A pack
233
+ * ships only what its own framework declares here and takes a
234
+ * project's wrappers through its options.
235
235
  */
236
236
  classDecorators: string[];
237
237
  methodDecorators: string[];
238
+ /**
239
+ * Method decorator name → the type its field hangs off when the
240
+ * class decorator names none. NestJS puts `@Query` on the root
241
+ * `Query` type and `@Mutation` on `Mutation` whatever the class
242
+ * says, so those two answer for themselves.
243
+ *
244
+ * A decorator the map leaves out has no answer of its own, and a
245
+ * class that names no type leaves the field's owner unread. The
246
+ * binding then names no type and pairs with nothing, rather than
247
+ * claiming a field the schema does not have.
248
+ */
249
+ methodDecoratorTypeMap: Record<string, string>;
238
250
  } | {
239
251
  /**
240
252
  * NestJS-style REST controller discovery. Class decorated with
@@ -248,10 +260,9 @@ type DiscoveryMatch = {
248
260
  * Same wrapper-decorator tolerance as `decoratedMethod`: the
249
261
  * import-module gate fires on at least one method-route
250
262
  * decorator from the framework module, but class decorators
251
- * match by name only so project-internal wrappers
252
- * (`@PublicController` / `@AuthedController` factories
253
- * composing `@Controller()`) work without per-project pack
254
- * config.
263
+ * match by name only, so a project-internal wrapper composing
264
+ * the framework's decorator matches once the project names it
265
+ * in the pack's options.
255
266
  *
256
267
  * The adapter populates `DiscoveredUnit.routeInfo` so the
257
268
  * produced binding carries `rest(method, path)`.
@@ -374,7 +385,49 @@ type BindingExtraction = {
374
385
  type: "fromRegistration";
375
386
  position: number;
376
387
  } | {
388
+ type: "fromArgumentProperty";
389
+ position: number;
390
+ property: string;
391
+ } | {
392
+ /**
393
+ * The route path comes from where the file sits, which is how
394
+ * Next.js and React Router describe their routes. The pack
395
+ * spells out its own convention here, since the adapter knows
396
+ * about files and the pack knows what the framework does with
397
+ * their names.
398
+ *
399
+ * `app/api/orders/[id]/route.ts` under `{ root: "app",
400
+ * dropBasenames: ["route"], dynamic: "brackets" }` comes out as
401
+ * `/api/orders/{id}`, which pairs with an Express provider
402
+ * writing `/api/orders/:id`.
403
+ */
377
404
  type: "fromFilename";
405
+ /**
406
+ * Where a route path starts. The directories below it become
407
+ * the path; everything above it belongs to the project layout
408
+ * and is dropped.
409
+ */
410
+ root: string;
411
+ /**
412
+ * Filenames that say what a file is rather than adding a
413
+ * segment: `route`, `page`, `index`, `_index`.
414
+ */
415
+ dropBasenames?: string[];
416
+ /**
417
+ * How the framework writes a parameter in a filename. Next.js
418
+ * uses `[id]`, React Router uses `$id`.
419
+ */
420
+ dynamic?: "brackets" | "dollarPrefix";
421
+ /**
422
+ * Whether a directory in parentheses organises files without
423
+ * appearing in the URL, as `app/(marketing)/about` does.
424
+ */
425
+ dropParenthesized?: boolean;
426
+ /**
427
+ * Whether one filename holds the whole path with dots between
428
+ * the segments, as `routes/orders.$id.tsx` does.
429
+ */
430
+ flat?: boolean;
378
431
  } | {
379
432
  type: "fromContract";
380
433
  } | {
@@ -487,6 +540,10 @@ interface TerminalExtraction {
487
540
  } | {
488
541
  from: "constructor";
489
542
  codes: Record<string, number>;
543
+ } | {
544
+ from: "argumentProperty";
545
+ position: number;
546
+ name: string;
490
547
  } | {
491
548
  from: "argumentConstructor";
492
549
  position: number;
@@ -619,17 +676,38 @@ interface ResponsePropertyMapping {
619
676
  /** What the value semantically represents */
620
677
  semantics: ResponsePropertyMeaning;
621
678
  }
679
+ /**
680
+ * A library wrapper whose result is the function it was handed. The
681
+ * adapter works this out on its own for factories declared in the
682
+ * project by reading their bodies. A library's body is not readable,
683
+ * so the pack states it.
684
+ */
685
+ interface TransparentWrapper {
686
+ /** Callee text as written, e.g. "Sentry.wrapHandler". */
687
+ callee: string;
688
+ /** Which argument holds the wrapped function. */
689
+ argument: number;
690
+ /**
691
+ * The module the callee has to have been imported from. Without it a
692
+ * local object spelled the same way would be taken for the library.
693
+ */
694
+ module: string;
695
+ }
622
696
  interface PatternPack {
623
697
  name: string;
624
698
  /**
625
- * Pack version stamp — feeds the cache invalidation key. Bump on
699
+ * Pack version stamp, which feeds the cache invalidation key. Bump on
626
700
  * any change that affects discovered units / extracted summaries.
627
- * Format is opaque to the adapter; semver is the obvious choice
628
- * but a content hash, build SHA, or monotonic integer works too.
629
- * Optional — packs that omit it use the literal string `"unset"`,
630
- * which means cache entries from one process to the next aren't
631
- * meaningfully versioned (suitable for development; production
632
- * packs should declare a version).
701
+ * Format is opaque to the adapter, so semver or a content hash both
702
+ * work.
703
+ *
704
+ * Optional, because whoever loads the pack knows more about it than
705
+ * the pack does. The CLI folds a hash of the file it loaded and of
706
+ * the config it passed into this stamp, so a pack run through the CLI
707
+ * invalidates on an edit whether or not it declares a version. A host
708
+ * that builds packs some other way carries that responsibility
709
+ * itself. A pack with nothing to stamp reads as `"unset"`, and warm
710
+ * caches will answer for code that has since changed.
633
711
  */
634
712
  version?: string;
635
713
  languages: string[];
@@ -792,6 +870,17 @@ interface PatternPack {
792
870
  * available without an import).
793
871
  */
794
872
  requiresImport?: string[];
873
+ /**
874
+ * Library wrappers whose result is the wrapped function. The adapter
875
+ * derives this automatically for project-local factories by reading
876
+ * their bodies; a library wrapper's body is not readable, so the
877
+ * pack states the judgment: a call to `callee` resolves to its
878
+ * `argument`-th argument.
879
+ *
880
+ * `callee` matches the call expression text as written, e.g.
881
+ * `"Sentry.wrapHandler"`.
882
+ */
883
+ transparentWrappers?: TransparentWrapper[];
795
884
  /**
796
885
  * Per-property-access recognizers — sister to
797
886
  * `invocationRecognizers` but firing on `PropertyAccessExpression`
@@ -884,6 +973,31 @@ interface DiscoveredCustomUnit {
884
973
  method: string;
885
974
  path: string;
886
975
  };
976
+ /**
977
+ * GraphQL field identity for units a callback discovers against an
978
+ * external manifest rather than an in-code resolver map. AppSync
979
+ * routes a field to a Lambda in the deploy template, so the field is
980
+ * the boundary that code serves. When set, the adapter builds a
981
+ * `graphql-resolver` binding from `(typeName, fieldName)`, which
982
+ * pairs with the operations a client sends.
983
+ */
984
+ resolverInfo?: {
985
+ typeName: string;
986
+ fieldName: string;
987
+ };
988
+ /**
989
+ * Message-bus channel identity for consumer units a callback
990
+ * discovers against a subject the code itself names (a handler
991
+ * factory whose config carries the expected subject). When set, the
992
+ * adapter builds a `message-bus` binding from `(messageBus, channel)`,
993
+ * which pairs with producers sending on the same channel.
994
+ */
995
+ channelInfo?: {
996
+ messageBus: MessageBusSemantics["messageBus"];
997
+ channel: string;
998
+ };
999
+ /** The thing that gets deployed and runs this unit, when known. */
1000
+ deployableUnit?: DeployableUnit;
887
1001
  /**
888
1002
  * Metadata merged onto the resulting summary's `metadata` field.
889
1003
  */
@@ -1167,6 +1281,23 @@ interface RawDeclaredContract {
1167
1281
  */
1168
1282
  provenance?: "derived" | "independent";
1169
1283
  }
1284
+ /**
1285
+ * What sits where a unit's body should be.
1286
+ *
1287
+ * - "absent": there is no body behind this declaration (an overload
1288
+ * signature, an ambient declaration, an abstract method), so the
1289
+ * pack read nothing about what the unit does.
1290
+ * - "empty": a body with nothing in it. A summary that says nothing
1291
+ * about it has described it completely.
1292
+ * - "statements": a body with work in it, including a concise
1293
+ * arrow's expression. A summary that says nothing about it has
1294
+ * described none of it.
1295
+ * - "elsewhere": the unit's body is not in what this run read. A
1296
+ * route registered with a handler the caller supplies announces a
1297
+ * boundary and names no function to look in, and the boundary is
1298
+ * worth reporting with nothing behind it.
1299
+ */
1300
+ type BodyContent = "absent" | "empty" | "statements" | "elsewhere";
1170
1301
  interface RawCodeStructure {
1171
1302
  identity: {
1172
1303
  name: string;
@@ -1186,8 +1317,25 @@ interface RawCodeStructure {
1186
1317
  * helpers, pure utilities, and anything the adapter can't place.
1187
1318
  */
1188
1319
  boundaryBinding: BoundaryBinding | null;
1320
+ /** The thing that gets deployed and runs this code, when known. */
1321
+ deployableUnit?: DeployableUnit;
1189
1322
  parameters: RawParameter[];
1190
1323
  branches: RawBranch[];
1324
+ /**
1325
+ * Return statements in the body that none of the pack's terminals
1326
+ * matched. A handler shaped in a way the pack does not describe still
1327
+ * returns something, and saying nothing about it reads the same as
1328
+ * saying it does nothing.
1329
+ */
1330
+ unmatchedReturns?: number;
1331
+ /**
1332
+ * What the adapter found where the unit's body should be. A summary
1333
+ * with nothing in it means one of two different things, and only the
1334
+ * adapter can say which: a body nobody could read, or a body with
1335
+ * nothing in it to read. Adapters that do not say leave this unset,
1336
+ * and everything downstream treats the summary as it did before.
1337
+ */
1338
+ bodyContent?: BodyContent;
1191
1339
  dependencyCalls: RawDependencyCall[];
1192
1340
  declaredContract: RawDeclaredContract | null;
1193
1341
  /**
@@ -1254,6 +1402,17 @@ interface RawCodeStructure {
1254
1402
  reference: string;
1255
1403
  reason: string;
1256
1404
  };
1405
+ /**
1406
+ * Set when the source does not state part of the boundary this unit
1407
+ * sits on. The binding still goes out with that part left empty, so
1408
+ * the unit pairs with nothing rather than with whatever a guess named,
1409
+ * and this sentence tells a reader which part is missing and why.
1410
+ *
1411
+ * A statement about the reading rather than about the code, so it
1412
+ * lands as an `unreadOutcome` gap and no checker holds it against the
1413
+ * unit.
1414
+ */
1415
+ unreadBinding?: string;
1257
1416
  }
1258
1417
  interface ExtractorOptions {
1259
1418
  gapHandling: "strict" | "permissive" | "silent";
@@ -1266,4 +1425,4 @@ declare function terminalToOutput(terminal: RawTerminal): Output;
1266
1425
  declare function effectToIR(effect: RawEffect): Effect;
1267
1426
  declare function paramToInput(param: RawParameter): Input;
1268
1427
 
1269
- export { type AccessRecognizer, type BindingExtraction, type ContractPattern, type DiscoveredCustomUnit, type DiscoveredSubUnit, type DiscoveredSubUnitParent, type DiscoveryMatch, type DiscoveryPattern, type EffectArg, type ExtractorOptions, type InputMappingPattern, type InvocationRecognizer, type PatternPack, type RawBranch, type RawCodeStructure, type RawCondition, type RawDeclaredContract, type RawDependencyCall, type RawEffect, type RawParameter, type RawTerminal, type ResponsePropertyMapping, type ResponsePropertyMeaning, type TerminalExtraction, type TerminalMatch, type TerminalPattern, assembleSummary, assessConfidence, detectGaps, effectToIR, httpRouteDiscovery, makeTransitionId, paramToInput, terminalToOutput };
1428
+ export { type AccessRecognizer, type BindingExtraction, type BodyContent, type ContractPattern, type DiscoveredCustomUnit, type DiscoveredSubUnit, type DiscoveredSubUnitParent, type DiscoveryMatch, type DiscoveryPattern, type EffectArg, type ExtractorOptions, type InputMappingPattern, type InvocationRecognizer, type PatternPack, type RawBranch, type RawCodeStructure, type RawCondition, type RawDeclaredContract, type RawDependencyCall, type RawEffect, type RawParameter, type RawTerminal, type ResponsePropertyMapping, type ResponsePropertyMeaning, type TerminalExtraction, type TerminalMatch, type TerminalPattern, type TransparentWrapper, assembleSummary, assessConfidence, detectGaps, effectToIR, httpRouteDiscovery, makeTransitionId, paramToInput, terminalToOutput };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Effect, Predicate, TypeShape, RenderNode, CodeUnitKind, BoundaryBinding, BehavioralSummary, ConfidenceInfo, Transition, Gap, Input, Output } from '@suss/behavioral-ir';
1
+ import { Effect, MessageBusSemantics, DeployableUnit, Predicate, TypeShape, RenderNode, CodeUnitKind, BoundaryBinding, BehavioralSummary, ConfidenceInfo, Transition, Gap, Input, Output } from '@suss/behavioral-ir';
2
2
 
3
3
  type DiscoveryMatch = {
4
4
  type: "namedExport";
@@ -211,30 +211,42 @@ type DiscoveryMatch = {
211
211
  * The adapter populates `DiscoveredUnit.resolverInfo` so the
212
212
  * produced binding carries `graphql-resolver(typeName, fieldName)`.
213
213
  * `typeName` resolves from the class decorator's first argument
214
- * (`@Resolver(() => User)` → `"User"`); when the class decorator
215
- * has no argument it defaults to the operation kind (`"Query"`,
216
- * `"Mutation"`, `"Subscription"`). `fieldName` reads the method
217
- * decorator's `{ name }` option override when present, otherwise
218
- * the method name.
214
+ * (`@Resolver(() => User)` → `"User"`), and from
215
+ * `methodDecoratorTypeMap` when the class decorator has no
216
+ * argument. `fieldName` reads the method decorator's `{ name }`
217
+ * option override when present, otherwise the method name.
219
218
  */
220
219
  type: "decoratedMethod";
221
220
  /**
222
221
  * The module a pack-recognised decorator must be imported from
223
222
  * for discovery to fire. Codebases sometimes wrap framework
224
- * decorators in their own re-exports (NestJS apps frequently
225
- * declare `MetadataResolver` / `CoreResolver` factories that
226
- * compose `@Resolver()` with extra metadata). When that happens,
227
- * the import-module gate would miss them; pass an array of
228
- * accepted modules and any one match suffices.
223
+ * decorators in their own re-exports, composing the framework's
224
+ * decorator with extra metadata. When that happens, the
225
+ * import-module gate would miss them; pass an array of accepted
226
+ * modules and any one match suffices.
229
227
  */
230
228
  importModule: string | string[];
231
229
  /**
232
230
  * Class decorator names to recognise. The first entry that
233
231
  * appears on a class wins for typeName extraction (the rest are
234
- * fallbacks for codebases with multiple wrapper styles).
232
+ * fallbacks for codebases with multiple wrapper styles). A pack
233
+ * ships only what its own framework declares here and takes a
234
+ * project's wrappers through its options.
235
235
  */
236
236
  classDecorators: string[];
237
237
  methodDecorators: string[];
238
+ /**
239
+ * Method decorator name → the type its field hangs off when the
240
+ * class decorator names none. NestJS puts `@Query` on the root
241
+ * `Query` type and `@Mutation` on `Mutation` whatever the class
242
+ * says, so those two answer for themselves.
243
+ *
244
+ * A decorator the map leaves out has no answer of its own, and a
245
+ * class that names no type leaves the field's owner unread. The
246
+ * binding then names no type and pairs with nothing, rather than
247
+ * claiming a field the schema does not have.
248
+ */
249
+ methodDecoratorTypeMap: Record<string, string>;
238
250
  } | {
239
251
  /**
240
252
  * NestJS-style REST controller discovery. Class decorated with
@@ -248,10 +260,9 @@ type DiscoveryMatch = {
248
260
  * Same wrapper-decorator tolerance as `decoratedMethod`: the
249
261
  * import-module gate fires on at least one method-route
250
262
  * decorator from the framework module, but class decorators
251
- * match by name only so project-internal wrappers
252
- * (`@PublicController` / `@AuthedController` factories
253
- * composing `@Controller()`) work without per-project pack
254
- * config.
263
+ * match by name only, so a project-internal wrapper composing
264
+ * the framework's decorator matches once the project names it
265
+ * in the pack's options.
255
266
  *
256
267
  * The adapter populates `DiscoveredUnit.routeInfo` so the
257
268
  * produced binding carries `rest(method, path)`.
@@ -374,7 +385,49 @@ type BindingExtraction = {
374
385
  type: "fromRegistration";
375
386
  position: number;
376
387
  } | {
388
+ type: "fromArgumentProperty";
389
+ position: number;
390
+ property: string;
391
+ } | {
392
+ /**
393
+ * The route path comes from where the file sits, which is how
394
+ * Next.js and React Router describe their routes. The pack
395
+ * spells out its own convention here, since the adapter knows
396
+ * about files and the pack knows what the framework does with
397
+ * their names.
398
+ *
399
+ * `app/api/orders/[id]/route.ts` under `{ root: "app",
400
+ * dropBasenames: ["route"], dynamic: "brackets" }` comes out as
401
+ * `/api/orders/{id}`, which pairs with an Express provider
402
+ * writing `/api/orders/:id`.
403
+ */
377
404
  type: "fromFilename";
405
+ /**
406
+ * Where a route path starts. The directories below it become
407
+ * the path; everything above it belongs to the project layout
408
+ * and is dropped.
409
+ */
410
+ root: string;
411
+ /**
412
+ * Filenames that say what a file is rather than adding a
413
+ * segment: `route`, `page`, `index`, `_index`.
414
+ */
415
+ dropBasenames?: string[];
416
+ /**
417
+ * How the framework writes a parameter in a filename. Next.js
418
+ * uses `[id]`, React Router uses `$id`.
419
+ */
420
+ dynamic?: "brackets" | "dollarPrefix";
421
+ /**
422
+ * Whether a directory in parentheses organises files without
423
+ * appearing in the URL, as `app/(marketing)/about` does.
424
+ */
425
+ dropParenthesized?: boolean;
426
+ /**
427
+ * Whether one filename holds the whole path with dots between
428
+ * the segments, as `routes/orders.$id.tsx` does.
429
+ */
430
+ flat?: boolean;
378
431
  } | {
379
432
  type: "fromContract";
380
433
  } | {
@@ -487,6 +540,10 @@ interface TerminalExtraction {
487
540
  } | {
488
541
  from: "constructor";
489
542
  codes: Record<string, number>;
543
+ } | {
544
+ from: "argumentProperty";
545
+ position: number;
546
+ name: string;
490
547
  } | {
491
548
  from: "argumentConstructor";
492
549
  position: number;
@@ -619,17 +676,38 @@ interface ResponsePropertyMapping {
619
676
  /** What the value semantically represents */
620
677
  semantics: ResponsePropertyMeaning;
621
678
  }
679
+ /**
680
+ * A library wrapper whose result is the function it was handed. The
681
+ * adapter works this out on its own for factories declared in the
682
+ * project by reading their bodies. A library's body is not readable,
683
+ * so the pack states it.
684
+ */
685
+ interface TransparentWrapper {
686
+ /** Callee text as written, e.g. "Sentry.wrapHandler". */
687
+ callee: string;
688
+ /** Which argument holds the wrapped function. */
689
+ argument: number;
690
+ /**
691
+ * The module the callee has to have been imported from. Without it a
692
+ * local object spelled the same way would be taken for the library.
693
+ */
694
+ module: string;
695
+ }
622
696
  interface PatternPack {
623
697
  name: string;
624
698
  /**
625
- * Pack version stamp — feeds the cache invalidation key. Bump on
699
+ * Pack version stamp, which feeds the cache invalidation key. Bump on
626
700
  * any change that affects discovered units / extracted summaries.
627
- * Format is opaque to the adapter; semver is the obvious choice
628
- * but a content hash, build SHA, or monotonic integer works too.
629
- * Optional — packs that omit it use the literal string `"unset"`,
630
- * which means cache entries from one process to the next aren't
631
- * meaningfully versioned (suitable for development; production
632
- * packs should declare a version).
701
+ * Format is opaque to the adapter, so semver or a content hash both
702
+ * work.
703
+ *
704
+ * Optional, because whoever loads the pack knows more about it than
705
+ * the pack does. The CLI folds a hash of the file it loaded and of
706
+ * the config it passed into this stamp, so a pack run through the CLI
707
+ * invalidates on an edit whether or not it declares a version. A host
708
+ * that builds packs some other way carries that responsibility
709
+ * itself. A pack with nothing to stamp reads as `"unset"`, and warm
710
+ * caches will answer for code that has since changed.
633
711
  */
634
712
  version?: string;
635
713
  languages: string[];
@@ -792,6 +870,17 @@ interface PatternPack {
792
870
  * available without an import).
793
871
  */
794
872
  requiresImport?: string[];
873
+ /**
874
+ * Library wrappers whose result is the wrapped function. The adapter
875
+ * derives this automatically for project-local factories by reading
876
+ * their bodies; a library wrapper's body is not readable, so the
877
+ * pack states the judgment: a call to `callee` resolves to its
878
+ * `argument`-th argument.
879
+ *
880
+ * `callee` matches the call expression text as written, e.g.
881
+ * `"Sentry.wrapHandler"`.
882
+ */
883
+ transparentWrappers?: TransparentWrapper[];
795
884
  /**
796
885
  * Per-property-access recognizers — sister to
797
886
  * `invocationRecognizers` but firing on `PropertyAccessExpression`
@@ -884,6 +973,31 @@ interface DiscoveredCustomUnit {
884
973
  method: string;
885
974
  path: string;
886
975
  };
976
+ /**
977
+ * GraphQL field identity for units a callback discovers against an
978
+ * external manifest rather than an in-code resolver map. AppSync
979
+ * routes a field to a Lambda in the deploy template, so the field is
980
+ * the boundary that code serves. When set, the adapter builds a
981
+ * `graphql-resolver` binding from `(typeName, fieldName)`, which
982
+ * pairs with the operations a client sends.
983
+ */
984
+ resolverInfo?: {
985
+ typeName: string;
986
+ fieldName: string;
987
+ };
988
+ /**
989
+ * Message-bus channel identity for consumer units a callback
990
+ * discovers against a subject the code itself names (a handler
991
+ * factory whose config carries the expected subject). When set, the
992
+ * adapter builds a `message-bus` binding from `(messageBus, channel)`,
993
+ * which pairs with producers sending on the same channel.
994
+ */
995
+ channelInfo?: {
996
+ messageBus: MessageBusSemantics["messageBus"];
997
+ channel: string;
998
+ };
999
+ /** The thing that gets deployed and runs this unit, when known. */
1000
+ deployableUnit?: DeployableUnit;
887
1001
  /**
888
1002
  * Metadata merged onto the resulting summary's `metadata` field.
889
1003
  */
@@ -1167,6 +1281,23 @@ interface RawDeclaredContract {
1167
1281
  */
1168
1282
  provenance?: "derived" | "independent";
1169
1283
  }
1284
+ /**
1285
+ * What sits where a unit's body should be.
1286
+ *
1287
+ * - "absent": there is no body behind this declaration (an overload
1288
+ * signature, an ambient declaration, an abstract method), so the
1289
+ * pack read nothing about what the unit does.
1290
+ * - "empty": a body with nothing in it. A summary that says nothing
1291
+ * about it has described it completely.
1292
+ * - "statements": a body with work in it, including a concise
1293
+ * arrow's expression. A summary that says nothing about it has
1294
+ * described none of it.
1295
+ * - "elsewhere": the unit's body is not in what this run read. A
1296
+ * route registered with a handler the caller supplies announces a
1297
+ * boundary and names no function to look in, and the boundary is
1298
+ * worth reporting with nothing behind it.
1299
+ */
1300
+ type BodyContent = "absent" | "empty" | "statements" | "elsewhere";
1170
1301
  interface RawCodeStructure {
1171
1302
  identity: {
1172
1303
  name: string;
@@ -1186,8 +1317,25 @@ interface RawCodeStructure {
1186
1317
  * helpers, pure utilities, and anything the adapter can't place.
1187
1318
  */
1188
1319
  boundaryBinding: BoundaryBinding | null;
1320
+ /** The thing that gets deployed and runs this code, when known. */
1321
+ deployableUnit?: DeployableUnit;
1189
1322
  parameters: RawParameter[];
1190
1323
  branches: RawBranch[];
1324
+ /**
1325
+ * Return statements in the body that none of the pack's terminals
1326
+ * matched. A handler shaped in a way the pack does not describe still
1327
+ * returns something, and saying nothing about it reads the same as
1328
+ * saying it does nothing.
1329
+ */
1330
+ unmatchedReturns?: number;
1331
+ /**
1332
+ * What the adapter found where the unit's body should be. A summary
1333
+ * with nothing in it means one of two different things, and only the
1334
+ * adapter can say which: a body nobody could read, or a body with
1335
+ * nothing in it to read. Adapters that do not say leave this unset,
1336
+ * and everything downstream treats the summary as it did before.
1337
+ */
1338
+ bodyContent?: BodyContent;
1191
1339
  dependencyCalls: RawDependencyCall[];
1192
1340
  declaredContract: RawDeclaredContract | null;
1193
1341
  /**
@@ -1254,6 +1402,17 @@ interface RawCodeStructure {
1254
1402
  reference: string;
1255
1403
  reason: string;
1256
1404
  };
1405
+ /**
1406
+ * Set when the source does not state part of the boundary this unit
1407
+ * sits on. The binding still goes out with that part left empty, so
1408
+ * the unit pairs with nothing rather than with whatever a guess named,
1409
+ * and this sentence tells a reader which part is missing and why.
1410
+ *
1411
+ * A statement about the reading rather than about the code, so it
1412
+ * lands as an `unreadOutcome` gap and no checker holds it against the
1413
+ * unit.
1414
+ */
1415
+ unreadBinding?: string;
1257
1416
  }
1258
1417
  interface ExtractorOptions {
1259
1418
  gapHandling: "strict" | "permissive" | "silent";
@@ -1266,4 +1425,4 @@ declare function terminalToOutput(terminal: RawTerminal): Output;
1266
1425
  declare function effectToIR(effect: RawEffect): Effect;
1267
1426
  declare function paramToInput(param: RawParameter): Input;
1268
1427
 
1269
- export { type AccessRecognizer, type BindingExtraction, type ContractPattern, type DiscoveredCustomUnit, type DiscoveredSubUnit, type DiscoveredSubUnitParent, type DiscoveryMatch, type DiscoveryPattern, type EffectArg, type ExtractorOptions, type InputMappingPattern, type InvocationRecognizer, type PatternPack, type RawBranch, type RawCodeStructure, type RawCondition, type RawDeclaredContract, type RawDependencyCall, type RawEffect, type RawParameter, type RawTerminal, type ResponsePropertyMapping, type ResponsePropertyMeaning, type TerminalExtraction, type TerminalMatch, type TerminalPattern, assembleSummary, assessConfidence, detectGaps, effectToIR, httpRouteDiscovery, makeTransitionId, paramToInput, terminalToOutput };
1428
+ export { type AccessRecognizer, type BindingExtraction, type BodyContent, type ContractPattern, type DiscoveredCustomUnit, type DiscoveredSubUnit, type DiscoveredSubUnitParent, type DiscoveryMatch, type DiscoveryPattern, type EffectArg, type ExtractorOptions, type InputMappingPattern, type InvocationRecognizer, type PatternPack, type RawBranch, type RawCodeStructure, type RawCondition, type RawDeclaredContract, type RawDependencyCall, type RawEffect, type RawParameter, type RawTerminal, type ResponsePropertyMapping, type ResponsePropertyMeaning, type TerminalExtraction, type TerminalMatch, type TerminalPattern, type TransparentWrapper, assembleSummary, assessConfidence, detectGaps, effectToIR, httpRouteDiscovery, makeTransitionId, paramToInput, terminalToOutput };
package/dist/index.js CHANGED
@@ -64,7 +64,8 @@ function assembleSummary(raw, options = DEFAULT_OPTIONS) {
64
64
  identity: {
65
65
  name: raw.identity.name,
66
66
  exportPath: raw.identity.exportPath,
67
- boundaryBinding: raw.boundaryBinding ?? null
67
+ boundaryBinding: raw.boundaryBinding ?? null,
68
+ ...raw.deployableUnit !== void 0 ? { deployableUnit: raw.deployableUnit } : {}
68
69
  },
69
70
  inputs,
70
71
  transitions,
@@ -119,6 +120,34 @@ function detectGaps(raw, transitions, options) {
119
120
  return [];
120
121
  }
121
122
  const gaps = [];
123
+ if (raw.unmatchedReturns !== void 0 && raw.unmatchedReturns > 0) {
124
+ const count = raw.unmatchedReturns;
125
+ gaps.push({
126
+ // Not a contract violation: the handler may be answering
127
+ // correctly, in a shape nobody taught the pack.
128
+ type: "unreadOutcome",
129
+ conditions: [],
130
+ consequence: "unknown",
131
+ description: count === 1 ? "One return in this function matches none of the terminal shapes this pack looks for, so what it produces is not described here" : `${count} returns in this function match none of the terminal shapes this pack looks for, so what they produce is not described here`
132
+ });
133
+ }
134
+ if (raw.unreadBinding !== void 0) {
135
+ gaps.push({
136
+ type: "unreadOutcome",
137
+ conditions: [],
138
+ consequence: "unknown",
139
+ description: raw.unreadBinding
140
+ });
141
+ }
142
+ const unreadBody = describeUnreadBody(raw);
143
+ if (unreadBody !== null) {
144
+ gaps.push({
145
+ type: "unreadOutcome",
146
+ conditions: [],
147
+ consequence: "unknown",
148
+ description: unreadBody
149
+ });
150
+ }
122
151
  if (raw.declaredContract) {
123
152
  const producedStatuses = new Set(
124
153
  transitions.flatMap((t) => {
@@ -158,7 +187,34 @@ function detectGaps(raw, transitions, options) {
158
187
  }
159
188
  return gaps;
160
189
  }
190
+ function bodyWentUnread(raw) {
191
+ if (raw.bodyContent === "absent" || raw.bodyContent === "elsewhere") {
192
+ return true;
193
+ }
194
+ return raw.bodyContent === "statements" && raw.branches.length === 0;
195
+ }
196
+ function describeUnreadBody(raw) {
197
+ if (!bodyWentUnread(raw)) {
198
+ return null;
199
+ }
200
+ if (raw.bodyContent === "absent") {
201
+ return "This unit is a declaration with no body behind it, so nothing about what it does was read here";
202
+ }
203
+ if (raw.bodyContent === "elsewhere") {
204
+ return "The handler on this boundary comes from outside the code that registers it, so nothing about what it does was read here";
205
+ }
206
+ if ((raw.unmatchedReturns ?? 0) > 0) {
207
+ return null;
208
+ }
209
+ return "Nothing this unit's body does matches a shape this pack looks for, so what it does is not described here";
210
+ }
161
211
  function assessConfidence(raw) {
212
+ if ((raw.unmatchedReturns ?? 0) > 0) {
213
+ return { source: "inferred_static", level: "low" };
214
+ }
215
+ if (bodyWentUnread(raw)) {
216
+ return { source: "inferred_static", level: "low" };
217
+ }
162
218
  let total = 0;
163
219
  let opaque = 0;
164
220
  for (const branch of raw.branches) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@suss/extractor",
3
- "version": "0.0.2",
3
+ "version": "0.1.1",
4
4
  "type": "module",
5
5
  "description": "Framework-agnostic extraction pipeline that turns raw extractions into suss behavioral summaries.",
6
6
  "keywords": [
@@ -45,7 +45,7 @@
45
45
  "typecheck": "tsc --noEmit"
46
46
  },
47
47
  "dependencies": {
48
- "@suss/behavioral-ir": "0.0.2"
48
+ "@suss/behavioral-ir": "0.1.1"
49
49
  },
50
50
  "devDependencies": {
51
51
  "tsup": "*",