@suss/extractor 0.22.3 → 0.23.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/README.md CHANGED
@@ -75,11 +75,17 @@ What it does not read:
75
75
 
76
76
  - **Anything after the continuation returns.** A middleware that inspects the response on the way back out is read up to the `next()` call and no further.
77
77
  - **A wrapper whose continuation the pack does not declare, or whose call the walk cannot see.** Nothing says where control passes on, so the wrapper's outcomes are reported beside the unit's own rather than around them.
78
- - **A throw the walk never saw.** An error handler composes over the paths that end by throwing, so a route whose 500 comes from a call the walk could not follow still does not report 500.
78
+ - **A throw the walk never saw.** An error handler composes over the paths that end by throwing, so a route whose 500 comes from a call the walk could not follow still does not report 500 as a transition. The contract comparison below is the one place that counts it anyway.
79
79
  - **Registration order against route order.** A wrapper registered after a route still composes into it.
80
80
 
81
81
  Composition multiplies paths, so it is capped by the same `MAX_PATHS` budget path enumeration uses. Past it the two sides are reported side by side and the unit gets a gap saying so.
82
82
 
83
+ ### The declared contract, compared again
84
+
85
+ `assembleSummary` compares a route's declared responses against its own body and records a gap for each status declared but never produced, or produced but never declared. That runs before anything around the route is read, so a contract that declares the 401 the auth middleware returns was reported as declaring a status nothing produces, once per route.
86
+
87
+ `composeWrappers` runs the same comparison over the composed transitions and replaces those gaps. A status a wrapper produces counts as produced, and one the contract leaves out is reported against the wrapper by name: "requireCaller, registered around this handler, produces status 429 which is not declared in the hono contract". An error handler's responses count as produced on every route it covers, whether or not a path through the route was seen to throw, since anything the route calls can throw at runtime. A run with `gapHandling: "silent"` gets no gaps from this step either.
88
+
83
89
  ## Which branch an effect belongs to
84
90
 
85
91
  An adapter finds the calls a body makes once, and it finds the branches separately. Something then has to say which branch each call runs on. `guardsHoldOn` and `runsBefore` in `effectGuards.ts` are that decision, and both adapters that make it call these rather than keeping a copy.
package/dist/index.cjs CHANGED
@@ -90,7 +90,68 @@ __export(index_exports, {
90
90
  });
91
91
  module.exports = __toCommonJS(index_exports);
92
92
  var import_node_crypto3 = require("crypto");
93
- var import_behavioral_ir4 = require("@suss/behavioral-ir");
93
+ var import_behavioral_ir5 = require("@suss/behavioral-ir");
94
+
95
+ // src/contractStatusGaps.ts
96
+ var import_behavioral_ir = require("@suss/behavioral-ir");
97
+ function contractStatusGaps(contract, transitions) {
98
+ const producers = producersByStatus(transitions);
99
+ const declared = new Set(contract.responses.map((r) => r.statusCode));
100
+ const gaps = [];
101
+ for (const status of declared) {
102
+ if (!producers.has(status)) {
103
+ gaps.push({
104
+ type: "unhandledCase",
105
+ conditions: [],
106
+ consequence: "frameworkDefault",
107
+ description: `Declared response ${status} is never produced by the handler`
108
+ });
109
+ }
110
+ }
111
+ for (const [status, wrappers] of producers) {
112
+ if (declared.has(status)) {
113
+ continue;
114
+ }
115
+ gaps.push({
116
+ type: "unhandledCase",
117
+ conditions: [],
118
+ consequence: "unknown",
119
+ description: `${producerOf(wrappers)} produces status ${status} which is not declared in the ${contract.framework} contract`
120
+ });
121
+ }
122
+ return gaps;
123
+ }
124
+ function producersByStatus(transitions) {
125
+ const producers = /* @__PURE__ */ new Map();
126
+ for (const transition of transitions) {
127
+ if (transition.output.type !== "response") {
128
+ continue;
129
+ }
130
+ const status = transition.output.statusCode;
131
+ if (status?.type !== "literal" || typeof status.value !== "number") {
132
+ continue;
133
+ }
134
+ const from = (0, import_behavioral_ir.readWrapperMetadata)(transition)?.from;
135
+ const seen = producers.get(status.value);
136
+ if (seen === void 0) {
137
+ producers.set(
138
+ status.value,
139
+ new Set(from === void 0 ? [] : [from.name])
140
+ );
141
+ } else if (from === void 0) {
142
+ seen.clear();
143
+ } else if (seen.size > 0) {
144
+ seen.add(from.name);
145
+ }
146
+ }
147
+ return producers;
148
+ }
149
+ function producerOf(wrappers) {
150
+ if (wrappers.size === 0) {
151
+ return "Handler";
152
+ }
153
+ return `${[...wrappers].sort().join(" and ")}, registered around this handler,`;
154
+ }
94
155
 
95
156
  // src/inputReads.ts
96
157
  function inputReadsOf(args) {
@@ -183,10 +244,10 @@ function mergeInputReads(derived, syntactic) {
183
244
  }
184
245
 
185
246
  // src/composeWrappers.ts
186
- var import_behavioral_ir2 = require("@suss/behavioral-ir");
247
+ var import_behavioral_ir3 = require("@suss/behavioral-ir");
187
248
 
188
249
  // src/paths/enumeratePaths.ts
189
- var import_behavioral_ir = require("@suss/behavioral-ir");
250
+ var import_behavioral_ir2 = require("@suss/behavioral-ir");
190
251
  var MAX_PATHS = 256;
191
252
  var UnmodeledFlow = class extends Error {
192
253
  };
@@ -230,7 +291,7 @@ var loopCompletedCond = (headerText, exit) => fixedCond(
230
291
  exit === "throw" ? "earlyThrow" : "earlyReturn"
231
292
  );
232
293
  var catchEntryCond = () => fixedCond(
233
- { sourceText: import_behavioral_ir.CATCH_ENTRY_TEXT, expression: null },
294
+ { sourceText: import_behavioral_ir2.CATCH_ENTRY_TEXT, expression: null },
234
295
  "positive",
235
296
  "catchBlock"
236
297
  );
@@ -607,7 +668,7 @@ var BUDGET_GAP = {
607
668
  consequence: "unknown",
608
669
  description: `Composing the wrappers registered around this unit would have gone past the path budget of ${MAX_PATHS}, so what each of them produces is reported beside this unit's own outcomes rather than under the conditions that reach it`
609
670
  };
610
- function composeWrappers(summaries) {
671
+ function composeWrappers(summaries, options = {}) {
611
672
  const byKey = /* @__PURE__ */ new Map();
612
673
  for (const summary of summaries) {
613
674
  const key = summaryKey(summary.location.file, summary.identity.name);
@@ -615,20 +676,21 @@ function composeWrappers(summaries) {
615
676
  byKey.set(key, summary);
616
677
  }
617
678
  }
618
- return summaries.map((summary) => composeOne(summary, byKey));
679
+ const keepGaps = options.gapHandling !== "silent";
680
+ return summaries.map((summary) => composeOne(summary, byKey, keepGaps));
619
681
  }
620
682
  function summaryKey(file, name) {
621
683
  return `${file}::${name}`;
622
684
  }
623
- function composeOne(summary, byKey) {
624
- const recorded = (0, import_behavioral_ir2.readWrapperMetadata)(summary);
685
+ function composeOne(summary, byKey, keepGaps) {
686
+ const recorded = (0, import_behavioral_ir3.readWrapperMetadata)(summary);
625
687
  const applied = recorded?.applied ?? [];
626
688
  const covering = applied.filter(
627
689
  (reference) => coversUnit(reference, summary)
628
690
  );
629
691
  const narrowed = covering.length === applied.length ? summary : {
630
692
  ...summary,
631
- metadata: (0, import_behavioral_ir2.withWrapperMetadata)(summary.metadata, {
693
+ metadata: (0, import_behavioral_ir3.withWrapperMetadata)(summary.metadata, {
632
694
  applied: covering
633
695
  })
634
696
  };
@@ -655,14 +717,45 @@ function composeOne(summary, byKey) {
655
717
  }
656
718
  composition = merge(composition, applyThrowWrapper(wrapper, composition));
657
719
  }
658
- if (composition.transitions === summary.transitions) {
720
+ const transitions = composition.transitions === summary.transitions ? summary.transitions : withDistinctIds(composition.transitions);
721
+ if (!keepGaps) {
722
+ return { ...narrowed, transitions };
723
+ }
724
+ const gaps = withContractStatusGaps(summary, transitions, wrappers);
725
+ if (transitions === summary.transitions && gaps === summary.gaps) {
659
726
  return narrowed;
660
727
  }
661
728
  return {
662
729
  ...narrowed,
663
- transitions: withDistinctIds(composition.transitions),
664
- gaps: composition.degraded ? [...summary.gaps, BUDGET_GAP] : summary.gaps
730
+ transitions,
731
+ gaps: composition.degraded ? [...gaps, BUDGET_GAP] : gaps
732
+ };
733
+ }
734
+ function withContractStatusGaps(summary, composed, wrappers) {
735
+ const contract = (0, import_behavioral_ir3.readHttpMetadata)(summary)?.declaredContract;
736
+ const binding = summary.identity.boundaryBinding;
737
+ if (contract === void 0 || binding !== null && !(0, import_behavioral_ir3.exchangesHttpResponses)(binding)) {
738
+ return summary.gaps;
739
+ }
740
+ const declared = {
741
+ framework: contract.framework ?? "declared",
742
+ responses: contract.responses
665
743
  };
744
+ const stale = new Set(
745
+ contractStatusGaps(declared, summary.transitions).map(gapKey)
746
+ );
747
+ const onThrow = wrappers.filter((wrapper) => wrapper.reference.onThrow === true).flatMap(
748
+ (wrapper) => attribute(wrapper.summary.transitions, wrapper.reference)
749
+ );
750
+ const gaps = [
751
+ ...summary.gaps.filter((gap) => !stale.has(gapKey(gap))),
752
+ ...contractStatusGaps(declared, [...composed, ...onThrow])
753
+ ];
754
+ const unchanged = gaps.length === summary.gaps.length && gaps.every((gap, i) => gapKey(gap) === gapKey(summary.gaps[i]));
755
+ return unchanged ? summary.gaps : gaps;
756
+ }
757
+ function gapKey(gap) {
758
+ return `${gap.type}|${gap.description}`;
666
759
  }
667
760
  function merge(before, step) {
668
761
  return {
@@ -675,7 +768,7 @@ function coversUnit(reference, summary) {
675
768
  return true;
676
769
  }
677
770
  const binding = summary.identity.boundaryBinding;
678
- return binding !== null && (0, import_behavioral_ir2.withinScope)(binding, reference.scope);
771
+ return binding !== null && (0, import_behavioral_ir3.withinScope)(binding, reference.scope);
679
772
  }
680
773
  function applyWrapper(wrapper, inner) {
681
774
  const shortCircuits = attribute(
@@ -732,7 +825,7 @@ function splice(outer, inner) {
732
825
  function attribute(transitions, reference) {
733
826
  return transitions.map((transition) => ({
734
827
  ...transition,
735
- metadata: (0, import_behavioral_ir2.withWrapperMetadata)(transition.metadata, { from: reference })
828
+ metadata: (0, import_behavioral_ir3.withWrapperMetadata)(transition.metadata, { from: reference })
736
829
  }));
737
830
  }
738
831
  function withDistinctIds(transitions) {
@@ -1603,7 +1696,7 @@ function fileStampEquals(a, b) {
1603
1696
  }
1604
1697
 
1605
1698
  // src/extractionReport.ts
1606
- var import_behavioral_ir3 = require("@suss/behavioral-ir");
1699
+ var import_behavioral_ir4 = require("@suss/behavioral-ir");
1607
1700
  var emptyTally = () => ({
1608
1701
  candidateFiles: 0,
1609
1702
  unitsDiscovered: 0,
@@ -1654,7 +1747,7 @@ function summaryCountsByPack(summaries) {
1654
1747
  }
1655
1748
  const entry = counts.get(recognition) ?? emptyCounts();
1656
1749
  entry.bound += 1;
1657
- if (import_behavioral_ir3.BOUNDARY_ROLE[summary.kind] === "provider") {
1750
+ if (import_behavioral_ir4.BOUNDARY_ROLE[summary.kind] === "provider") {
1658
1751
  entry.providers += 1;
1659
1752
  }
1660
1753
  if (summary.transitions.length > 0) {
@@ -2122,22 +2215,22 @@ function buildMetadata(raw) {
2122
2215
  let metadata = {};
2123
2216
  const http = buildHttpMetadataValue(raw);
2124
2217
  if (http !== null) {
2125
- metadata = (0, import_behavioral_ir4.withHttpMetadata)(metadata, http);
2218
+ metadata = (0, import_behavioral_ir5.withHttpMetadata)(metadata, http);
2126
2219
  }
2127
2220
  const graphql = buildGraphqlMetadataValue(raw);
2128
2221
  if (graphql !== null) {
2129
- metadata = (0, import_behavioral_ir4.withGraphqlMetadata)(metadata, graphql);
2222
+ metadata = (0, import_behavioral_ir5.withGraphqlMetadata)(metadata, graphql);
2130
2223
  }
2131
2224
  if (raw.sourceDocumentLabel !== void 0) {
2132
- metadata = (0, import_behavioral_ir4.withSourceDocumentMetadata)(metadata, {
2225
+ metadata = (0, import_behavioral_ir5.withSourceDocumentMetadata)(metadata, {
2133
2226
  label: raw.sourceDocumentLabel
2134
2227
  });
2135
2228
  }
2136
2229
  if (raw.mount !== void 0) {
2137
- metadata = (0, import_behavioral_ir4.withMountMetadata)(metadata, raw.mount);
2230
+ metadata = (0, import_behavioral_ir5.withMountMetadata)(metadata, raw.mount);
2138
2231
  }
2139
2232
  if (raw.wrappers !== void 0 && raw.wrappers.length > 0) {
2140
- metadata = (0, import_behavioral_ir4.withWrapperMetadata)(metadata, { applied: raw.wrappers });
2233
+ metadata = (0, import_behavioral_ir5.withWrapperMetadata)(metadata, { applied: raw.wrappers });
2141
2234
  }
2142
2235
  return Object.keys(metadata).length > 0 ? metadata : null;
2143
2236
  }
@@ -2187,7 +2280,7 @@ function answersWithHttpResponses(raw) {
2187
2280
  if (raw.boundaryBinding === null || raw.boundaryBinding === void 0) {
2188
2281
  return true;
2189
2282
  }
2190
- return (0, import_behavioral_ir4.exchangesHttpResponses)(raw.boundaryBinding);
2283
+ return (0, import_behavioral_ir5.exchangesHttpResponses)(raw.boundaryBinding);
2191
2284
  }
2192
2285
  function detectGaps(raw, transitions, options) {
2193
2286
  if (options.gapHandling === "silent") {
@@ -2221,41 +2314,7 @@ function detectGaps(raw, transitions, options) {
2221
2314
  });
2222
2315
  }
2223
2316
  if (raw.declaredContract && answersWithHttpResponses(raw)) {
2224
- const producedStatuses = new Set(
2225
- transitions.flatMap((t) => {
2226
- if (t.output.type !== "response") {
2227
- return [];
2228
- }
2229
- const sc = t.output.statusCode;
2230
- if (sc?.type === "literal") {
2231
- return [sc.value];
2232
- }
2233
- return [];
2234
- })
2235
- );
2236
- const declaredStatuses = new Set(
2237
- raw.declaredContract.responses.map((r) => r.statusCode)
2238
- );
2239
- for (const declared of declaredStatuses) {
2240
- if (!producedStatuses.has(declared)) {
2241
- gaps.push({
2242
- type: "unhandledCase",
2243
- conditions: [],
2244
- consequence: "frameworkDefault",
2245
- description: `Declared response ${declared} is never produced by the handler`
2246
- });
2247
- }
2248
- }
2249
- for (const produced of producedStatuses) {
2250
- if (!declaredStatuses.has(produced)) {
2251
- gaps.push({
2252
- type: "unhandledCase",
2253
- conditions: [],
2254
- consequence: "unknown",
2255
- description: `Handler produces status ${produced} which is not declared in the ${raw.declaredContract.framework} contract`
2256
- });
2257
- }
2258
- }
2317
+ gaps.push(...contractStatusGaps(raw.declaredContract, transitions));
2259
2318
  }
2260
2319
  return gaps;
2261
2320
  }
package/dist/index.d.cts CHANGED
@@ -721,6 +721,66 @@ type DeclaredBinding = {
721
721
  messageBus: MessageBusSemantics["messageBus"];
722
722
  channel: ChannelSource;
723
723
  };
724
+ /**
725
+ * How a framework takes a function that runs around a handler rather
726
+ * than as one. Middleware and an error handler are registered through
727
+ * a method on the routable, `app.use(fn)` or `app.onError(fn)`. A
728
+ * validation hook is handed to the routable's constructor instead,
729
+ * `new OpenAPIHono({ defaultHook })`, and runs for every route on it.
730
+ */
731
+ type WrapperRegistration = WrapperMethodRegistration | WrapperOptionRegistration;
732
+ /** What every wrapper registration says about the function it registers. */
733
+ interface WrapperFunctionShape {
734
+ /**
735
+ * Parameter position of the continuation inside the wrapper, the one
736
+ * it calls to hand control on. Absent when the wrapper never
737
+ * continues, which is how an error handler that always responds is
738
+ * written.
739
+ */
740
+ continuationParam?: number;
741
+ /**
742
+ * Parameter position that receives what the wrapped unit threw. A
743
+ * wrapper that declares one runs only when the wrapped unit's path
744
+ * ended by throwing, which is a fact about how the framework invokes
745
+ * it rather than anything its body says. Absent means the wrapper
746
+ * runs on the ordinary path.
747
+ */
748
+ throwParam?: number;
749
+ /**
750
+ * Parameter position that receives a value the framework worked out
751
+ * before calling the wrapper, the outcome of validating the request
752
+ * for a hook. The pack's terminals read one parameter further along
753
+ * from there, as they do past `throwParam`, and nothing else changes:
754
+ * the wrapper still runs on the ordinary path.
755
+ */
756
+ resultParam?: number;
757
+ }
758
+ /** A wrapper registered through a method on the routable. */
759
+ interface WrapperMethodRegistration extends WrapperFunctionShape {
760
+ /** Method name that registers a wrapper, e.g. "use" or "onError". */
761
+ method: string;
762
+ /** Argument position of the wrapper function. */
763
+ targetPosition: number;
764
+ /**
765
+ * Argument position of a path pattern narrowing which routes the
766
+ * wrapper runs for, when the method takes one. Absent means the
767
+ * wrapper runs for every route on the subject.
768
+ */
769
+ scopePosition?: number;
770
+ /**
771
+ * Only match a registered function declared with exactly this many
772
+ * parameters. Express tells its error handlers apart from its
773
+ * middleware by arity alone, both being `app.use(fn)`.
774
+ */
775
+ arity?: number;
776
+ }
777
+ /** A wrapper handed to the routable's constructor as an option. */
778
+ interface WrapperOptionRegistration extends WrapperFunctionShape {
779
+ /** The option the wrapper is under, e.g. "defaultHook". */
780
+ constructorOption: string;
781
+ /** Argument position of the options object in the constructor call. */
782
+ targetPosition: number;
783
+ }
724
784
  interface DiscoveryPattern {
725
785
  /** The kind of code unit this discovers: "handler", "loader", "action", "component", etc. */
726
786
  kind: string;
@@ -767,39 +827,7 @@ interface DiscoveryPattern {
767
827
  * routable records a reference to it. A registration whose function
768
828
  * the resolution store cannot follow contributes nothing.
769
829
  */
770
- wraps?: {
771
- /** Method name that registers a wrapper, e.g. "use" or "onError". */
772
- method: string;
773
- /** Argument position of the wrapper function. */
774
- targetPosition: number;
775
- /**
776
- * Argument position of a path pattern narrowing which routes the
777
- * wrapper runs for, when the method takes one. Absent means the
778
- * wrapper runs for every route on the subject.
779
- */
780
- scopePosition?: number;
781
- /**
782
- * Parameter position of the continuation inside the wrapper, the one
783
- * it calls to hand control on. Absent when the wrapper never
784
- * continues, which is how an error handler that always responds is
785
- * written.
786
- */
787
- continuationParam?: number;
788
- /**
789
- * Parameter position that receives what the wrapped unit threw. A
790
- * wrapper that declares one runs only when the wrapped unit's path
791
- * ended by throwing, which is a fact about how the framework invokes
792
- * it rather than anything its body says. Absent means the wrapper
793
- * runs on the ordinary path.
794
- */
795
- throwParam?: number;
796
- /**
797
- * Only match a registered function declared with exactly this many
798
- * parameters. Express tells its error handlers apart from its
799
- * middleware by arity alone, both being `app.use(fn)`.
800
- */
801
- arity?: number;
802
- };
830
+ wraps?: WrapperRegistration;
803
831
  /**
804
832
  * This pattern only runs against files that import one of these module
805
833
  * specifiers, or a sub-path of one. An empty array means no gate at all
@@ -1863,12 +1891,19 @@ declare function valueToReadFurtherFrom<T>(reading: Reading<T>): T | null;
1863
1891
  * what composition does not read.
1864
1892
  */
1865
1893
 
1894
+ /**
1895
+ * Whether to keep gaps at all. A run that asked for none has none on
1896
+ * the summaries coming in, and composition adds none either.
1897
+ */
1898
+ interface ComposeOptions {
1899
+ gapHandling?: "strict" | "permissive" | "silent";
1900
+ }
1866
1901
  /**
1867
1902
  * Every summary, with the ones that record wrappers replaced by their
1868
1903
  * composition. A summary with no wrappers, and one whose wrappers this
1869
1904
  * run has no summaries for, comes back untouched.
1870
1905
  */
1871
- declare function composeWrappers(summaries: readonly BehavioralSummary[]): BehavioralSummary[];
1906
+ declare function composeWrappers(summaries: readonly BehavioralSummary[], options?: ComposeOptions): BehavioralSummary[];
1872
1907
 
1873
1908
  /**
1874
1909
  * Which branch an effect belongs to.
@@ -3407,4 +3442,4 @@ declare function rawConditionToPredicate(c: RawCondition): Predicate;
3407
3442
  declare function effectToIR(effect: RawEffect): Effect;
3408
3443
  declare function paramToInput(param: RawParameter): Input;
3409
3444
 
3410
- export { type AccessRecognizer, type AdapterCodeStamp, type AdapterStamp, type AstCapableOps, type BindingExtraction, type BodyContent, type CacheAttribution, type CacheDiagnostic, type CacheInput, type CacheLayer, type CacheLookup, type CallOps, type CaseGroup, type ChannelSource, type ChosenReading, type ConditionHandle, type ConditionInfo, type ConditionSource, type ConstructedFrom, type ContractPattern, type DeclaredBinding, type DeclaredBy, type DeclaredMatch, type DefaultedReading, type DiscoveredCustomUnit, type DiscoveredSubUnit, type DiscoveredSubUnitParent, type DiscoveryMatch, type DiscoveryPattern, type EffectArg, type EmptyStage, type ExitKind, type ExtractionReport, type ExtractorOptions, type FailureDelivery, type HealthCheck, type HealthViolation, type HelperDeclarations, type HelperSearch, type HelperSink, type HelperValue, IdMap, IdSet, type Identified, type InputMappingPattern, type InputRead, type InvocationRecognizer, type LanguageAdapter, type LoweredStatementParts, MAX_ENTRIES, MAX_PATHS, type ModuleInitOptions, type NodeVisitor, type OpsCarrier, type PackDeclarations, type PackFailure, type PackFunnel, type PackGradient, type PackTally, type PartialPlan, PathBudgetExceeded, type PatternPack, type ProjectHelper, type ProjectHelpers, type RawBranch, type RawCodeStructure, type RawCondition, type RawDeclaredContract, type RawDependencyCall, type RawEffect, type RawParameter, type RawTerminal, type Reading, type ReceiverOrigin, type RegistrationHelper, type ResponsePropertyMapping, type ResponsePropertyMeaning, type RootRecord, SKIP_CHILDREN, type SourceRange, type StatementBlock, type StructuredPathConditionsInput, type StructuredPathConditionsResult, type StructuredStatement, type TerminalExtraction, type TerminalMatch, type TerminalPattern, type Timer, type TimingReport, type TransparentWrapper, UnmodeledFlow, type UnsettledName, type ValueEntry, type ValueOps, type WalkableNode, absentReading, ambiguousReading, andThenReading, assembleSummary, assessConfidence, buildUngatedExtractionReport, composeWrappers, computeContentHash, computeDistHashFrom, configuredCallOption, createAdapterStamp, createCacheLayer, createPackTallies, createTimer, detectGaps, effectToIR, emptyTally, enumerateOrDegrade, enumerateStructuredPaths, evaluatePackHealth, firstWrittenReading, formatPackHealth, guardsHoldOn, httpRouteDiscovery, makeTransitionId, mapReading, moduleInitStructure, noopTimer, packGradients, paramToInput, projectFileStamp, rawConditionToPredicate, recordPackFailure, registrationHelperDiscovery, routeHelperIndex, runDigest, runsBefore, scopeOption, sharedGatingConditions, stampModuleImports, storageSystemOption, summaryCountsByPack, tallyUnit, terminalToOutput, unreadableReading, unwrapJsonStringify, valueToReadFurtherFrom, walkDescendants, wrapperDiscovery, writtenReading };
3445
+ export { type AccessRecognizer, type AdapterCodeStamp, type AdapterStamp, type AstCapableOps, type BindingExtraction, type BodyContent, type CacheAttribution, type CacheDiagnostic, type CacheInput, type CacheLayer, type CacheLookup, type CallOps, type CaseGroup, type ChannelSource, type ChosenReading, type ConditionHandle, type ConditionInfo, type ConditionSource, type ConstructedFrom, type ContractPattern, type DeclaredBinding, type DeclaredBy, type DeclaredMatch, type DefaultedReading, type DiscoveredCustomUnit, type DiscoveredSubUnit, type DiscoveredSubUnitParent, type DiscoveryMatch, type DiscoveryPattern, type EffectArg, type EmptyStage, type ExitKind, type ExtractionReport, type ExtractorOptions, type FailureDelivery, type HealthCheck, type HealthViolation, type HelperDeclarations, type HelperSearch, type HelperSink, type HelperValue, IdMap, IdSet, type Identified, type InputMappingPattern, type InputRead, type InvocationRecognizer, type LanguageAdapter, type LoweredStatementParts, MAX_ENTRIES, MAX_PATHS, type ModuleInitOptions, type NodeVisitor, type OpsCarrier, type PackDeclarations, type PackFailure, type PackFunnel, type PackGradient, type PackTally, type PartialPlan, PathBudgetExceeded, type PatternPack, type ProjectHelper, type ProjectHelpers, type RawBranch, type RawCodeStructure, type RawCondition, type RawDeclaredContract, type RawDependencyCall, type RawEffect, type RawParameter, type RawTerminal, type Reading, type ReceiverOrigin, type RegistrationHelper, type ResponsePropertyMapping, type ResponsePropertyMeaning, type RootRecord, SKIP_CHILDREN, type SourceRange, type StatementBlock, type StructuredPathConditionsInput, type StructuredPathConditionsResult, type StructuredStatement, type TerminalExtraction, type TerminalMatch, type TerminalPattern, type Timer, type TimingReport, type TransparentWrapper, UnmodeledFlow, type UnsettledName, type ValueEntry, type ValueOps, type WalkableNode, type WrapperMethodRegistration, type WrapperOptionRegistration, type WrapperRegistration, absentReading, ambiguousReading, andThenReading, assembleSummary, assessConfidence, buildUngatedExtractionReport, composeWrappers, computeContentHash, computeDistHashFrom, configuredCallOption, createAdapterStamp, createCacheLayer, createPackTallies, createTimer, detectGaps, effectToIR, emptyTally, enumerateOrDegrade, enumerateStructuredPaths, evaluatePackHealth, firstWrittenReading, formatPackHealth, guardsHoldOn, httpRouteDiscovery, makeTransitionId, mapReading, moduleInitStructure, noopTimer, packGradients, paramToInput, projectFileStamp, rawConditionToPredicate, recordPackFailure, registrationHelperDiscovery, routeHelperIndex, runDigest, runsBefore, scopeOption, sharedGatingConditions, stampModuleImports, storageSystemOption, summaryCountsByPack, tallyUnit, terminalToOutput, unreadableReading, unwrapJsonStringify, valueToReadFurtherFrom, walkDescendants, wrapperDiscovery, writtenReading };
package/dist/index.d.ts CHANGED
@@ -721,6 +721,66 @@ type DeclaredBinding = {
721
721
  messageBus: MessageBusSemantics["messageBus"];
722
722
  channel: ChannelSource;
723
723
  };
724
+ /**
725
+ * How a framework takes a function that runs around a handler rather
726
+ * than as one. Middleware and an error handler are registered through
727
+ * a method on the routable, `app.use(fn)` or `app.onError(fn)`. A
728
+ * validation hook is handed to the routable's constructor instead,
729
+ * `new OpenAPIHono({ defaultHook })`, and runs for every route on it.
730
+ */
731
+ type WrapperRegistration = WrapperMethodRegistration | WrapperOptionRegistration;
732
+ /** What every wrapper registration says about the function it registers. */
733
+ interface WrapperFunctionShape {
734
+ /**
735
+ * Parameter position of the continuation inside the wrapper, the one
736
+ * it calls to hand control on. Absent when the wrapper never
737
+ * continues, which is how an error handler that always responds is
738
+ * written.
739
+ */
740
+ continuationParam?: number;
741
+ /**
742
+ * Parameter position that receives what the wrapped unit threw. A
743
+ * wrapper that declares one runs only when the wrapped unit's path
744
+ * ended by throwing, which is a fact about how the framework invokes
745
+ * it rather than anything its body says. Absent means the wrapper
746
+ * runs on the ordinary path.
747
+ */
748
+ throwParam?: number;
749
+ /**
750
+ * Parameter position that receives a value the framework worked out
751
+ * before calling the wrapper, the outcome of validating the request
752
+ * for a hook. The pack's terminals read one parameter further along
753
+ * from there, as they do past `throwParam`, and nothing else changes:
754
+ * the wrapper still runs on the ordinary path.
755
+ */
756
+ resultParam?: number;
757
+ }
758
+ /** A wrapper registered through a method on the routable. */
759
+ interface WrapperMethodRegistration extends WrapperFunctionShape {
760
+ /** Method name that registers a wrapper, e.g. "use" or "onError". */
761
+ method: string;
762
+ /** Argument position of the wrapper function. */
763
+ targetPosition: number;
764
+ /**
765
+ * Argument position of a path pattern narrowing which routes the
766
+ * wrapper runs for, when the method takes one. Absent means the
767
+ * wrapper runs for every route on the subject.
768
+ */
769
+ scopePosition?: number;
770
+ /**
771
+ * Only match a registered function declared with exactly this many
772
+ * parameters. Express tells its error handlers apart from its
773
+ * middleware by arity alone, both being `app.use(fn)`.
774
+ */
775
+ arity?: number;
776
+ }
777
+ /** A wrapper handed to the routable's constructor as an option. */
778
+ interface WrapperOptionRegistration extends WrapperFunctionShape {
779
+ /** The option the wrapper is under, e.g. "defaultHook". */
780
+ constructorOption: string;
781
+ /** Argument position of the options object in the constructor call. */
782
+ targetPosition: number;
783
+ }
724
784
  interface DiscoveryPattern {
725
785
  /** The kind of code unit this discovers: "handler", "loader", "action", "component", etc. */
726
786
  kind: string;
@@ -767,39 +827,7 @@ interface DiscoveryPattern {
767
827
  * routable records a reference to it. A registration whose function
768
828
  * the resolution store cannot follow contributes nothing.
769
829
  */
770
- wraps?: {
771
- /** Method name that registers a wrapper, e.g. "use" or "onError". */
772
- method: string;
773
- /** Argument position of the wrapper function. */
774
- targetPosition: number;
775
- /**
776
- * Argument position of a path pattern narrowing which routes the
777
- * wrapper runs for, when the method takes one. Absent means the
778
- * wrapper runs for every route on the subject.
779
- */
780
- scopePosition?: number;
781
- /**
782
- * Parameter position of the continuation inside the wrapper, the one
783
- * it calls to hand control on. Absent when the wrapper never
784
- * continues, which is how an error handler that always responds is
785
- * written.
786
- */
787
- continuationParam?: number;
788
- /**
789
- * Parameter position that receives what the wrapped unit threw. A
790
- * wrapper that declares one runs only when the wrapped unit's path
791
- * ended by throwing, which is a fact about how the framework invokes
792
- * it rather than anything its body says. Absent means the wrapper
793
- * runs on the ordinary path.
794
- */
795
- throwParam?: number;
796
- /**
797
- * Only match a registered function declared with exactly this many
798
- * parameters. Express tells its error handlers apart from its
799
- * middleware by arity alone, both being `app.use(fn)`.
800
- */
801
- arity?: number;
802
- };
830
+ wraps?: WrapperRegistration;
803
831
  /**
804
832
  * This pattern only runs against files that import one of these module
805
833
  * specifiers, or a sub-path of one. An empty array means no gate at all
@@ -1863,12 +1891,19 @@ declare function valueToReadFurtherFrom<T>(reading: Reading<T>): T | null;
1863
1891
  * what composition does not read.
1864
1892
  */
1865
1893
 
1894
+ /**
1895
+ * Whether to keep gaps at all. A run that asked for none has none on
1896
+ * the summaries coming in, and composition adds none either.
1897
+ */
1898
+ interface ComposeOptions {
1899
+ gapHandling?: "strict" | "permissive" | "silent";
1900
+ }
1866
1901
  /**
1867
1902
  * Every summary, with the ones that record wrappers replaced by their
1868
1903
  * composition. A summary with no wrappers, and one whose wrappers this
1869
1904
  * run has no summaries for, comes back untouched.
1870
1905
  */
1871
- declare function composeWrappers(summaries: readonly BehavioralSummary[]): BehavioralSummary[];
1906
+ declare function composeWrappers(summaries: readonly BehavioralSummary[], options?: ComposeOptions): BehavioralSummary[];
1872
1907
 
1873
1908
  /**
1874
1909
  * Which branch an effect belongs to.
@@ -3407,4 +3442,4 @@ declare function rawConditionToPredicate(c: RawCondition): Predicate;
3407
3442
  declare function effectToIR(effect: RawEffect): Effect;
3408
3443
  declare function paramToInput(param: RawParameter): Input;
3409
3444
 
3410
- export { type AccessRecognizer, type AdapterCodeStamp, type AdapterStamp, type AstCapableOps, type BindingExtraction, type BodyContent, type CacheAttribution, type CacheDiagnostic, type CacheInput, type CacheLayer, type CacheLookup, type CallOps, type CaseGroup, type ChannelSource, type ChosenReading, type ConditionHandle, type ConditionInfo, type ConditionSource, type ConstructedFrom, type ContractPattern, type DeclaredBinding, type DeclaredBy, type DeclaredMatch, type DefaultedReading, type DiscoveredCustomUnit, type DiscoveredSubUnit, type DiscoveredSubUnitParent, type DiscoveryMatch, type DiscoveryPattern, type EffectArg, type EmptyStage, type ExitKind, type ExtractionReport, type ExtractorOptions, type FailureDelivery, type HealthCheck, type HealthViolation, type HelperDeclarations, type HelperSearch, type HelperSink, type HelperValue, IdMap, IdSet, type Identified, type InputMappingPattern, type InputRead, type InvocationRecognizer, type LanguageAdapter, type LoweredStatementParts, MAX_ENTRIES, MAX_PATHS, type ModuleInitOptions, type NodeVisitor, type OpsCarrier, type PackDeclarations, type PackFailure, type PackFunnel, type PackGradient, type PackTally, type PartialPlan, PathBudgetExceeded, type PatternPack, type ProjectHelper, type ProjectHelpers, type RawBranch, type RawCodeStructure, type RawCondition, type RawDeclaredContract, type RawDependencyCall, type RawEffect, type RawParameter, type RawTerminal, type Reading, type ReceiverOrigin, type RegistrationHelper, type ResponsePropertyMapping, type ResponsePropertyMeaning, type RootRecord, SKIP_CHILDREN, type SourceRange, type StatementBlock, type StructuredPathConditionsInput, type StructuredPathConditionsResult, type StructuredStatement, type TerminalExtraction, type TerminalMatch, type TerminalPattern, type Timer, type TimingReport, type TransparentWrapper, UnmodeledFlow, type UnsettledName, type ValueEntry, type ValueOps, type WalkableNode, absentReading, ambiguousReading, andThenReading, assembleSummary, assessConfidence, buildUngatedExtractionReport, composeWrappers, computeContentHash, computeDistHashFrom, configuredCallOption, createAdapterStamp, createCacheLayer, createPackTallies, createTimer, detectGaps, effectToIR, emptyTally, enumerateOrDegrade, enumerateStructuredPaths, evaluatePackHealth, firstWrittenReading, formatPackHealth, guardsHoldOn, httpRouteDiscovery, makeTransitionId, mapReading, moduleInitStructure, noopTimer, packGradients, paramToInput, projectFileStamp, rawConditionToPredicate, recordPackFailure, registrationHelperDiscovery, routeHelperIndex, runDigest, runsBefore, scopeOption, sharedGatingConditions, stampModuleImports, storageSystemOption, summaryCountsByPack, tallyUnit, terminalToOutput, unreadableReading, unwrapJsonStringify, valueToReadFurtherFrom, walkDescendants, wrapperDiscovery, writtenReading };
3445
+ export { type AccessRecognizer, type AdapterCodeStamp, type AdapterStamp, type AstCapableOps, type BindingExtraction, type BodyContent, type CacheAttribution, type CacheDiagnostic, type CacheInput, type CacheLayer, type CacheLookup, type CallOps, type CaseGroup, type ChannelSource, type ChosenReading, type ConditionHandle, type ConditionInfo, type ConditionSource, type ConstructedFrom, type ContractPattern, type DeclaredBinding, type DeclaredBy, type DeclaredMatch, type DefaultedReading, type DiscoveredCustomUnit, type DiscoveredSubUnit, type DiscoveredSubUnitParent, type DiscoveryMatch, type DiscoveryPattern, type EffectArg, type EmptyStage, type ExitKind, type ExtractionReport, type ExtractorOptions, type FailureDelivery, type HealthCheck, type HealthViolation, type HelperDeclarations, type HelperSearch, type HelperSink, type HelperValue, IdMap, IdSet, type Identified, type InputMappingPattern, type InputRead, type InvocationRecognizer, type LanguageAdapter, type LoweredStatementParts, MAX_ENTRIES, MAX_PATHS, type ModuleInitOptions, type NodeVisitor, type OpsCarrier, type PackDeclarations, type PackFailure, type PackFunnel, type PackGradient, type PackTally, type PartialPlan, PathBudgetExceeded, type PatternPack, type ProjectHelper, type ProjectHelpers, type RawBranch, type RawCodeStructure, type RawCondition, type RawDeclaredContract, type RawDependencyCall, type RawEffect, type RawParameter, type RawTerminal, type Reading, type ReceiverOrigin, type RegistrationHelper, type ResponsePropertyMapping, type ResponsePropertyMeaning, type RootRecord, SKIP_CHILDREN, type SourceRange, type StatementBlock, type StructuredPathConditionsInput, type StructuredPathConditionsResult, type StructuredStatement, type TerminalExtraction, type TerminalMatch, type TerminalPattern, type Timer, type TimingReport, type TransparentWrapper, UnmodeledFlow, type UnsettledName, type ValueEntry, type ValueOps, type WalkableNode, type WrapperMethodRegistration, type WrapperOptionRegistration, type WrapperRegistration, absentReading, ambiguousReading, andThenReading, assembleSummary, assessConfidence, buildUngatedExtractionReport, composeWrappers, computeContentHash, computeDistHashFrom, configuredCallOption, createAdapterStamp, createCacheLayer, createPackTallies, createTimer, detectGaps, effectToIR, emptyTally, enumerateOrDegrade, enumerateStructuredPaths, evaluatePackHealth, firstWrittenReading, formatPackHealth, guardsHoldOn, httpRouteDiscovery, makeTransitionId, mapReading, moduleInitStructure, noopTimer, packGradients, paramToInput, projectFileStamp, rawConditionToPredicate, recordPackFailure, registrationHelperDiscovery, routeHelperIndex, runDigest, runsBefore, scopeOption, sharedGatingConditions, stampModuleImports, storageSystemOption, summaryCountsByPack, tallyUnit, terminalToOutput, unreadableReading, unwrapJsonStringify, valueToReadFurtherFrom, walkDescendants, wrapperDiscovery, writtenReading };
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // src/index.ts
2
2
  import { createHash as createHash3 } from "crypto";
3
3
  import {
4
- exchangesHttpResponses,
4
+ exchangesHttpResponses as exchangesHttpResponses2,
5
5
  withGraphqlMetadata,
6
6
  withHttpMetadata,
7
7
  withMountMetadata,
@@ -9,6 +9,67 @@ import {
9
9
  withWrapperMetadata as withWrapperMetadata2
10
10
  } from "@suss/behavioral-ir";
11
11
 
12
+ // src/contractStatusGaps.ts
13
+ import { readWrapperMetadata } from "@suss/behavioral-ir";
14
+ function contractStatusGaps(contract, transitions) {
15
+ const producers = producersByStatus(transitions);
16
+ const declared = new Set(contract.responses.map((r) => r.statusCode));
17
+ const gaps = [];
18
+ for (const status of declared) {
19
+ if (!producers.has(status)) {
20
+ gaps.push({
21
+ type: "unhandledCase",
22
+ conditions: [],
23
+ consequence: "frameworkDefault",
24
+ description: `Declared response ${status} is never produced by the handler`
25
+ });
26
+ }
27
+ }
28
+ for (const [status, wrappers] of producers) {
29
+ if (declared.has(status)) {
30
+ continue;
31
+ }
32
+ gaps.push({
33
+ type: "unhandledCase",
34
+ conditions: [],
35
+ consequence: "unknown",
36
+ description: `${producerOf(wrappers)} produces status ${status} which is not declared in the ${contract.framework} contract`
37
+ });
38
+ }
39
+ return gaps;
40
+ }
41
+ function producersByStatus(transitions) {
42
+ const producers = /* @__PURE__ */ new Map();
43
+ for (const transition of transitions) {
44
+ if (transition.output.type !== "response") {
45
+ continue;
46
+ }
47
+ const status = transition.output.statusCode;
48
+ if (status?.type !== "literal" || typeof status.value !== "number") {
49
+ continue;
50
+ }
51
+ const from = readWrapperMetadata(transition)?.from;
52
+ const seen = producers.get(status.value);
53
+ if (seen === void 0) {
54
+ producers.set(
55
+ status.value,
56
+ new Set(from === void 0 ? [] : [from.name])
57
+ );
58
+ } else if (from === void 0) {
59
+ seen.clear();
60
+ } else if (seen.size > 0) {
61
+ seen.add(from.name);
62
+ }
63
+ }
64
+ return producers;
65
+ }
66
+ function producerOf(wrappers) {
67
+ if (wrappers.size === 0) {
68
+ return "Handler";
69
+ }
70
+ return `${[...wrappers].sort().join(" and ")}, registered around this handler,`;
71
+ }
72
+
12
73
  // src/inputReads.ts
13
74
  function inputReadsOf(args) {
14
75
  const found = /* @__PURE__ */ new Map();
@@ -101,7 +162,9 @@ function mergeInputReads(derived, syntactic) {
101
162
 
102
163
  // src/composeWrappers.ts
103
164
  import {
104
- readWrapperMetadata,
165
+ exchangesHttpResponses,
166
+ readHttpMetadata,
167
+ readWrapperMetadata as readWrapperMetadata2,
105
168
  withinScope,
106
169
  withWrapperMetadata
107
170
  } from "@suss/behavioral-ir";
@@ -528,7 +591,7 @@ var BUDGET_GAP = {
528
591
  consequence: "unknown",
529
592
  description: `Composing the wrappers registered around this unit would have gone past the path budget of ${MAX_PATHS}, so what each of them produces is reported beside this unit's own outcomes rather than under the conditions that reach it`
530
593
  };
531
- function composeWrappers(summaries) {
594
+ function composeWrappers(summaries, options = {}) {
532
595
  const byKey = /* @__PURE__ */ new Map();
533
596
  for (const summary of summaries) {
534
597
  const key = summaryKey(summary.location.file, summary.identity.name);
@@ -536,13 +599,14 @@ function composeWrappers(summaries) {
536
599
  byKey.set(key, summary);
537
600
  }
538
601
  }
539
- return summaries.map((summary) => composeOne(summary, byKey));
602
+ const keepGaps = options.gapHandling !== "silent";
603
+ return summaries.map((summary) => composeOne(summary, byKey, keepGaps));
540
604
  }
541
605
  function summaryKey(file, name) {
542
606
  return `${file}::${name}`;
543
607
  }
544
- function composeOne(summary, byKey) {
545
- const recorded = readWrapperMetadata(summary);
608
+ function composeOne(summary, byKey, keepGaps) {
609
+ const recorded = readWrapperMetadata2(summary);
546
610
  const applied = recorded?.applied ?? [];
547
611
  const covering = applied.filter(
548
612
  (reference) => coversUnit(reference, summary)
@@ -576,14 +640,45 @@ function composeOne(summary, byKey) {
576
640
  }
577
641
  composition = merge(composition, applyThrowWrapper(wrapper, composition));
578
642
  }
579
- if (composition.transitions === summary.transitions) {
643
+ const transitions = composition.transitions === summary.transitions ? summary.transitions : withDistinctIds(composition.transitions);
644
+ if (!keepGaps) {
645
+ return { ...narrowed, transitions };
646
+ }
647
+ const gaps = withContractStatusGaps(summary, transitions, wrappers);
648
+ if (transitions === summary.transitions && gaps === summary.gaps) {
580
649
  return narrowed;
581
650
  }
582
651
  return {
583
652
  ...narrowed,
584
- transitions: withDistinctIds(composition.transitions),
585
- gaps: composition.degraded ? [...summary.gaps, BUDGET_GAP] : summary.gaps
653
+ transitions,
654
+ gaps: composition.degraded ? [...gaps, BUDGET_GAP] : gaps
655
+ };
656
+ }
657
+ function withContractStatusGaps(summary, composed, wrappers) {
658
+ const contract = readHttpMetadata(summary)?.declaredContract;
659
+ const binding = summary.identity.boundaryBinding;
660
+ if (contract === void 0 || binding !== null && !exchangesHttpResponses(binding)) {
661
+ return summary.gaps;
662
+ }
663
+ const declared = {
664
+ framework: contract.framework ?? "declared",
665
+ responses: contract.responses
586
666
  };
667
+ const stale = new Set(
668
+ contractStatusGaps(declared, summary.transitions).map(gapKey)
669
+ );
670
+ const onThrow = wrappers.filter((wrapper) => wrapper.reference.onThrow === true).flatMap(
671
+ (wrapper) => attribute(wrapper.summary.transitions, wrapper.reference)
672
+ );
673
+ const gaps = [
674
+ ...summary.gaps.filter((gap) => !stale.has(gapKey(gap))),
675
+ ...contractStatusGaps(declared, [...composed, ...onThrow])
676
+ ];
677
+ const unchanged = gaps.length === summary.gaps.length && gaps.every((gap, i) => gapKey(gap) === gapKey(summary.gaps[i]));
678
+ return unchanged ? summary.gaps : gaps;
679
+ }
680
+ function gapKey(gap) {
681
+ return `${gap.type}|${gap.description}`;
587
682
  }
588
683
  function merge(before, step) {
589
684
  return {
@@ -2107,7 +2202,7 @@ function answersWithHttpResponses(raw) {
2107
2202
  if (raw.boundaryBinding === null || raw.boundaryBinding === void 0) {
2108
2203
  return true;
2109
2204
  }
2110
- return exchangesHttpResponses(raw.boundaryBinding);
2205
+ return exchangesHttpResponses2(raw.boundaryBinding);
2111
2206
  }
2112
2207
  function detectGaps(raw, transitions, options) {
2113
2208
  if (options.gapHandling === "silent") {
@@ -2141,41 +2236,7 @@ function detectGaps(raw, transitions, options) {
2141
2236
  });
2142
2237
  }
2143
2238
  if (raw.declaredContract && answersWithHttpResponses(raw)) {
2144
- const producedStatuses = new Set(
2145
- transitions.flatMap((t) => {
2146
- if (t.output.type !== "response") {
2147
- return [];
2148
- }
2149
- const sc = t.output.statusCode;
2150
- if (sc?.type === "literal") {
2151
- return [sc.value];
2152
- }
2153
- return [];
2154
- })
2155
- );
2156
- const declaredStatuses = new Set(
2157
- raw.declaredContract.responses.map((r) => r.statusCode)
2158
- );
2159
- for (const declared of declaredStatuses) {
2160
- if (!producedStatuses.has(declared)) {
2161
- gaps.push({
2162
- type: "unhandledCase",
2163
- conditions: [],
2164
- consequence: "frameworkDefault",
2165
- description: `Declared response ${declared} is never produced by the handler`
2166
- });
2167
- }
2168
- }
2169
- for (const produced of producedStatuses) {
2170
- if (!declaredStatuses.has(produced)) {
2171
- gaps.push({
2172
- type: "unhandledCase",
2173
- conditions: [],
2174
- consequence: "unknown",
2175
- description: `Handler produces status ${produced} which is not declared in the ${raw.declaredContract.framework} contract`
2176
- });
2177
- }
2178
- }
2239
+ gaps.push(...contractStatusGaps(raw.declaredContract, transitions));
2179
2240
  }
2180
2241
  return gaps;
2181
2242
  }
package/package.json CHANGED
@@ -1,14 +1,15 @@
1
1
  {
2
2
  "name": "@suss/extractor",
3
- "version": "0.22.3",
3
+ "version": "0.23.0",
4
4
  "type": "module",
5
- "description": "Framework-agnostic extraction pipeline that turns raw extractions into suss behavioral summaries.",
5
+ "description": "The suss extraction pipeline, which turns what a language adapter read into the summaries the checker pairs across a boundary.",
6
6
  "keywords": [
7
7
  "suss",
8
- "behavioral-summary",
9
8
  "static-analysis",
9
+ "behavioral-summary",
10
10
  "extraction",
11
- "typescript"
11
+ "typescript",
12
+ "api-contract"
12
13
  ],
13
14
  "license": "Apache-2.0",
14
15
  "author": "Nimbus Cloud AI LLC",
@@ -45,7 +46,7 @@
45
46
  "typecheck": "tsc --noEmit"
46
47
  },
47
48
  "dependencies": {
48
- "@suss/behavioral-ir": "0.22.3",
49
+ "@suss/behavioral-ir": "0.23.0",
49
50
  "zod": "^4.0.0"
50
51
  },
51
52
  "devDependencies": {