@suss/extractor 0.20.0 → 0.21.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -5,7 +5,8 @@ import {
5
5
  withGraphqlMetadata,
6
6
  withHttpMetadata,
7
7
  withMountMetadata,
8
- withSourceDocumentMetadata
8
+ withSourceDocumentMetadata,
9
+ withWrapperMetadata as withWrapperMetadata2
9
10
  } from "@suss/behavioral-ir";
10
11
 
11
12
  // src/inputReads.ts
@@ -98,80 +99,12 @@ function mergeInputReads(derived, syntactic) {
98
99
  return [...found.values()];
99
100
  }
100
101
 
101
- // src/packHelpers.ts
102
- function httpRouteDiscovery(opts) {
103
- const kind = opts.kind ?? "handler";
104
- const calls = opts.importNames.map((importName) => ({
105
- kind,
106
- match: {
107
- type: "registrationCall",
108
- importModule: opts.importModule,
109
- importName,
110
- registrationChain: [...opts.methods]
111
- },
112
- bindingExtraction: {
113
- // `.all` registers every method, so it records "*", which the pairing
114
- // engine treats as agreeing with any method at all.
115
- method: {
116
- type: "fromRegistration",
117
- position: "methodName",
118
- nameMap: { all: "*" }
119
- },
120
- path: { type: "fromArgument", position: 0 }
121
- },
122
- ...opts.mount !== void 0 ? { mount: opts.mount } : {},
123
- requiresImport: [opts.importModule]
124
- }));
125
- if (opts.importNames.length === 0) {
126
- return calls;
127
- }
128
- return [
129
- ...calls,
130
- // Routes registered in a loop over an array of specs, a shape
131
- // registration-call discovery cannot see. The receiver comes from
132
- // the same import declaration as the calls above, so a loop that
133
- // never touches this library's routable is left alone.
134
- {
135
- kind,
136
- match: {
137
- type: "registrationLoop",
138
- elementShape: LOOP_ELEMENT_SHAPE,
139
- receiver: {
140
- importModule: opts.importModule,
141
- importNames: [...opts.importNames]
142
- }
143
- },
144
- requiresImport: [opts.importModule]
145
- }
146
- ];
147
- }
148
- var LOOP_ELEMENT_SHAPE = {
149
- methodKey: "method",
150
- pathKey: "path",
151
- handlerKey: "handler"
152
- };
153
- function registrationHelperDiscovery(helpers, kind = "handler") {
154
- return helpers.map((helper) => ({
155
- kind,
156
- match: {
157
- type: "registrationTemplate",
158
- helperName: helper.helperName,
159
- ...helper.importModule !== void 0 ? { importModule: helper.importModule } : {},
160
- registrations: helper.registrations.map((one) => ({ ...one }))
161
- }
162
- }));
163
- }
164
- function unwrapJsonStringify(body) {
165
- if (body === null || typeof body !== "object") {
166
- return body;
167
- }
168
- const candidate = body;
169
- if (candidate.kind !== "call" || candidate.callee !== "JSON.stringify") {
170
- return body;
171
- }
172
- const inner = candidate.args?.[0];
173
- return inner ?? body;
174
- }
102
+ // src/composeWrappers.ts
103
+ import {
104
+ readWrapperMetadata,
105
+ withinScope,
106
+ withWrapperMetadata
107
+ } from "@suss/behavioral-ir";
175
108
 
176
109
  // src/paths/enumeratePaths.ts
177
110
  import { CATCH_ENTRY_TEXT } from "@suss/behavioral-ir";
@@ -588,23 +521,372 @@ function reasonToDegrade(error) {
588
521
  return null;
589
522
  }
590
523
 
591
- // 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
592
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}`;
593
875
  function sharedGatingConditions(paths, readPredicate) {
594
876
  const [first, ...rest] = paths ?? [];
595
877
  if (first === void 0) {
596
878
  return [];
597
879
  }
598
- const shared = new Set(first.map(keyOf));
880
+ const shared = new Set(first.map(keyOf2));
599
881
  for (const path of rest) {
600
- const here = new Set(path.map(keyOf));
882
+ const here = new Set(path.map(keyOf2));
601
883
  for (const value of shared) {
602
884
  if (!here.has(value)) {
603
885
  shared.delete(value);
604
886
  }
605
887
  }
606
888
  }
607
- return first.filter((condition) => shared.has(keyOf(condition))).map((condition) => ({
889
+ return first.filter((condition) => shared.has(keyOf2(condition))).map((condition) => ({
608
890
  sourceText: condition.sourceText,
609
891
  structured: readPredicate === void 0 || condition.expression === null ? null : readPredicate(condition.expression),
610
892
  polarity: condition.polarity,
@@ -658,6 +940,21 @@ var IdMap = class {
658
940
  }
659
941
  };
660
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
+
661
958
  // src/reading.ts
662
959
  function writtenReading(value, range) {
663
960
  return { kind: "written", value, range };
@@ -861,6 +1158,9 @@ function buildMetadata(raw) {
861
1158
  if (raw.mount !== void 0) {
862
1159
  metadata = withMountMetadata(metadata, raw.mount);
863
1160
  }
1161
+ if (raw.wrappers !== void 0 && raw.wrappers.length > 0) {
1162
+ metadata = withWrapperMetadata2(metadata, { applied: raw.wrappers });
1163
+ }
864
1164
  return Object.keys(metadata).length > 0 ? metadata : null;
865
1165
  }
866
1166
  function buildHttpMetadataValue(raw) {
@@ -1123,27 +1423,39 @@ function paramToInput(param) {
1123
1423
  export {
1124
1424
  IdMap,
1125
1425
  IdSet,
1426
+ MAX_PATHS,
1126
1427
  PathBudgetExceeded,
1428
+ SKIP_CHILDREN,
1127
1429
  UnmodeledFlow,
1128
1430
  absentReading,
1129
1431
  ambiguousReading,
1130
1432
  andThenReading,
1131
1433
  assembleSummary,
1132
1434
  assessConfidence,
1435
+ composeWrappers,
1436
+ configuredCallOption,
1133
1437
  detectGaps,
1134
1438
  effectToIR,
1135
1439
  enumerateOrDegrade,
1136
1440
  enumerateStructuredPaths,
1137
1441
  firstWrittenReading,
1442
+ guardsHoldOn,
1138
1443
  httpRouteDiscovery,
1139
1444
  makeTransitionId,
1140
1445
  mapReading,
1141
1446
  paramToInput,
1447
+ rawConditionToPredicate,
1142
1448
  registrationHelperDiscovery,
1449
+ routeHelperIndex,
1450
+ runsBefore,
1451
+ scopeOption,
1143
1452
  sharedGatingConditions,
1453
+ storageSystemOption,
1144
1454
  terminalToOutput,
1145
1455
  unreadableReading,
1146
1456
  unwrapJsonStringify,
1147
1457
  valueToReadFurtherFrom,
1458
+ walkDescendants,
1459
+ wrapperDiscovery,
1148
1460
  writtenReading
1149
1461
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@suss/extractor",
3
- "version": "0.20.0",
3
+ "version": "0.21.1",
4
4
  "type": "module",
5
5
  "description": "Framework-agnostic extraction pipeline that turns raw extractions into suss behavioral summaries.",
6
6
  "keywords": [
@@ -45,7 +45,8 @@
45
45
  "typecheck": "tsc --noEmit"
46
46
  },
47
47
  "dependencies": {
48
- "@suss/behavioral-ir": "0.20.0"
48
+ "@suss/behavioral-ir": "0.21.1",
49
+ "zod": "^4.0.0"
49
50
  },
50
51
  "devDependencies": {
51
52
  "tsup": "*",