@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.cjs CHANGED
@@ -20,17 +20,33 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
+ IdMap: () => IdMap,
24
+ IdSet: () => IdSet,
25
+ PathBudgetExceeded: () => PathBudgetExceeded,
26
+ UnmodeledFlow: () => UnmodeledFlow,
27
+ absentReading: () => absentReading,
28
+ ambiguousReading: () => ambiguousReading,
29
+ andThenReading: () => andThenReading,
23
30
  assembleSummary: () => assembleSummary,
24
31
  assessConfidence: () => assessConfidence,
25
32
  detectGaps: () => detectGaps,
26
33
  effectToIR: () => effectToIR,
34
+ enumerateOrDegrade: () => enumerateOrDegrade,
35
+ enumerateStructuredPaths: () => enumerateStructuredPaths,
36
+ firstWrittenReading: () => firstWrittenReading,
27
37
  httpRouteDiscovery: () => httpRouteDiscovery,
28
38
  makeTransitionId: () => makeTransitionId,
39
+ mapReading: () => mapReading,
29
40
  paramToInput: () => paramToInput,
30
- terminalToOutput: () => terminalToOutput
41
+ sharedGatingConditions: () => sharedGatingConditions,
42
+ terminalToOutput: () => terminalToOutput,
43
+ unreadableReading: () => unreadableReading,
44
+ valueToReadFurtherFrom: () => valueToReadFurtherFrom,
45
+ writtenReading: () => writtenReading
31
46
  });
32
47
  module.exports = __toCommonJS(index_exports);
33
48
  var import_node_crypto = require("crypto");
49
+ var import_behavioral_ir = require("@suss/behavioral-ir");
34
50
 
35
51
  // src/inputReads.ts
36
52
  function inputReadsOf(args) {
@@ -124,13 +140,521 @@ function httpRouteDiscovery(opts) {
124
140
  registrationChain: [...opts.methods]
125
141
  },
126
142
  bindingExtraction: {
127
- method: { type: "fromRegistration", position: "methodName" },
143
+ // `.all` registers every method, so it records "*", which the pairing
144
+ // engine treats as agreeing with any method at all.
145
+ method: {
146
+ type: "fromRegistration",
147
+ position: "methodName",
148
+ nameMap: { all: "*" }
149
+ },
128
150
  path: { type: "fromRegistration", position: 0 }
129
151
  },
152
+ ...opts.mount !== void 0 ? { mount: opts.mount } : {},
130
153
  requiresImport: [opts.importModule]
131
154
  }));
132
155
  }
133
156
 
157
+ // src/paths/enumeratePaths.ts
158
+ var MAX_PATHS = 256;
159
+ var UnmodeledFlow = class extends Error {
160
+ };
161
+ var PathBudgetExceeded = class extends Error {
162
+ };
163
+ function branchCond(branchStmt, condition, polarity, oppositeExit) {
164
+ return {
165
+ info: {
166
+ sourceText: condition.sourceText,
167
+ polarity,
168
+ source: "explicit",
169
+ expression: condition.expression
170
+ },
171
+ branchStmt,
172
+ oppositeExit
173
+ };
174
+ }
175
+ function fixedCond(condition, polarity, source) {
176
+ return {
177
+ info: {
178
+ sourceText: condition.sourceText,
179
+ polarity,
180
+ source,
181
+ expression: condition.expression
182
+ },
183
+ branchStmt: null,
184
+ oppositeExit: null
185
+ };
186
+ }
187
+ var loopIterationCond = (headerText) => fixedCond(
188
+ { sourceText: `some iteration of: ${headerText}`, expression: null },
189
+ "positive",
190
+ "explicit"
191
+ );
192
+ var loopCompletedCond = (headerText, exit) => fixedCond(
193
+ {
194
+ sourceText: `loop exited via ${exit ?? "return"}: ${headerText}`,
195
+ expression: null
196
+ },
197
+ "negative",
198
+ exit === "throw" ? "earlyThrow" : "earlyReturn"
199
+ );
200
+ var catchEntryCond = () => fixedCond(
201
+ { sourceText: "catch", expression: null },
202
+ "positive",
203
+ "catchBlock"
204
+ );
205
+ function classify(path, home, isAncestorOrSelf) {
206
+ return path.map((cond) => {
207
+ if (cond.branchStmt === null) {
208
+ return cond.info;
209
+ }
210
+ const encloses = home !== null && isAncestorOrSelf(cond.branchStmt, home);
211
+ if (encloses || cond.info.polarity === "positive") {
212
+ return { ...cond.info, source: "explicit" };
213
+ }
214
+ const source = cond.oppositeExit === "throw" ? "earlyThrow" : "earlyReturn";
215
+ return { ...cond.info, source };
216
+ });
217
+ }
218
+ function childrenOf(stmt) {
219
+ if (stmt.kind === "if") {
220
+ return stmt.elseBody === null ? stmt.thenBody : [...stmt.thenBody, ...stmt.elseBody];
221
+ }
222
+ if (stmt.kind === "switch") {
223
+ return stmt.groups.flatMap((g) => g.body);
224
+ }
225
+ if (stmt.kind === "loop") {
226
+ return stmt.body;
227
+ }
228
+ if (stmt.kind === "try") {
229
+ return [
230
+ ...stmt.tryBody,
231
+ ...stmt.catchBody ?? [],
232
+ ...stmt.finallyBody ?? []
233
+ ];
234
+ }
235
+ return [];
236
+ }
237
+ function hasStrayBreak(nodes) {
238
+ return nodes.some((node) => {
239
+ if (node.kind === "exit" && node.exit === "break") {
240
+ return true;
241
+ }
242
+ if (node.kind === "switch" || node.kind === "loop") {
243
+ return false;
244
+ }
245
+ return hasStrayBreak(childrenOf(node));
246
+ });
247
+ }
248
+ function exitKindOfList(stmts) {
249
+ let sawReturn = false;
250
+ for (const stmt of stmts) {
251
+ if (stmt.exitKind === "throw") {
252
+ return "throw";
253
+ }
254
+ if (stmt.exitKind === "return") {
255
+ sawReturn = true;
256
+ }
257
+ }
258
+ return sawReturn ? "return" : null;
259
+ }
260
+ function chargeBudget(state) {
261
+ state.pathCount++;
262
+ if (state.pathCount > MAX_PATHS) {
263
+ throw new PathBudgetExceeded();
264
+ }
265
+ }
266
+ function recordTerminal(ctx, terminal, home, path) {
267
+ chargeBudget(ctx.state);
268
+ const info = classify(path, home, ctx.isAncestorOrSelf);
269
+ const existing = ctx.state.byTerminal.get(terminal) ?? [];
270
+ existing.push(info);
271
+ ctx.state.byTerminal.set(terminal, existing);
272
+ }
273
+ function containsTerminal(ctx, stmt) {
274
+ if ((ctx.terminalsByStmt.get(stmt)?.length ?? 0) > 0) {
275
+ return true;
276
+ }
277
+ return childrenOf(stmt).some((child) => containsTerminal(ctx, child));
278
+ }
279
+ function stepIf(stmt, path, ctx) {
280
+ const armsExit = stmt.exitKind !== null;
281
+ const armsHaveTerminals = containsTerminal(ctx, stmt);
282
+ if (!armsExit && !armsHaveTerminals) {
283
+ return [path];
284
+ }
285
+ const thenExit = exitKindOfList(stmt.thenBody);
286
+ const elseExit = stmt.elseBody === null ? null : exitKindOfList(stmt.elseBody);
287
+ const thenPath = [
288
+ ...path,
289
+ branchCond(stmt, stmt.condition, "positive", elseExit)
290
+ ];
291
+ const elsePath = [
292
+ ...path,
293
+ branchCond(stmt, stmt.condition, "negative", thenExit)
294
+ ];
295
+ const out = [];
296
+ out.push(...enumerate(ctx, stmt.thenBody, thenPath));
297
+ if (stmt.elseBody !== null) {
298
+ out.push(...enumerate(ctx, stmt.elseBody, elsePath));
299
+ } else {
300
+ out.push(elsePath);
301
+ }
302
+ return out;
303
+ }
304
+ function stepSwitch(stmt, path, ctx) {
305
+ const out = [];
306
+ const negations = [];
307
+ let defaultRanBody = false;
308
+ for (let i = 0; i < stmt.groups.length; i++) {
309
+ const group = stmt.groups[i];
310
+ const fallthroughSafe = i === stmt.groups.length - 1;
311
+ if (hasStrayBreak(group.body)) {
312
+ throw new UnmodeledFlow("non-trailing break in switch clause");
313
+ }
314
+ const groupPath = group.condition === null ? [...path, ...negations] : [...path, fixedCond(group.condition, "positive", "explicit")];
315
+ const conts = enumerate(ctx, group.body, groupPath);
316
+ if (conts.length > 0) {
317
+ if (!group.hasTrailingBreak && !fallthroughSafe) {
318
+ throw new UnmodeledFlow("fallthrough into a non-empty switch clause");
319
+ }
320
+ out.push(...conts);
321
+ }
322
+ if (group.condition === null) {
323
+ defaultRanBody = true;
324
+ } else {
325
+ const exit = exitKindOfList(group.body);
326
+ const negationSource = exit === "throw" ? "earlyThrow" : exit === "return" ? "earlyReturn" : "explicit";
327
+ negations.push(fixedCond(group.condition, "negative", negationSource));
328
+ }
329
+ }
330
+ if (!defaultRanBody) {
331
+ out.push([...path, ...negations]);
332
+ }
333
+ return out;
334
+ }
335
+ function stepLoop(stmt, path, ctx) {
336
+ const bodyExit = exitKindOfList(stmt.body);
337
+ if (containsTerminal(ctx, stmt)) {
338
+ enumerate(ctx, stmt.body, [
339
+ ...path,
340
+ loopIterationCond(stmt.condition.sourceText)
341
+ ]);
342
+ }
343
+ if (bodyExit !== null) {
344
+ return [[...path, loopCompletedCond(stmt.condition.sourceText, bodyExit)]];
345
+ }
346
+ return [path];
347
+ }
348
+ function stepTry(stmt, path, ctx) {
349
+ if (stmt.finallyBody !== null) {
350
+ const finallyHasTerminal = stmt.finallyBody.some(
351
+ (s) => containsTerminal(ctx, s)
352
+ );
353
+ if (exitKindOfList(stmt.finallyBody) !== null || finallyHasTerminal) {
354
+ throw new UnmodeledFlow("finally block with exits or terminals");
355
+ }
356
+ }
357
+ const out = [];
358
+ out.push(...enumerate(ctx, stmt.tryBody, path));
359
+ if (stmt.catchBody !== null) {
360
+ const catchPath = [...path, catchEntryCond()];
361
+ out.push(...enumerate(ctx, stmt.catchBody, catchPath));
362
+ }
363
+ return out;
364
+ }
365
+ function stepExit(_stmt, _path, _ctx) {
366
+ return [];
367
+ }
368
+ function stepOpaque(_stmt, path, _ctx) {
369
+ return [path];
370
+ }
371
+ var stepHandlers = {
372
+ if: stepIf,
373
+ switch: stepSwitch,
374
+ loop: stepLoop,
375
+ try: stepTry,
376
+ exit: stepExit,
377
+ opaque: stepOpaque
378
+ };
379
+ function stepStatement(ctx, stmt, path) {
380
+ const ownTerminals = ctx.terminalsByStmt.get(stmt) ?? [];
381
+ for (const terminal of ownTerminals) {
382
+ recordTerminal(ctx, terminal, stmt, path);
383
+ }
384
+ const handler = stepHandlers[stmt.kind];
385
+ return handler(
386
+ stmt,
387
+ path,
388
+ ctx
389
+ );
390
+ }
391
+ function soleDisagreement(left, right) {
392
+ if (left.length !== right.length) {
393
+ return null;
394
+ }
395
+ let found = null;
396
+ for (let at = 0; at < left.length; at++) {
397
+ const one = left[at];
398
+ const other = right[at];
399
+ if (one === other) {
400
+ continue;
401
+ }
402
+ if (found !== null) {
403
+ return null;
404
+ }
405
+ if (one === void 0 || other === void 0 || one.branchStmt === null || one.branchStmt !== other.branchStmt || one.info.polarity === other.info.polarity) {
406
+ return null;
407
+ }
408
+ found = at;
409
+ }
410
+ return found;
411
+ }
412
+ function mergeRejoined(paths) {
413
+ let current = paths;
414
+ for (let round = 0; round < current.length; round++) {
415
+ const out = [];
416
+ const taken = /* @__PURE__ */ new Set();
417
+ let mergedAny = false;
418
+ for (let i = 0; i < current.length; i++) {
419
+ if (taken.has(i)) {
420
+ continue;
421
+ }
422
+ let combined = current[i];
423
+ for (let j = i + 1; j < current.length; j++) {
424
+ if (taken.has(j)) {
425
+ continue;
426
+ }
427
+ const at = soleDisagreement(combined, current[j]);
428
+ if (at === null) {
429
+ continue;
430
+ }
431
+ combined = [...combined.slice(0, at), ...combined.slice(at + 1)];
432
+ taken.add(j);
433
+ mergedAny = true;
434
+ break;
435
+ }
436
+ out.push(combined);
437
+ }
438
+ current = out;
439
+ if (!mergedAny) {
440
+ break;
441
+ }
442
+ }
443
+ return current;
444
+ }
445
+ function enumerate(ctx, stmts, prefix) {
446
+ let frontiers = [[...prefix]];
447
+ for (const stmt of stmts) {
448
+ const nextFrontiers = [];
449
+ for (const path of frontiers) {
450
+ nextFrontiers.push(...stepStatement(ctx, stmt, path));
451
+ }
452
+ frontiers = mergeRejoined(nextFrontiers);
453
+ if (frontiers.length > MAX_PATHS) {
454
+ throw new PathBudgetExceeded();
455
+ }
456
+ if (frontiers.length === 0) {
457
+ break;
458
+ }
459
+ }
460
+ return frontiers;
461
+ }
462
+ function buildParentMap(statements) {
463
+ const parentOf = /* @__PURE__ */ new Map();
464
+ const visit = (stmt, parent) => {
465
+ parentOf.set(stmt, parent);
466
+ for (const child of childrenOf(stmt)) {
467
+ visit(child, stmt);
468
+ }
469
+ };
470
+ for (const stmt of statements) {
471
+ visit(stmt, null);
472
+ }
473
+ return parentOf;
474
+ }
475
+ function enumerateStructuredPaths(input) {
476
+ const parentOf = buildParentMap(input.statements);
477
+ const isAncestorOrSelf = (a, b) => {
478
+ let current = b;
479
+ while (current !== null) {
480
+ if (current === a) {
481
+ return true;
482
+ }
483
+ current = parentOf.get(current) ?? null;
484
+ }
485
+ return false;
486
+ };
487
+ const state = {
488
+ byTerminal: /* @__PURE__ */ new Map(),
489
+ fallthrough: [],
490
+ pathCount: 0
491
+ };
492
+ const ctx = {
493
+ terminalsByStmt: input.terminalsByStmt,
494
+ state,
495
+ isAncestorOrSelf
496
+ };
497
+ const continuations = enumerate(ctx, input.statements, []);
498
+ for (const path of continuations) {
499
+ chargeBudget(state);
500
+ state.fallthrough.push(classify(path, null, isAncestorOrSelf));
501
+ }
502
+ return { byTerminal: state.byTerminal, fallthrough: state.fallthrough };
503
+ }
504
+ function enumerateOrDegrade(input, terminals) {
505
+ try {
506
+ return { ...enumerateStructuredPaths(input), degraded: null };
507
+ } catch (error) {
508
+ const reason = reasonToDegrade(error);
509
+ if (reason === null) {
510
+ throw error;
511
+ }
512
+ const marker = {
513
+ sourceText: `unmodeled control flow (${reason})`,
514
+ polarity: "positive",
515
+ source: "explicit",
516
+ expression: null
517
+ };
518
+ const byTerminal = /* @__PURE__ */ new Map();
519
+ for (const terminal of terminals) {
520
+ byTerminal.set(terminal, [[marker]]);
521
+ }
522
+ return { byTerminal, fallthrough: [[marker]], degraded: reason };
523
+ }
524
+ }
525
+ function reasonToDegrade(error) {
526
+ if (error instanceof PathBudgetExceeded) {
527
+ return "path budget exceeded";
528
+ }
529
+ if (error instanceof UnmodeledFlow) {
530
+ return error.message;
531
+ }
532
+ return null;
533
+ }
534
+
535
+ // src/paths/gatingConditions.ts
536
+ var keyOf = (condition) => `${condition.polarity}:${condition.sourceText}`;
537
+ function sharedGatingConditions(paths, readPredicate) {
538
+ const [first, ...rest] = paths ?? [];
539
+ if (first === void 0) {
540
+ return [];
541
+ }
542
+ const shared = new Set(first.map(keyOf));
543
+ for (const path of rest) {
544
+ const here = new Set(path.map(keyOf));
545
+ for (const value of shared) {
546
+ if (!here.has(value)) {
547
+ shared.delete(value);
548
+ }
549
+ }
550
+ }
551
+ return first.filter((condition) => shared.has(keyOf(condition))).map((condition) => ({
552
+ sourceText: condition.sourceText,
553
+ structured: readPredicate === void 0 || condition.expression === null ? null : readPredicate(condition.expression),
554
+ polarity: condition.polarity,
555
+ source: condition.source
556
+ }));
557
+ }
558
+
559
+ // src/paths/nodeIdentity.ts
560
+ var IdSet = class {
561
+ byKey = /* @__PURE__ */ new Map();
562
+ constructor(nodes = []) {
563
+ for (const node of nodes) {
564
+ this.add(node);
565
+ }
566
+ }
567
+ add(node) {
568
+ this.byKey.set(node.id, node);
569
+ return this;
570
+ }
571
+ has(node) {
572
+ return this.byKey.has(node.id);
573
+ }
574
+ /** The node this set was built with, which is the caller's own handle for it. */
575
+ get(node) {
576
+ return this.byKey.get(node.id);
577
+ }
578
+ get size() {
579
+ return this.byKey.size;
580
+ }
581
+ [Symbol.iterator]() {
582
+ return this.byKey.values();
583
+ }
584
+ };
585
+ var IdMap = class {
586
+ entries = /* @__PURE__ */ new Map();
587
+ set(node, value) {
588
+ this.entries.set(node.id, [node, value]);
589
+ return this;
590
+ }
591
+ get(node) {
592
+ return this.entries.get(node.id)?.[1];
593
+ }
594
+ has(node) {
595
+ return this.entries.has(node.id);
596
+ }
597
+ get size() {
598
+ return this.entries.size;
599
+ }
600
+ [Symbol.iterator]() {
601
+ return this.entries.values();
602
+ }
603
+ };
604
+
605
+ // src/reading.ts
606
+ function writtenReading(value, range) {
607
+ return { kind: "written", value, range };
608
+ }
609
+ var absentReading = { kind: "absent" };
610
+ function unreadableReading(reason, range) {
611
+ return { kind: "unreadable", reason, range };
612
+ }
613
+ function ambiguousReading(candidates, reason, range) {
614
+ return { kind: "ambiguous", candidates, reason, range };
615
+ }
616
+ function dispatchReading(table, reading) {
617
+ return table[reading.kind](reading);
618
+ }
619
+ function mapReading(reading, f) {
620
+ const table = {
621
+ written: (r) => writtenReading(f(r.value), r.range),
622
+ absent: () => absentReading,
623
+ unreadable: (r) => unreadableReading(r.reason, r.range),
624
+ ambiguous: (r) => ambiguousReading(r.candidates.map(f), r.reason, r.range)
625
+ };
626
+ return dispatchReading(table, reading);
627
+ }
628
+ function andThenReading(reading, f) {
629
+ const table = {
630
+ written: (r) => f(r.value, r.range),
631
+ absent: () => absentReading,
632
+ unreadable: (r) => unreadableReading(r.reason, r.range),
633
+ ambiguous: (r) => ambiguousReading(
634
+ r.candidates.flatMap((candidate) => {
635
+ const next = f(candidate, r.range);
636
+ return next.kind === "written" ? [next.value] : [];
637
+ }),
638
+ r.reason,
639
+ r.range
640
+ )
641
+ };
642
+ return dispatchReading(table, reading);
643
+ }
644
+ function firstWrittenReading(readings) {
645
+ const written = readings.find((reading) => reading.kind === "written");
646
+ const chosen = written ?? readings.find((reading) => reading.kind !== "absent") ?? absentReading;
647
+ return {
648
+ reading: chosen,
649
+ passedOver: readings.filter(
650
+ (reading) => reading !== chosen && (reading.kind === "unreadable" || reading.kind === "ambiguous")
651
+ )
652
+ };
653
+ }
654
+ function valueToReadFurtherFrom(reading) {
655
+ return reading.kind === "written" ? reading.value : null;
656
+ }
657
+
134
658
  // src/index.ts
135
659
  function valuesOfOutput(output) {
136
660
  if (output.type === "response" && output.statusCode !== null) {
@@ -146,8 +670,69 @@ function makeTransitionId(functionName, branch) {
146
670
  const conditionHash = (0, import_node_crypto.createHash)("sha1").update(conditionSig).digest("hex").slice(0, 7);
147
671
  return `${functionName}:${terminal.kind}:${statusKey}:${conditionHash}`;
148
672
  }
673
+ function collapseReading(table, reading) {
674
+ return table[reading.kind](reading);
675
+ }
676
+ function claimedValue(defaulted) {
677
+ const table = {
678
+ written: (r) => r.value,
679
+ absent: () => defaulted.libraryDefault ?? null,
680
+ unreadable: () => null,
681
+ ambiguous: () => null
682
+ };
683
+ return collapseReading(table, defaulted.reading);
684
+ }
685
+ function unreadReasonOf(reading) {
686
+ const table = {
687
+ written: () => null,
688
+ absent: () => null,
689
+ unreadable: (r) => r.reason,
690
+ ambiguous: (r) => r.reason
691
+ };
692
+ return collapseReading(table, reading);
693
+ }
694
+ function bodyWithClaimedShape(terminal, reading) {
695
+ return {
696
+ typeText: terminal.body?.typeText ?? null,
697
+ shape: claimedValue(reading)
698
+ };
699
+ }
700
+ function branchWithCollapsedReadings(branch) {
701
+ const { statusCodeReading, bodyShapeReading } = branch;
702
+ if (statusCodeReading === void 0 && bodyShapeReading === void 0) {
703
+ return branch;
704
+ }
705
+ const status = statusCodeReading !== void 0 ? claimedValue(statusCodeReading) : null;
706
+ return {
707
+ ...branch,
708
+ terminal: {
709
+ ...branch.terminal,
710
+ ...statusCodeReading !== void 0 ? {
711
+ statusCode: status !== null ? { type: "literal", value: status } : null
712
+ } : {},
713
+ ...bodyShapeReading !== void 0 ? { body: bodyWithClaimedShape(branch.terminal, bodyShapeReading) } : {}
714
+ }
715
+ };
716
+ }
717
+ function readingsOfBranch(branch) {
718
+ return [
719
+ ...branch.bodyShapeReading !== void 0 ? [branch.bodyShapeReading.reading] : [],
720
+ ...branch.statusCodeReading !== void 0 ? [branch.statusCodeReading.reading] : []
721
+ ];
722
+ }
723
+ function unreadSentences(raw) {
724
+ const handedOver = [
725
+ ...raw.readings ?? [],
726
+ ...raw.branches.flatMap(readingsOfBranch)
727
+ ];
728
+ return [
729
+ ...raw.unreadBinding !== void 0 ? [raw.unreadBinding] : [],
730
+ ...handedOver.map(unreadReasonOf).filter((reason) => reason !== null)
731
+ ];
732
+ }
149
733
  function assembleSummary(raw, options = DEFAULT_OPTIONS) {
150
- const transitions = raw.branches.map((branch) => {
734
+ const transitions = raw.branches.map((rawBranch) => {
735
+ const branch = branchWithCollapsedReadings(rawBranch);
151
736
  const conditions = branch.conditions.map(
152
737
  rawConditionToPredicate
153
738
  );
@@ -184,6 +769,7 @@ function assembleSummary(raw, options = DEFAULT_OPTIONS) {
184
769
  },
185
770
  identity: {
186
771
  name: raw.identity.name,
772
+ ...raw.identity.nameKind !== void 0 ? { nameKind: raw.identity.nameKind } : {},
187
773
  exportPath: raw.identity.exportPath,
188
774
  boundaryBinding: raw.boundaryBinding ?? null,
189
775
  ...raw.deployableUnit !== void 0 ? { deployableUnit: raw.deployableUnit } : {}
@@ -198,21 +784,25 @@ function assembleSummary(raw, options = DEFAULT_OPTIONS) {
198
784
  };
199
785
  }
200
786
  function buildMetadata(raw) {
201
- const metadata = {};
202
- const http = buildHttpMetadata(raw);
787
+ let metadata = {};
788
+ const http = buildHttpMetadataValue(raw);
203
789
  if (http !== null) {
204
- metadata.http = http;
790
+ metadata = (0, import_behavioral_ir.withHttpMetadata)(metadata, http);
205
791
  }
206
- const graphql = buildGraphqlMetadata(raw);
792
+ const graphql = buildGraphqlMetadataValue(raw);
207
793
  if (graphql !== null) {
208
- metadata.graphql = graphql;
794
+ metadata = (0, import_behavioral_ir.withGraphqlMetadata)(metadata, graphql);
209
795
  }
210
796
  return Object.keys(metadata).length > 0 ? metadata : null;
211
797
  }
212
- function buildHttpMetadata(raw) {
798
+ function buildHttpMetadataValue(raw) {
213
799
  const http = {};
214
800
  if (raw.declaredContract !== null) {
215
- http.declaredContract = raw.declaredContract;
801
+ http.declaredContract = {
802
+ framework: raw.declaredContract.framework,
803
+ responses: raw.declaredContract.responses,
804
+ provenance: raw.declaredContract.provenance ?? "independent"
805
+ };
216
806
  }
217
807
  if (raw.bodyAccessors !== void 0 && raw.bodyAccessors.length > 0) {
218
808
  http.bodyAccessors = raw.bodyAccessors;
@@ -222,7 +812,7 @@ function buildHttpMetadata(raw) {
222
812
  }
223
813
  return Object.keys(http).length > 0 ? http : null;
224
814
  }
225
- function buildGraphqlMetadata(raw) {
815
+ function buildGraphqlMetadataValue(raw) {
226
816
  const graphql = {};
227
817
  if (raw.graphqlDocument !== void 0) {
228
818
  graphql.document = raw.graphqlDocument;
@@ -238,6 +828,12 @@ function buildGraphqlMetadata(raw) {
238
828
  }
239
829
  return Object.keys(graphql).length > 0 ? graphql : null;
240
830
  }
831
+ function answersWithHttpResponses(raw) {
832
+ if (raw.boundaryBinding === null || raw.boundaryBinding === void 0) {
833
+ return true;
834
+ }
835
+ return (0, import_behavioral_ir.exchangesHttpResponses)(raw.boundaryBinding);
836
+ }
241
837
  function detectGaps(raw, transitions, options) {
242
838
  if (options.gapHandling === "silent") {
243
839
  return [];
@@ -246,20 +842,18 @@ function detectGaps(raw, transitions, options) {
246
842
  if (raw.unmatchedReturns !== void 0 && raw.unmatchedReturns > 0) {
247
843
  const count = raw.unmatchedReturns;
248
844
  gaps.push({
249
- // Not a contract violation: the handler may be answering
250
- // correctly, in a shape nobody taught the pack.
251
845
  type: "unreadOutcome",
252
846
  conditions: [],
253
847
  consequence: "unknown",
254
848
  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`
255
849
  });
256
850
  }
257
- if (raw.unreadBinding !== void 0) {
851
+ for (const sentence of unreadSentences(raw)) {
258
852
  gaps.push({
259
853
  type: "unreadOutcome",
260
854
  conditions: [],
261
855
  consequence: "unknown",
262
- description: raw.unreadBinding
856
+ description: sentence
263
857
  });
264
858
  }
265
859
  const unreadBody = describeUnreadBody(raw);
@@ -271,7 +865,7 @@ function detectGaps(raw, transitions, options) {
271
865
  description: unreadBody
272
866
  });
273
867
  }
274
- if (raw.declaredContract) {
868
+ if (raw.declaredContract && answersWithHttpResponses(raw)) {
275
869
  const producedStatuses = new Set(
276
870
  transitions.flatMap((t) => {
277
871
  if (t.output.type !== "response") {
@@ -451,12 +1045,27 @@ function paramToInput(param) {
451
1045
  }
452
1046
  // Annotate the CommonJS export names for ESM import in node:
453
1047
  0 && (module.exports = {
1048
+ IdMap,
1049
+ IdSet,
1050
+ PathBudgetExceeded,
1051
+ UnmodeledFlow,
1052
+ absentReading,
1053
+ ambiguousReading,
1054
+ andThenReading,
454
1055
  assembleSummary,
455
1056
  assessConfidence,
456
1057
  detectGaps,
457
1058
  effectToIR,
1059
+ enumerateOrDegrade,
1060
+ enumerateStructuredPaths,
1061
+ firstWrittenReading,
458
1062
  httpRouteDiscovery,
459
1063
  makeTransitionId,
1064
+ mapReading,
460
1065
  paramToInput,
461
- terminalToOutput
1066
+ sharedGatingConditions,
1067
+ terminalToOutput,
1068
+ unreadableReading,
1069
+ valueToReadFurtherFrom,
1070
+ writtenReading
462
1071
  });