@suss/extractor 0.19.0 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -4,7 +4,9 @@ import {
4
4
  exchangesHttpResponses,
5
5
  withGraphqlMetadata,
6
6
  withHttpMetadata,
7
- withSourceDocumentMetadata
7
+ withMountMetadata,
8
+ withSourceDocumentMetadata,
9
+ withWrapperMetadata as withWrapperMetadata2
8
10
  } from "@suss/behavioral-ir";
9
11
 
10
12
  // src/inputReads.ts
@@ -97,80 +99,12 @@ function mergeInputReads(derived, syntactic) {
97
99
  return [...found.values()];
98
100
  }
99
101
 
100
- // src/packHelpers.ts
101
- function httpRouteDiscovery(opts) {
102
- const kind = opts.kind ?? "handler";
103
- const calls = opts.importNames.map((importName) => ({
104
- kind,
105
- match: {
106
- type: "registrationCall",
107
- importModule: opts.importModule,
108
- importName,
109
- registrationChain: [...opts.methods]
110
- },
111
- bindingExtraction: {
112
- // `.all` registers every method, so it records "*", which the pairing
113
- // engine treats as agreeing with any method at all.
114
- method: {
115
- type: "fromRegistration",
116
- position: "methodName",
117
- nameMap: { all: "*" }
118
- },
119
- path: { type: "fromArgument", position: 0 }
120
- },
121
- ...opts.mount !== void 0 ? { mount: opts.mount } : {},
122
- requiresImport: [opts.importModule]
123
- }));
124
- if (opts.importNames.length === 0) {
125
- return calls;
126
- }
127
- return [
128
- ...calls,
129
- // Routes registered in a loop over an array of specs, a shape
130
- // registration-call discovery cannot see. The receiver comes from
131
- // the same import declaration as the calls above, so a loop that
132
- // never touches this library's routable is left alone.
133
- {
134
- kind,
135
- match: {
136
- type: "registrationLoop",
137
- elementShape: LOOP_ELEMENT_SHAPE,
138
- receiver: {
139
- importModule: opts.importModule,
140
- importNames: [...opts.importNames]
141
- }
142
- },
143
- requiresImport: [opts.importModule]
144
- }
145
- ];
146
- }
147
- var LOOP_ELEMENT_SHAPE = {
148
- methodKey: "method",
149
- pathKey: "path",
150
- handlerKey: "handler"
151
- };
152
- function registrationHelperDiscovery(helpers, kind = "handler") {
153
- return helpers.map((helper) => ({
154
- kind,
155
- match: {
156
- type: "registrationTemplate",
157
- helperName: helper.helperName,
158
- ...helper.importModule !== void 0 ? { importModule: helper.importModule } : {},
159
- registrations: helper.registrations.map((one) => ({ ...one }))
160
- }
161
- }));
162
- }
163
- function unwrapJsonStringify(body) {
164
- if (body === null || typeof body !== "object") {
165
- return body;
166
- }
167
- const candidate = body;
168
- if (candidate.kind !== "call" || candidate.callee !== "JSON.stringify") {
169
- return body;
170
- }
171
- const inner = candidate.args?.[0];
172
- return inner ?? body;
173
- }
102
+ // src/composeWrappers.ts
103
+ import {
104
+ readWrapperMetadata,
105
+ withinScope,
106
+ withWrapperMetadata
107
+ } from "@suss/behavioral-ir";
174
108
 
175
109
  // src/paths/enumeratePaths.ts
176
110
  import { CATCH_ENTRY_TEXT } from "@suss/behavioral-ir";
@@ -587,23 +521,372 @@ function reasonToDegrade(error) {
587
521
  return null;
588
522
  }
589
523
 
590
- // src/paths/gatingConditions.ts
524
+ // src/composeWrappers.ts
525
+ var BUDGET_GAP = {
526
+ type: "unreadOutcome",
527
+ conditions: [],
528
+ consequence: "unknown",
529
+ 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
+ };
531
+ function composeWrappers(summaries) {
532
+ const byKey = /* @__PURE__ */ new Map();
533
+ for (const summary of summaries) {
534
+ const key = summaryKey(summary.location.file, summary.identity.name);
535
+ if (!byKey.has(key)) {
536
+ byKey.set(key, summary);
537
+ }
538
+ }
539
+ return summaries.map((summary) => composeOne(summary, byKey));
540
+ }
541
+ function summaryKey(file, name) {
542
+ return `${file}::${name}`;
543
+ }
544
+ function composeOne(summary, byKey) {
545
+ const recorded = readWrapperMetadata(summary);
546
+ const applied = recorded?.applied ?? [];
547
+ const covering = applied.filter(
548
+ (reference) => coversUnit(reference, summary)
549
+ );
550
+ const narrowed = covering.length === applied.length ? summary : {
551
+ ...summary,
552
+ metadata: withWrapperMetadata(summary.metadata, {
553
+ applied: covering
554
+ })
555
+ };
556
+ const wrappers = covering.flatMap((reference) => {
557
+ const found = byKey.get(summaryKey(reference.file, reference.name));
558
+ return found === void 0 ? [] : [{ reference, summary: found }];
559
+ });
560
+ if (wrappers.length === 0) {
561
+ return narrowed;
562
+ }
563
+ let composition = {
564
+ transitions: summary.transitions,
565
+ degraded: false
566
+ };
567
+ for (const wrapper of [...wrappers].reverse()) {
568
+ if (wrapper.reference.onThrow === true) {
569
+ continue;
570
+ }
571
+ composition = merge(composition, applyWrapper(wrapper, composition));
572
+ }
573
+ for (const wrapper of wrappers) {
574
+ if (wrapper.reference.onThrow !== true) {
575
+ continue;
576
+ }
577
+ composition = merge(composition, applyThrowWrapper(wrapper, composition));
578
+ }
579
+ if (composition.transitions === summary.transitions) {
580
+ return narrowed;
581
+ }
582
+ return {
583
+ ...narrowed,
584
+ transitions: withDistinctIds(composition.transitions),
585
+ gaps: composition.degraded ? [...summary.gaps, BUDGET_GAP] : summary.gaps
586
+ };
587
+ }
588
+ function merge(before, step) {
589
+ return {
590
+ transitions: step.transitions,
591
+ degraded: before.degraded || step.degraded
592
+ };
593
+ }
594
+ function coversUnit(reference, summary) {
595
+ if (reference.scope === void 0) {
596
+ return true;
597
+ }
598
+ const binding = summary.identity.boundaryBinding;
599
+ return binding !== null && withinScope(binding, reference.scope);
600
+ }
601
+ function applyWrapper(wrapper, inner) {
602
+ const shortCircuits = attribute(
603
+ wrapper.summary.transitions.filter((t) => t.output.type !== "delegate"),
604
+ wrapper.reference
605
+ );
606
+ const passThroughs = wrapper.summary.transitions.filter(
607
+ (t) => t.output.type === "delegate"
608
+ );
609
+ if (passThroughs.length === 0 || shortCircuits.length + passThroughs.length * inner.transitions.length > MAX_PATHS) {
610
+ return {
611
+ transitions: [...shortCircuits, ...inner.transitions],
612
+ degraded: passThroughs.length > 0
613
+ };
614
+ }
615
+ const continued = passThroughs.flatMap(
616
+ (passThrough) => inner.transitions.map((transition) => splice(passThrough, transition))
617
+ );
618
+ return { transitions: [...shortCircuits, ...continued], degraded: false };
619
+ }
620
+ function applyThrowWrapper(wrapper, inner) {
621
+ const thrown = inner.transitions.filter((t) => t.output.type === "throw");
622
+ if (thrown.length === 0) {
623
+ return inner;
624
+ }
625
+ const rest = inner.transitions.filter((t) => t.output.type !== "throw");
626
+ const handled = wrapper.summary.transitions;
627
+ if (rest.length + thrown.length * handled.length > MAX_PATHS) {
628
+ return {
629
+ transitions: [
630
+ ...inner.transitions,
631
+ ...attribute(handled, wrapper.reference)
632
+ ],
633
+ degraded: true
634
+ };
635
+ }
636
+ const composed = thrown.flatMap(
637
+ (transition) => attribute(
638
+ handled.map((handler) => splice(transition, handler)),
639
+ wrapper.reference
640
+ )
641
+ );
642
+ return { transitions: [...rest, ...composed], degraded: false };
643
+ }
644
+ function splice(outer, inner) {
645
+ return {
646
+ ...inner,
647
+ id: `${inner.id}:via:${outer.id}`,
648
+ conditions: [...outer.conditions, ...inner.conditions],
649
+ effects: [...outer.effects, ...inner.effects],
650
+ isDefault: outer.isDefault && inner.isDefault
651
+ };
652
+ }
653
+ function attribute(transitions, reference) {
654
+ return transitions.map((transition) => ({
655
+ ...transition,
656
+ metadata: withWrapperMetadata(transition.metadata, { from: reference })
657
+ }));
658
+ }
659
+ function withDistinctIds(transitions) {
660
+ const seen = /* @__PURE__ */ new Map();
661
+ return transitions.map((transition) => {
662
+ const taken = seen.get(transition.id);
663
+ seen.set(transition.id, (taken ?? 0) + 1);
664
+ return taken === void 0 ? transition : { ...transition, id: `${transition.id}#${taken + 1}` };
665
+ });
666
+ }
667
+
668
+ // src/effectGuards.ts
591
669
  var keyOf = (condition) => `${condition.polarity}:${condition.sourceText}`;
670
+ var opposite = (condition) => `${condition.polarity === "positive" ? "negative" : "positive"}:${condition.sourceText}`;
671
+ function guardsHoldOn(preconditions, conditions) {
672
+ if (preconditions === void 0 || preconditions.length === 0) {
673
+ return true;
674
+ }
675
+ const onBranch = new Set(conditions.map(keyOf));
676
+ return preconditions.every(
677
+ (precondition) => !onBranch.has(opposite(precondition))
678
+ );
679
+ }
680
+ function runsBefore(effectLine, terminalEndLine) {
681
+ return effectLine <= terminalEndLine;
682
+ }
683
+
684
+ // src/packHelpers.ts
685
+ function httpRouteDiscovery(opts) {
686
+ const kind = opts.kind ?? "handler";
687
+ const calls = opts.importNames.map((importName) => ({
688
+ kind,
689
+ match: {
690
+ type: "registrationCall",
691
+ importModule: opts.importModule,
692
+ importName,
693
+ registrationChain: [...opts.methods]
694
+ },
695
+ bindingExtraction: {
696
+ // `.all` registers every method, so it records "*", which the pairing
697
+ // engine treats as agreeing with any method at all.
698
+ method: {
699
+ type: "fromRegistration",
700
+ position: "methodName",
701
+ nameMap: { all: "*" }
702
+ },
703
+ path: { type: "fromArgument", position: 0 }
704
+ },
705
+ ...opts.mount !== void 0 ? { mount: opts.mount } : {},
706
+ requiresImport: [opts.importModule]
707
+ }));
708
+ if (opts.importNames.length === 0) {
709
+ return calls;
710
+ }
711
+ return [
712
+ ...calls,
713
+ // Routes registered in a loop over an array of specs, a shape
714
+ // registration-call discovery cannot see. The receiver comes from
715
+ // the same import declaration as the calls above, so a loop that
716
+ // never touches this library's routable is left alone.
717
+ {
718
+ kind,
719
+ match: {
720
+ type: "registrationLoop",
721
+ elementShape: LOOP_ELEMENT_SHAPE,
722
+ receiver: {
723
+ importModule: opts.importModule,
724
+ importNames: [...opts.importNames]
725
+ }
726
+ },
727
+ requiresImport: [opts.importModule]
728
+ }
729
+ ];
730
+ }
731
+ function wrapperDiscovery(opts) {
732
+ return opts.importNames.flatMap(
733
+ (importName) => opts.wraps.map((wraps) => ({
734
+ kind: opts.kind ?? "middleware",
735
+ match: {
736
+ type: "registrationCall",
737
+ importModule: opts.importModule,
738
+ importName,
739
+ registrationChain: []
740
+ },
741
+ wraps: { ...wraps },
742
+ requiresImport: [opts.importModule]
743
+ }))
744
+ );
745
+ }
746
+ var LOOP_ELEMENT_SHAPE = {
747
+ methodKey: "method",
748
+ pathKey: "path",
749
+ handlerKey: "handler"
750
+ };
751
+ function registrationHelperDiscovery(helpers, kind = "handler") {
752
+ return helpers.map((helper) => ({
753
+ kind,
754
+ match: {
755
+ type: "registrationTemplate",
756
+ helperName: helper.helperName,
757
+ ...helper.importModule !== void 0 ? { importModule: helper.importModule } : {},
758
+ ...helper.subject !== void 0 ? { subject: helper.subject } : {},
759
+ registrations: helper.registrations.map((one) => ({ ...one }))
760
+ }
761
+ }));
762
+ }
763
+ function routeHelperIndex(opts) {
764
+ const methods = new Set(
765
+ opts.methods.map(
766
+ (method) => method.startsWith(".") ? method.slice(1) : method
767
+ )
768
+ );
769
+ return {
770
+ find: { by: "subject" },
771
+ declare: (helpers) => ({
772
+ discovery: registrationHelperDiscovery(
773
+ helpers.flatMap((helper) => routesRegisteredBy(helper, methods, opts)),
774
+ opts.kind ?? "handler"
775
+ )
776
+ })
777
+ };
778
+ }
779
+ function routesRegisteredBy(helper, methods, opts) {
780
+ const registrations = [];
781
+ let subjectArgument;
782
+ for (const sink of helper.sinks) {
783
+ const registration = routeRegisteredBy(helper, sink, methods);
784
+ if (registration === null) {
785
+ continue;
786
+ }
787
+ registrations.push(registration);
788
+ if (sink.receiver.as === "parameter") {
789
+ subjectArgument = sink.receiver.position;
790
+ }
791
+ }
792
+ if (registrations.length === 0 || subjectArgument === void 0) {
793
+ return [];
794
+ }
795
+ return [
796
+ {
797
+ helperName: helper.name,
798
+ importModule: helper.file,
799
+ subject: {
800
+ argument: subjectArgument,
801
+ importModule: opts.importModule,
802
+ importNames: [...opts.importNames]
803
+ },
804
+ registrations
805
+ }
806
+ ];
807
+ }
808
+ function routeRegisteredBy(helper, sink, methods) {
809
+ if (sink.method === null || !methods.has(sink.method)) {
810
+ return null;
811
+ }
812
+ const { receiver } = sink;
813
+ if (receiver.as !== "parameter" || receiver.property !== void 0 || !helper.subjectParameters.includes(receiver.position)) {
814
+ return null;
815
+ }
816
+ const pathTemplate = sink.arguments[0];
817
+ const handler = sink.arguments[sink.arguments.length - 1];
818
+ if (sink.arguments.length < 2 || pathTemplate === void 0 || pathTemplate.as !== "text" || handler === void 0) {
819
+ return null;
820
+ }
821
+ const handlerArg = handlerSlot(handler);
822
+ return handlerArg === null ? null : {
823
+ // `.all` registers every method, which the pairing engine
824
+ // spells as a wildcard.
825
+ method: sink.method === "all" ? "*" : sink.method.toUpperCase(),
826
+ pathTemplate: pathTemplate.text,
827
+ handlerArg
828
+ };
829
+ }
830
+ function handlerSlot(value) {
831
+ if (value.as !== "parameter") {
832
+ return null;
833
+ }
834
+ return value.property === void 0 ? `{${value.position}}` : `{${value.position}}.${value.property}`;
835
+ }
836
+ function unwrapJsonStringify(body) {
837
+ if (body === null || typeof body !== "object") {
838
+ return body;
839
+ }
840
+ const candidate = body;
841
+ if (candidate.kind !== "call" || candidate.callee !== "JSON.stringify") {
842
+ return body;
843
+ }
844
+ const inner = candidate.args?.[0];
845
+ return inner ?? body;
846
+ }
847
+
848
+ // src/packOptions.ts
849
+ import { z } from "zod";
850
+ var storageSystemOption = z.enum([
851
+ "postgresql",
852
+ "mysql",
853
+ "sqlite"
854
+ ]);
855
+ var scopeOption = z.string();
856
+ var configuredCallOption = z.object({
857
+ /** Module that declares the receiver's type. */
858
+ module: z.string(),
859
+ /** Type name of the receiver, as exported from that module. */
860
+ receiver: z.string(),
861
+ /** Method that performs the send. */
862
+ method: z.string(),
863
+ /** Argument index carrying the subject. */
864
+ subjectArg: z.number(),
865
+ /**
866
+ * Which argument the message body is. Left out when the method has no
867
+ * single body argument, as a batch method taking a list of entries
868
+ * does, and then no body is reported.
869
+ */
870
+ bodyArg: z.number().optional()
871
+ }).strict();
872
+
873
+ // src/paths/gatingConditions.ts
874
+ var keyOf2 = (condition) => `${condition.polarity}:${condition.sourceText}`;
592
875
  function sharedGatingConditions(paths, readPredicate) {
593
876
  const [first, ...rest] = paths ?? [];
594
877
  if (first === void 0) {
595
878
  return [];
596
879
  }
597
- const shared = new Set(first.map(keyOf));
880
+ const shared = new Set(first.map(keyOf2));
598
881
  for (const path of rest) {
599
- const here = new Set(path.map(keyOf));
882
+ const here = new Set(path.map(keyOf2));
600
883
  for (const value of shared) {
601
884
  if (!here.has(value)) {
602
885
  shared.delete(value);
603
886
  }
604
887
  }
605
888
  }
606
- return first.filter((condition) => shared.has(keyOf(condition))).map((condition) => ({
889
+ return first.filter((condition) => shared.has(keyOf2(condition))).map((condition) => ({
607
890
  sourceText: condition.sourceText,
608
891
  structured: readPredicate === void 0 || condition.expression === null ? null : readPredicate(condition.expression),
609
892
  polarity: condition.polarity,
@@ -657,6 +940,21 @@ var IdMap = class {
657
940
  }
658
941
  };
659
942
 
943
+ // src/paths/nodeWalk.ts
944
+ var SKIP_CHILDREN = /* @__PURE__ */ Symbol("skipChildren");
945
+ function walkDescendants(root, carried, visitor) {
946
+ for (const child of root.namedChildren) {
947
+ if (child === null) {
948
+ continue;
949
+ }
950
+ visitor.at(child, carried);
951
+ const inner = visitor.into(child, carried);
952
+ if (inner !== SKIP_CHILDREN) {
953
+ walkDescendants(child, inner, visitor);
954
+ }
955
+ }
956
+ }
957
+
660
958
  // src/reading.ts
661
959
  function writtenReading(value, range) {
662
960
  return { kind: "written", value, range };
@@ -823,6 +1121,7 @@ function assembleSummary(raw, options = DEFAULT_OPTIONS) {
823
1121
  location: {
824
1122
  file: raw.identity.file,
825
1123
  range: raw.identity.range,
1124
+ ...raw.identity.span !== void 0 ? { span: raw.identity.span } : {},
826
1125
  exportName: raw.identity.exportName
827
1126
  },
828
1127
  identity: {
@@ -856,6 +1155,12 @@ function buildMetadata(raw) {
856
1155
  label: raw.sourceDocumentLabel
857
1156
  });
858
1157
  }
1158
+ if (raw.mount !== void 0) {
1159
+ metadata = withMountMetadata(metadata, raw.mount);
1160
+ }
1161
+ if (raw.wrappers !== void 0 && raw.wrappers.length > 0) {
1162
+ metadata = withWrapperMetadata2(metadata, { applied: raw.wrappers });
1163
+ }
859
1164
  return Object.keys(metadata).length > 0 ? metadata : null;
860
1165
  }
861
1166
  function buildHttpMetadataValue(raw) {
@@ -1118,27 +1423,39 @@ function paramToInput(param) {
1118
1423
  export {
1119
1424
  IdMap,
1120
1425
  IdSet,
1426
+ MAX_PATHS,
1121
1427
  PathBudgetExceeded,
1428
+ SKIP_CHILDREN,
1122
1429
  UnmodeledFlow,
1123
1430
  absentReading,
1124
1431
  ambiguousReading,
1125
1432
  andThenReading,
1126
1433
  assembleSummary,
1127
1434
  assessConfidence,
1435
+ composeWrappers,
1436
+ configuredCallOption,
1128
1437
  detectGaps,
1129
1438
  effectToIR,
1130
1439
  enumerateOrDegrade,
1131
1440
  enumerateStructuredPaths,
1132
1441
  firstWrittenReading,
1442
+ guardsHoldOn,
1133
1443
  httpRouteDiscovery,
1134
1444
  makeTransitionId,
1135
1445
  mapReading,
1136
1446
  paramToInput,
1447
+ rawConditionToPredicate,
1137
1448
  registrationHelperDiscovery,
1449
+ routeHelperIndex,
1450
+ runsBefore,
1451
+ scopeOption,
1138
1452
  sharedGatingConditions,
1453
+ storageSystemOption,
1139
1454
  terminalToOutput,
1140
1455
  unreadableReading,
1141
1456
  unwrapJsonStringify,
1142
1457
  valueToReadFurtherFrom,
1458
+ walkDescendants,
1459
+ wrapperDiscovery,
1143
1460
  writtenReading
1144
1461
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@suss/extractor",
3
- "version": "0.19.0",
3
+ "version": "0.21.0",
4
4
  "type": "module",
5
5
  "description": "Framework-agnostic extraction pipeline that turns raw extractions into suss behavioral summaries.",
6
6
  "keywords": [
@@ -45,7 +45,8 @@
45
45
  "typecheck": "tsc --noEmit"
46
46
  },
47
47
  "dependencies": {
48
- "@suss/behavioral-ir": "0.19.0"
48
+ "@suss/behavioral-ir": "0.21.0",
49
+ "zod": "^4.0.0"
49
50
  },
50
51
  "devDependencies": {
51
52
  "tsup": "*",