@suss/extractor 0.3.1 → 0.4.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.js CHANGED
@@ -1,5 +1,10 @@
1
1
  // src/index.ts
2
2
  import { createHash } from "crypto";
3
+ import {
4
+ exchangesHttpResponses,
5
+ withGraphqlMetadata,
6
+ withHttpMetadata
7
+ } from "@suss/behavioral-ir";
3
8
 
4
9
  // src/inputReads.ts
5
10
  function inputReadsOf(args) {
@@ -93,13 +98,521 @@ function httpRouteDiscovery(opts) {
93
98
  registrationChain: [...opts.methods]
94
99
  },
95
100
  bindingExtraction: {
96
- method: { type: "fromRegistration", position: "methodName" },
101
+ // `.all` registers every method, so it records "*", which the pairing
102
+ // engine treats as agreeing with any method at all.
103
+ method: {
104
+ type: "fromRegistration",
105
+ position: "methodName",
106
+ nameMap: { all: "*" }
107
+ },
97
108
  path: { type: "fromRegistration", position: 0 }
98
109
  },
110
+ ...opts.mount !== void 0 ? { mount: opts.mount } : {},
99
111
  requiresImport: [opts.importModule]
100
112
  }));
101
113
  }
102
114
 
115
+ // src/paths/enumeratePaths.ts
116
+ var MAX_PATHS = 256;
117
+ var UnmodeledFlow = class extends Error {
118
+ };
119
+ var PathBudgetExceeded = class extends Error {
120
+ };
121
+ function branchCond(branchStmt, condition, polarity, oppositeExit) {
122
+ return {
123
+ info: {
124
+ sourceText: condition.sourceText,
125
+ polarity,
126
+ source: "explicit",
127
+ expression: condition.expression
128
+ },
129
+ branchStmt,
130
+ oppositeExit
131
+ };
132
+ }
133
+ function fixedCond(condition, polarity, source) {
134
+ return {
135
+ info: {
136
+ sourceText: condition.sourceText,
137
+ polarity,
138
+ source,
139
+ expression: condition.expression
140
+ },
141
+ branchStmt: null,
142
+ oppositeExit: null
143
+ };
144
+ }
145
+ var loopIterationCond = (headerText) => fixedCond(
146
+ { sourceText: `some iteration of: ${headerText}`, expression: null },
147
+ "positive",
148
+ "explicit"
149
+ );
150
+ var loopCompletedCond = (headerText, exit) => fixedCond(
151
+ {
152
+ sourceText: `loop exited via ${exit ?? "return"}: ${headerText}`,
153
+ expression: null
154
+ },
155
+ "negative",
156
+ exit === "throw" ? "earlyThrow" : "earlyReturn"
157
+ );
158
+ var catchEntryCond = () => fixedCond(
159
+ { sourceText: "catch", expression: null },
160
+ "positive",
161
+ "catchBlock"
162
+ );
163
+ function classify(path, home, isAncestorOrSelf) {
164
+ return path.map((cond) => {
165
+ if (cond.branchStmt === null) {
166
+ return cond.info;
167
+ }
168
+ const encloses = home !== null && isAncestorOrSelf(cond.branchStmt, home);
169
+ if (encloses || cond.info.polarity === "positive") {
170
+ return { ...cond.info, source: "explicit" };
171
+ }
172
+ const source = cond.oppositeExit === "throw" ? "earlyThrow" : "earlyReturn";
173
+ return { ...cond.info, source };
174
+ });
175
+ }
176
+ function childrenOf(stmt) {
177
+ if (stmt.kind === "if") {
178
+ return stmt.elseBody === null ? stmt.thenBody : [...stmt.thenBody, ...stmt.elseBody];
179
+ }
180
+ if (stmt.kind === "switch") {
181
+ return stmt.groups.flatMap((g) => g.body);
182
+ }
183
+ if (stmt.kind === "loop") {
184
+ return stmt.body;
185
+ }
186
+ if (stmt.kind === "try") {
187
+ return [
188
+ ...stmt.tryBody,
189
+ ...stmt.catchBody ?? [],
190
+ ...stmt.finallyBody ?? []
191
+ ];
192
+ }
193
+ return [];
194
+ }
195
+ function hasStrayBreak(nodes) {
196
+ return nodes.some((node) => {
197
+ if (node.kind === "exit" && node.exit === "break") {
198
+ return true;
199
+ }
200
+ if (node.kind === "switch" || node.kind === "loop") {
201
+ return false;
202
+ }
203
+ return hasStrayBreak(childrenOf(node));
204
+ });
205
+ }
206
+ function exitKindOfList(stmts) {
207
+ let sawReturn = false;
208
+ for (const stmt of stmts) {
209
+ if (stmt.exitKind === "throw") {
210
+ return "throw";
211
+ }
212
+ if (stmt.exitKind === "return") {
213
+ sawReturn = true;
214
+ }
215
+ }
216
+ return sawReturn ? "return" : null;
217
+ }
218
+ function chargeBudget(state) {
219
+ state.pathCount++;
220
+ if (state.pathCount > MAX_PATHS) {
221
+ throw new PathBudgetExceeded();
222
+ }
223
+ }
224
+ function recordTerminal(ctx, terminal, home, path) {
225
+ chargeBudget(ctx.state);
226
+ const info = classify(path, home, ctx.isAncestorOrSelf);
227
+ const existing = ctx.state.byTerminal.get(terminal) ?? [];
228
+ existing.push(info);
229
+ ctx.state.byTerminal.set(terminal, existing);
230
+ }
231
+ function containsTerminal(ctx, stmt) {
232
+ if ((ctx.terminalsByStmt.get(stmt)?.length ?? 0) > 0) {
233
+ return true;
234
+ }
235
+ return childrenOf(stmt).some((child) => containsTerminal(ctx, child));
236
+ }
237
+ function stepIf(stmt, path, ctx) {
238
+ const armsExit = stmt.exitKind !== null;
239
+ const armsHaveTerminals = containsTerminal(ctx, stmt);
240
+ if (!armsExit && !armsHaveTerminals) {
241
+ return [path];
242
+ }
243
+ const thenExit = exitKindOfList(stmt.thenBody);
244
+ const elseExit = stmt.elseBody === null ? null : exitKindOfList(stmt.elseBody);
245
+ const thenPath = [
246
+ ...path,
247
+ branchCond(stmt, stmt.condition, "positive", elseExit)
248
+ ];
249
+ const elsePath = [
250
+ ...path,
251
+ branchCond(stmt, stmt.condition, "negative", thenExit)
252
+ ];
253
+ const out = [];
254
+ out.push(...enumerate(ctx, stmt.thenBody, thenPath));
255
+ if (stmt.elseBody !== null) {
256
+ out.push(...enumerate(ctx, stmt.elseBody, elsePath));
257
+ } else {
258
+ out.push(elsePath);
259
+ }
260
+ return out;
261
+ }
262
+ function stepSwitch(stmt, path, ctx) {
263
+ const out = [];
264
+ const negations = [];
265
+ let defaultRanBody = false;
266
+ for (let i = 0; i < stmt.groups.length; i++) {
267
+ const group = stmt.groups[i];
268
+ const fallthroughSafe = i === stmt.groups.length - 1;
269
+ if (hasStrayBreak(group.body)) {
270
+ throw new UnmodeledFlow("non-trailing break in switch clause");
271
+ }
272
+ const groupPath = group.condition === null ? [...path, ...negations] : [...path, fixedCond(group.condition, "positive", "explicit")];
273
+ const conts = enumerate(ctx, group.body, groupPath);
274
+ if (conts.length > 0) {
275
+ if (!group.hasTrailingBreak && !fallthroughSafe) {
276
+ throw new UnmodeledFlow("fallthrough into a non-empty switch clause");
277
+ }
278
+ out.push(...conts);
279
+ }
280
+ if (group.condition === null) {
281
+ defaultRanBody = true;
282
+ } else {
283
+ const exit = exitKindOfList(group.body);
284
+ const negationSource = exit === "throw" ? "earlyThrow" : exit === "return" ? "earlyReturn" : "explicit";
285
+ negations.push(fixedCond(group.condition, "negative", negationSource));
286
+ }
287
+ }
288
+ if (!defaultRanBody) {
289
+ out.push([...path, ...negations]);
290
+ }
291
+ return out;
292
+ }
293
+ function stepLoop(stmt, path, ctx) {
294
+ const bodyExit = exitKindOfList(stmt.body);
295
+ if (containsTerminal(ctx, stmt)) {
296
+ enumerate(ctx, stmt.body, [
297
+ ...path,
298
+ loopIterationCond(stmt.condition.sourceText)
299
+ ]);
300
+ }
301
+ if (bodyExit !== null) {
302
+ return [[...path, loopCompletedCond(stmt.condition.sourceText, bodyExit)]];
303
+ }
304
+ return [path];
305
+ }
306
+ function stepTry(stmt, path, ctx) {
307
+ if (stmt.finallyBody !== null) {
308
+ const finallyHasTerminal = stmt.finallyBody.some(
309
+ (s) => containsTerminal(ctx, s)
310
+ );
311
+ if (exitKindOfList(stmt.finallyBody) !== null || finallyHasTerminal) {
312
+ throw new UnmodeledFlow("finally block with exits or terminals");
313
+ }
314
+ }
315
+ const out = [];
316
+ out.push(...enumerate(ctx, stmt.tryBody, path));
317
+ if (stmt.catchBody !== null) {
318
+ const catchPath = [...path, catchEntryCond()];
319
+ out.push(...enumerate(ctx, stmt.catchBody, catchPath));
320
+ }
321
+ return out;
322
+ }
323
+ function stepExit(_stmt, _path, _ctx) {
324
+ return [];
325
+ }
326
+ function stepOpaque(_stmt, path, _ctx) {
327
+ return [path];
328
+ }
329
+ var stepHandlers = {
330
+ if: stepIf,
331
+ switch: stepSwitch,
332
+ loop: stepLoop,
333
+ try: stepTry,
334
+ exit: stepExit,
335
+ opaque: stepOpaque
336
+ };
337
+ function stepStatement(ctx, stmt, path) {
338
+ const ownTerminals = ctx.terminalsByStmt.get(stmt) ?? [];
339
+ for (const terminal of ownTerminals) {
340
+ recordTerminal(ctx, terminal, stmt, path);
341
+ }
342
+ const handler = stepHandlers[stmt.kind];
343
+ return handler(
344
+ stmt,
345
+ path,
346
+ ctx
347
+ );
348
+ }
349
+ function soleDisagreement(left, right) {
350
+ if (left.length !== right.length) {
351
+ return null;
352
+ }
353
+ let found = null;
354
+ for (let at = 0; at < left.length; at++) {
355
+ const one = left[at];
356
+ const other = right[at];
357
+ if (one === other) {
358
+ continue;
359
+ }
360
+ if (found !== null) {
361
+ return null;
362
+ }
363
+ if (one === void 0 || other === void 0 || one.branchStmt === null || one.branchStmt !== other.branchStmt || one.info.polarity === other.info.polarity) {
364
+ return null;
365
+ }
366
+ found = at;
367
+ }
368
+ return found;
369
+ }
370
+ function mergeRejoined(paths) {
371
+ let current = paths;
372
+ for (let round = 0; round < current.length; round++) {
373
+ const out = [];
374
+ const taken = /* @__PURE__ */ new Set();
375
+ let mergedAny = false;
376
+ for (let i = 0; i < current.length; i++) {
377
+ if (taken.has(i)) {
378
+ continue;
379
+ }
380
+ let combined = current[i];
381
+ for (let j = i + 1; j < current.length; j++) {
382
+ if (taken.has(j)) {
383
+ continue;
384
+ }
385
+ const at = soleDisagreement(combined, current[j]);
386
+ if (at === null) {
387
+ continue;
388
+ }
389
+ combined = [...combined.slice(0, at), ...combined.slice(at + 1)];
390
+ taken.add(j);
391
+ mergedAny = true;
392
+ break;
393
+ }
394
+ out.push(combined);
395
+ }
396
+ current = out;
397
+ if (!mergedAny) {
398
+ break;
399
+ }
400
+ }
401
+ return current;
402
+ }
403
+ function enumerate(ctx, stmts, prefix) {
404
+ let frontiers = [[...prefix]];
405
+ for (const stmt of stmts) {
406
+ const nextFrontiers = [];
407
+ for (const path of frontiers) {
408
+ nextFrontiers.push(...stepStatement(ctx, stmt, path));
409
+ }
410
+ frontiers = mergeRejoined(nextFrontiers);
411
+ if (frontiers.length > MAX_PATHS) {
412
+ throw new PathBudgetExceeded();
413
+ }
414
+ if (frontiers.length === 0) {
415
+ break;
416
+ }
417
+ }
418
+ return frontiers;
419
+ }
420
+ function buildParentMap(statements) {
421
+ const parentOf = /* @__PURE__ */ new Map();
422
+ const visit = (stmt, parent) => {
423
+ parentOf.set(stmt, parent);
424
+ for (const child of childrenOf(stmt)) {
425
+ visit(child, stmt);
426
+ }
427
+ };
428
+ for (const stmt of statements) {
429
+ visit(stmt, null);
430
+ }
431
+ return parentOf;
432
+ }
433
+ function enumerateStructuredPaths(input) {
434
+ const parentOf = buildParentMap(input.statements);
435
+ const isAncestorOrSelf = (a, b) => {
436
+ let current = b;
437
+ while (current !== null) {
438
+ if (current === a) {
439
+ return true;
440
+ }
441
+ current = parentOf.get(current) ?? null;
442
+ }
443
+ return false;
444
+ };
445
+ const state = {
446
+ byTerminal: /* @__PURE__ */ new Map(),
447
+ fallthrough: [],
448
+ pathCount: 0
449
+ };
450
+ const ctx = {
451
+ terminalsByStmt: input.terminalsByStmt,
452
+ state,
453
+ isAncestorOrSelf
454
+ };
455
+ const continuations = enumerate(ctx, input.statements, []);
456
+ for (const path of continuations) {
457
+ chargeBudget(state);
458
+ state.fallthrough.push(classify(path, null, isAncestorOrSelf));
459
+ }
460
+ return { byTerminal: state.byTerminal, fallthrough: state.fallthrough };
461
+ }
462
+ function enumerateOrDegrade(input, terminals) {
463
+ try {
464
+ return { ...enumerateStructuredPaths(input), degraded: null };
465
+ } catch (error) {
466
+ const reason = reasonToDegrade(error);
467
+ if (reason === null) {
468
+ throw error;
469
+ }
470
+ const marker = {
471
+ sourceText: `unmodeled control flow (${reason})`,
472
+ polarity: "positive",
473
+ source: "explicit",
474
+ expression: null
475
+ };
476
+ const byTerminal = /* @__PURE__ */ new Map();
477
+ for (const terminal of terminals) {
478
+ byTerminal.set(terminal, [[marker]]);
479
+ }
480
+ return { byTerminal, fallthrough: [[marker]], degraded: reason };
481
+ }
482
+ }
483
+ function reasonToDegrade(error) {
484
+ if (error instanceof PathBudgetExceeded) {
485
+ return "path budget exceeded";
486
+ }
487
+ if (error instanceof UnmodeledFlow) {
488
+ return error.message;
489
+ }
490
+ return null;
491
+ }
492
+
493
+ // src/paths/gatingConditions.ts
494
+ var keyOf = (condition) => `${condition.polarity}:${condition.sourceText}`;
495
+ function sharedGatingConditions(paths, readPredicate) {
496
+ const [first, ...rest] = paths ?? [];
497
+ if (first === void 0) {
498
+ return [];
499
+ }
500
+ const shared = new Set(first.map(keyOf));
501
+ for (const path of rest) {
502
+ const here = new Set(path.map(keyOf));
503
+ for (const value of shared) {
504
+ if (!here.has(value)) {
505
+ shared.delete(value);
506
+ }
507
+ }
508
+ }
509
+ return first.filter((condition) => shared.has(keyOf(condition))).map((condition) => ({
510
+ sourceText: condition.sourceText,
511
+ structured: readPredicate === void 0 || condition.expression === null ? null : readPredicate(condition.expression),
512
+ polarity: condition.polarity,
513
+ source: condition.source
514
+ }));
515
+ }
516
+
517
+ // src/paths/nodeIdentity.ts
518
+ var IdSet = class {
519
+ byKey = /* @__PURE__ */ new Map();
520
+ constructor(nodes = []) {
521
+ for (const node of nodes) {
522
+ this.add(node);
523
+ }
524
+ }
525
+ add(node) {
526
+ this.byKey.set(node.id, node);
527
+ return this;
528
+ }
529
+ has(node) {
530
+ return this.byKey.has(node.id);
531
+ }
532
+ /** The node this set was built with, which is the caller's own handle for it. */
533
+ get(node) {
534
+ return this.byKey.get(node.id);
535
+ }
536
+ get size() {
537
+ return this.byKey.size;
538
+ }
539
+ [Symbol.iterator]() {
540
+ return this.byKey.values();
541
+ }
542
+ };
543
+ var IdMap = class {
544
+ entries = /* @__PURE__ */ new Map();
545
+ set(node, value) {
546
+ this.entries.set(node.id, [node, value]);
547
+ return this;
548
+ }
549
+ get(node) {
550
+ return this.entries.get(node.id)?.[1];
551
+ }
552
+ has(node) {
553
+ return this.entries.has(node.id);
554
+ }
555
+ get size() {
556
+ return this.entries.size;
557
+ }
558
+ [Symbol.iterator]() {
559
+ return this.entries.values();
560
+ }
561
+ };
562
+
563
+ // src/reading.ts
564
+ function writtenReading(value, range) {
565
+ return { kind: "written", value, range };
566
+ }
567
+ var absentReading = { kind: "absent" };
568
+ function unreadableReading(reason, range) {
569
+ return { kind: "unreadable", reason, range };
570
+ }
571
+ function ambiguousReading(candidates, reason, range) {
572
+ return { kind: "ambiguous", candidates, reason, range };
573
+ }
574
+ function dispatchReading(table, reading) {
575
+ return table[reading.kind](reading);
576
+ }
577
+ function mapReading(reading, f) {
578
+ const table = {
579
+ written: (r) => writtenReading(f(r.value), r.range),
580
+ absent: () => absentReading,
581
+ unreadable: (r) => unreadableReading(r.reason, r.range),
582
+ ambiguous: (r) => ambiguousReading(r.candidates.map(f), r.reason, r.range)
583
+ };
584
+ return dispatchReading(table, reading);
585
+ }
586
+ function andThenReading(reading, f) {
587
+ const table = {
588
+ written: (r) => f(r.value, r.range),
589
+ absent: () => absentReading,
590
+ unreadable: (r) => unreadableReading(r.reason, r.range),
591
+ ambiguous: (r) => ambiguousReading(
592
+ r.candidates.flatMap((candidate) => {
593
+ const next = f(candidate, r.range);
594
+ return next.kind === "written" ? [next.value] : [];
595
+ }),
596
+ r.reason,
597
+ r.range
598
+ )
599
+ };
600
+ return dispatchReading(table, reading);
601
+ }
602
+ function firstWrittenReading(readings) {
603
+ const written = readings.find((reading) => reading.kind === "written");
604
+ const chosen = written ?? readings.find((reading) => reading.kind !== "absent") ?? absentReading;
605
+ return {
606
+ reading: chosen,
607
+ passedOver: readings.filter(
608
+ (reading) => reading !== chosen && (reading.kind === "unreadable" || reading.kind === "ambiguous")
609
+ )
610
+ };
611
+ }
612
+ function valueToReadFurtherFrom(reading) {
613
+ return reading.kind === "written" ? reading.value : null;
614
+ }
615
+
103
616
  // src/index.ts
104
617
  function valuesOfOutput(output) {
105
618
  if (output.type === "response" && output.statusCode !== null) {
@@ -115,8 +628,69 @@ function makeTransitionId(functionName, branch) {
115
628
  const conditionHash = createHash("sha1").update(conditionSig).digest("hex").slice(0, 7);
116
629
  return `${functionName}:${terminal.kind}:${statusKey}:${conditionHash}`;
117
630
  }
631
+ function collapseReading(table, reading) {
632
+ return table[reading.kind](reading);
633
+ }
634
+ function claimedValue(defaulted) {
635
+ const table = {
636
+ written: (r) => r.value,
637
+ absent: () => defaulted.libraryDefault ?? null,
638
+ unreadable: () => null,
639
+ ambiguous: () => null
640
+ };
641
+ return collapseReading(table, defaulted.reading);
642
+ }
643
+ function unreadReasonOf(reading) {
644
+ const table = {
645
+ written: () => null,
646
+ absent: () => null,
647
+ unreadable: (r) => r.reason,
648
+ ambiguous: (r) => r.reason
649
+ };
650
+ return collapseReading(table, reading);
651
+ }
652
+ function bodyWithClaimedShape(terminal, reading) {
653
+ return {
654
+ typeText: terminal.body?.typeText ?? null,
655
+ shape: claimedValue(reading)
656
+ };
657
+ }
658
+ function branchWithCollapsedReadings(branch) {
659
+ const { statusCodeReading, bodyShapeReading } = branch;
660
+ if (statusCodeReading === void 0 && bodyShapeReading === void 0) {
661
+ return branch;
662
+ }
663
+ const status = statusCodeReading !== void 0 ? claimedValue(statusCodeReading) : null;
664
+ return {
665
+ ...branch,
666
+ terminal: {
667
+ ...branch.terminal,
668
+ ...statusCodeReading !== void 0 ? {
669
+ statusCode: status !== null ? { type: "literal", value: status } : null
670
+ } : {},
671
+ ...bodyShapeReading !== void 0 ? { body: bodyWithClaimedShape(branch.terminal, bodyShapeReading) } : {}
672
+ }
673
+ };
674
+ }
675
+ function readingsOfBranch(branch) {
676
+ return [
677
+ ...branch.bodyShapeReading !== void 0 ? [branch.bodyShapeReading.reading] : [],
678
+ ...branch.statusCodeReading !== void 0 ? [branch.statusCodeReading.reading] : []
679
+ ];
680
+ }
681
+ function unreadSentences(raw) {
682
+ const handedOver = [
683
+ ...raw.readings ?? [],
684
+ ...raw.branches.flatMap(readingsOfBranch)
685
+ ];
686
+ return [
687
+ ...raw.unreadBinding !== void 0 ? [raw.unreadBinding] : [],
688
+ ...handedOver.map(unreadReasonOf).filter((reason) => reason !== null)
689
+ ];
690
+ }
118
691
  function assembleSummary(raw, options = DEFAULT_OPTIONS) {
119
- const transitions = raw.branches.map((branch) => {
692
+ const transitions = raw.branches.map((rawBranch) => {
693
+ const branch = branchWithCollapsedReadings(rawBranch);
120
694
  const conditions = branch.conditions.map(
121
695
  rawConditionToPredicate
122
696
  );
@@ -153,6 +727,7 @@ function assembleSummary(raw, options = DEFAULT_OPTIONS) {
153
727
  },
154
728
  identity: {
155
729
  name: raw.identity.name,
730
+ ...raw.identity.nameKind !== void 0 ? { nameKind: raw.identity.nameKind } : {},
156
731
  exportPath: raw.identity.exportPath,
157
732
  boundaryBinding: raw.boundaryBinding ?? null,
158
733
  ...raw.deployableUnit !== void 0 ? { deployableUnit: raw.deployableUnit } : {}
@@ -167,21 +742,25 @@ function assembleSummary(raw, options = DEFAULT_OPTIONS) {
167
742
  };
168
743
  }
169
744
  function buildMetadata(raw) {
170
- const metadata = {};
171
- const http = buildHttpMetadata(raw);
745
+ let metadata = {};
746
+ const http = buildHttpMetadataValue(raw);
172
747
  if (http !== null) {
173
- metadata.http = http;
748
+ metadata = withHttpMetadata(metadata, http);
174
749
  }
175
- const graphql = buildGraphqlMetadata(raw);
750
+ const graphql = buildGraphqlMetadataValue(raw);
176
751
  if (graphql !== null) {
177
- metadata.graphql = graphql;
752
+ metadata = withGraphqlMetadata(metadata, graphql);
178
753
  }
179
754
  return Object.keys(metadata).length > 0 ? metadata : null;
180
755
  }
181
- function buildHttpMetadata(raw) {
756
+ function buildHttpMetadataValue(raw) {
182
757
  const http = {};
183
758
  if (raw.declaredContract !== null) {
184
- http.declaredContract = raw.declaredContract;
759
+ http.declaredContract = {
760
+ framework: raw.declaredContract.framework,
761
+ responses: raw.declaredContract.responses,
762
+ provenance: raw.declaredContract.provenance ?? "independent"
763
+ };
185
764
  }
186
765
  if (raw.bodyAccessors !== void 0 && raw.bodyAccessors.length > 0) {
187
766
  http.bodyAccessors = raw.bodyAccessors;
@@ -191,7 +770,7 @@ function buildHttpMetadata(raw) {
191
770
  }
192
771
  return Object.keys(http).length > 0 ? http : null;
193
772
  }
194
- function buildGraphqlMetadata(raw) {
773
+ function buildGraphqlMetadataValue(raw) {
195
774
  const graphql = {};
196
775
  if (raw.graphqlDocument !== void 0) {
197
776
  graphql.document = raw.graphqlDocument;
@@ -207,6 +786,12 @@ function buildGraphqlMetadata(raw) {
207
786
  }
208
787
  return Object.keys(graphql).length > 0 ? graphql : null;
209
788
  }
789
+ function answersWithHttpResponses(raw) {
790
+ if (raw.boundaryBinding === null || raw.boundaryBinding === void 0) {
791
+ return true;
792
+ }
793
+ return exchangesHttpResponses(raw.boundaryBinding);
794
+ }
210
795
  function detectGaps(raw, transitions, options) {
211
796
  if (options.gapHandling === "silent") {
212
797
  return [];
@@ -215,20 +800,18 @@ function detectGaps(raw, transitions, options) {
215
800
  if (raw.unmatchedReturns !== void 0 && raw.unmatchedReturns > 0) {
216
801
  const count = raw.unmatchedReturns;
217
802
  gaps.push({
218
- // Not a contract violation: the handler may be answering
219
- // correctly, in a shape nobody taught the pack.
220
803
  type: "unreadOutcome",
221
804
  conditions: [],
222
805
  consequence: "unknown",
223
806
  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`
224
807
  });
225
808
  }
226
- if (raw.unreadBinding !== void 0) {
809
+ for (const sentence of unreadSentences(raw)) {
227
810
  gaps.push({
228
811
  type: "unreadOutcome",
229
812
  conditions: [],
230
813
  consequence: "unknown",
231
- description: raw.unreadBinding
814
+ description: sentence
232
815
  });
233
816
  }
234
817
  const unreadBody = describeUnreadBody(raw);
@@ -240,7 +823,7 @@ function detectGaps(raw, transitions, options) {
240
823
  description: unreadBody
241
824
  });
242
825
  }
243
- if (raw.declaredContract) {
826
+ if (raw.declaredContract && answersWithHttpResponses(raw)) {
244
827
  const producedStatuses = new Set(
245
828
  transitions.flatMap((t) => {
246
829
  if (t.output.type !== "response") {
@@ -419,12 +1002,27 @@ function paramToInput(param) {
419
1002
  };
420
1003
  }
421
1004
  export {
1005
+ IdMap,
1006
+ IdSet,
1007
+ PathBudgetExceeded,
1008
+ UnmodeledFlow,
1009
+ absentReading,
1010
+ ambiguousReading,
1011
+ andThenReading,
422
1012
  assembleSummary,
423
1013
  assessConfidence,
424
1014
  detectGaps,
425
1015
  effectToIR,
1016
+ enumerateOrDegrade,
1017
+ enumerateStructuredPaths,
1018
+ firstWrittenReading,
426
1019
  httpRouteDiscovery,
427
1020
  makeTransitionId,
1021
+ mapReading,
428
1022
  paramToInput,
429
- terminalToOutput
1023
+ sharedGatingConditions,
1024
+ terminalToOutput,
1025
+ unreadableReading,
1026
+ valueToReadFurtherFrom,
1027
+ writtenReading
430
1028
  };