deepline 0.2.18 → 0.2.19

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.
@@ -160,7 +160,7 @@ export const SDK_RELEASE = {
160
160
  // 0.2.0 makes Dataset Handles uniformly async-only after 0.1.320 briefly
161
161
  // exposed storage-dependent synchronous access. This deliberate minor
162
162
  // release keeps lazy paging semantics independent of row residency.
163
- version: '0.2.18',
163
+ version: '0.2.19',
164
164
  contracts: {
165
165
  api: {
166
166
  name: 'sdk-http-api',
@@ -0,0 +1,27 @@
1
+ import {
2
+ createStaticPipelineStorageFields,
3
+ createStoredStaticPipelineV2,
4
+ hydrateStoredStaticPipelineV2,
5
+ readStoredStaticPipeline,
6
+ } from './static-pipeline';
7
+
8
+ export {
9
+ isStoredStaticPipelineV2IntegrityError,
10
+ STATIC_PIPELINE_V2_INTEGRITY_ERROR_CODE,
11
+ STATIC_PIPELINE_V2_INTEGRITY_USER_MESSAGE,
12
+ StoredStaticPipelineV2IntegrityError,
13
+ } from './static-pipeline';
14
+
15
+ /**
16
+ * Node- and Convex-agnostic codec for the durable static-pipeline format.
17
+ *
18
+ * `flatten` is the only supported recursive graph -> flat V2 conversion.
19
+ * `hydrate` is its strict inverse. `storageFields` creates the complete V2 plus
20
+ * the bounded V1 compatibility mirror. `read` handles legacy rows with no V2.
21
+ */
22
+ export const staticPipelineStorageCodec = Object.freeze({
23
+ flatten: createStoredStaticPipelineV2,
24
+ hydrate: hydrateStoredStaticPipelineV2,
25
+ storageFields: createStaticPipelineStorageFields,
26
+ read: readStoredStaticPipeline,
27
+ });
@@ -43,6 +43,45 @@ export interface PlayStaticPipeline {
43
43
  sheetContractErrors?: string[];
44
44
  }
45
45
 
46
+ /**
47
+ * Durable V2 representation for static pipelines. Convex documents have a
48
+ * finite nesting limit, so this stores the recursive graph as flat records.
49
+ * The V1 `staticPipeline` field remains a small compatibility projection for
50
+ * legacy readers; V2 is rehydrated before current readers consume it.
51
+ */
52
+ export interface StoredStaticPipelineV2 {
53
+ version: 2;
54
+ rootPipelineId: string;
55
+ pipelines: StoredStaticPipelineV2Pipeline[];
56
+ nodes: StoredStaticPipelineV2Node[];
57
+ branches: StoredStaticPipelineV2Branch[];
58
+ }
59
+
60
+ export interface StoredStaticPipelineV2Pipeline {
61
+ id: string;
62
+ parentNodeId?: string;
63
+ value: Record<string, unknown>;
64
+ }
65
+
66
+ export interface StoredStaticPipelineV2Node {
67
+ id: string;
68
+ pipelineId: string;
69
+ parentNodeId?: string;
70
+ branchId?: string;
71
+ container: 'stages' | 'substeps' | 'steps' | 'branch_steps';
72
+ index: number;
73
+ nestedPipelineId?: string;
74
+ value: Record<string, unknown>;
75
+ }
76
+
77
+ export interface StoredStaticPipelineV2Branch {
78
+ id: string;
79
+ nodeId: string;
80
+ index: number;
81
+ label: string;
82
+ condition?: string;
83
+ }
84
+
46
85
  function dedupeStaticFieldNames(
47
86
  fields: Array<string | null | undefined>,
48
87
  ): string[] {
@@ -99,10 +138,7 @@ export function deriveStaticPipelineEntryInputFields(
99
138
  }
100
139
 
101
140
  export type PlaySheetColumnSource =
102
- | 'input'
103
- | 'datasetColumn'
104
- | 'waterfallStep'
105
- | 'childPlayColumn';
141
+ 'input' | 'datasetColumn' | 'waterfallStep' | 'childPlayColumn';
106
142
 
107
143
  export interface PlaySheetColumnContract {
108
144
  id: string;
@@ -546,6 +582,520 @@ export function truncateStaticPipelineForRuntimeContract(
546
582
  });
547
583
  }
548
584
 
585
+ function asStaticRecord(value: unknown): Record<string, unknown> | null {
586
+ return value && typeof value === 'object' && !Array.isArray(value)
587
+ ? (value as Record<string, unknown>)
588
+ : null;
589
+ }
590
+
591
+ function storageSafeNodeValue(
592
+ value: Record<string, unknown>,
593
+ ): Record<string, unknown> {
594
+ const {
595
+ branches: _branches,
596
+ pipeline: _pipeline,
597
+ steps: _steps,
598
+ ...rest
599
+ } = value;
600
+ const out: Record<string, unknown> = { ...rest };
601
+
602
+ // `columns[].producers[].substep` can itself contain a recursive static
603
+ // graph. The sheet contract is the durable interface for columns, while this
604
+ // compact metadata keeps their labels available to older graph views.
605
+ if (Array.isArray(value.columns)) {
606
+ out.columns = value.columns.flatMap((column) => {
607
+ const record = asStaticRecord(column);
608
+ if (!record) return [];
609
+ return [
610
+ omitUndefinedProperties({
611
+ id: record.id,
612
+ source: record.source,
613
+ sqlName: record.sqlName,
614
+ }),
615
+ ];
616
+ });
617
+ }
618
+ if (Array.isArray(value.callPath)) out.callPath = [...value.callPath];
619
+ if (Array.isArray(value.inputFields))
620
+ out.inputFields = [...value.inputFields];
621
+ if (Array.isArray(value.rowKeyFields))
622
+ out.rowKeyFields = [...value.rowKeyFields];
623
+ if (Array.isArray(value.outputFields))
624
+ out.outputFields = [...value.outputFields];
625
+ if (Array.isArray(value.waterfallIds))
626
+ out.waterfallIds = [...value.waterfallIds];
627
+ if (Array.isArray(value.steps) && value.type === 'waterfall') {
628
+ out.steps = value.steps.flatMap((step) => {
629
+ const record = asStaticRecord(step);
630
+ if (!record) return [];
631
+ return [
632
+ omitUndefinedProperties({
633
+ id: record.id,
634
+ kind: record.kind,
635
+ toolId: record.toolId,
636
+ }),
637
+ ];
638
+ });
639
+ }
640
+ if (asStaticRecord(value.sourceRange)) {
641
+ out.sourceRange = { ...(value.sourceRange as Record<string, unknown>) };
642
+ }
643
+ if (asStaticRecord(value.sheetContract)) {
644
+ out.sheetContract = cloneStorageSafeSheetContract(
645
+ value.sheetContract as PlaySheetContract,
646
+ );
647
+ }
648
+ return stripUndefinedDeep(out);
649
+ }
650
+
651
+ function storageSafePipelineValue(
652
+ pipeline: PlayStaticPipeline,
653
+ ): Record<string, unknown> {
654
+ const { stages: _stages, substeps: _substeps, ...value } = pipeline;
655
+ return stripUndefinedDeep(
656
+ omitUndefinedProperties({
657
+ ...value,
658
+ inputFields: pipeline.inputFields ? [...pipeline.inputFields] : undefined,
659
+ rowKeyFields: pipeline.rowKeyFields
660
+ ? [...pipeline.rowKeyFields]
661
+ : undefined,
662
+ fields: [...(pipeline.fields ?? [])],
663
+ returnFields: pipeline.returnFields?.map((field) => ({ ...field })),
664
+ sheetContract: cloneStorageSafeSheetContract(pipeline.sheetContract),
665
+ sheetContractErrors: pipeline.sheetContractErrors
666
+ ? [...pipeline.sheetContractErrors]
667
+ : undefined,
668
+ }),
669
+ );
670
+ }
671
+
672
+ /** Creates the complete flat V2 graph from a bounded, storage-safe pipeline. */
673
+ export function createStoredStaticPipelineV2(
674
+ pipeline: PlayStaticPipeline | null | undefined,
675
+ ): StoredStaticPipelineV2 | null | undefined {
676
+ if (pipeline === null) return null;
677
+ if (pipeline === undefined) return undefined;
678
+
679
+ const bounded = truncateStaticPipelineForStorage(pipeline, {
680
+ maxEmbeddedPlayCallPipelineDepth: Number.POSITIVE_INFINITY,
681
+ maxStoredSubstepDepth: Number.POSITIVE_INFINITY,
682
+ });
683
+ if (!bounded) {
684
+ throw new Error(
685
+ 'Static pipeline truncation unexpectedly returned no graph',
686
+ );
687
+ }
688
+
689
+ const result: StoredStaticPipelineV2 = {
690
+ version: 2,
691
+ rootPipelineId: 'pipeline:0',
692
+ pipelines: [],
693
+ nodes: [],
694
+ branches: [],
695
+ };
696
+ let nextPipeline = 0;
697
+ let nextNode = 0;
698
+ let nextBranch = 0;
699
+
700
+ const appendPipeline = (
701
+ current: PlayStaticPipeline,
702
+ parentNodeId?: string,
703
+ ): string => {
704
+ const pipelineId = `pipeline:${nextPipeline++}`;
705
+ result.pipelines.push({
706
+ id: pipelineId,
707
+ ...(parentNodeId ? { parentNodeId } : {}),
708
+ value: storageSafePipelineValue(current),
709
+ });
710
+
711
+ const appendNodes = (
712
+ entries: unknown[],
713
+ container: StoredStaticPipelineV2Node['container'],
714
+ parentNodeId?: string,
715
+ branchId?: string,
716
+ ) => {
717
+ entries.forEach((entry, index) => {
718
+ const record = asStaticRecord(entry);
719
+ if (!record) return;
720
+ const nodeId = `node:${nextNode++}`;
721
+ const nested = asStaticRecord(record.pipeline);
722
+ const node: StoredStaticPipelineV2Node = {
723
+ id: nodeId,
724
+ pipelineId,
725
+ ...(parentNodeId ? { parentNodeId } : {}),
726
+ ...(branchId ? { branchId } : {}),
727
+ container,
728
+ index,
729
+ value: storageSafeNodeValue(record),
730
+ };
731
+ result.nodes.push(node);
732
+
733
+ if (nested) {
734
+ node.nestedPipelineId = appendPipeline(
735
+ nested as unknown as PlayStaticPipeline,
736
+ nodeId,
737
+ );
738
+ }
739
+ if (Array.isArray(record.steps) && record.type !== 'waterfall') {
740
+ appendNodes(record.steps, 'steps', nodeId);
741
+ }
742
+ if (Array.isArray(record.branches)) {
743
+ record.branches.forEach((branch, branchIndex) => {
744
+ const branchRecord = asStaticRecord(branch);
745
+ if (!branchRecord || typeof branchRecord.label !== 'string') return;
746
+ const id = `branch:${nextBranch++}`;
747
+ result.branches.push(
748
+ omitUndefinedProperties({
749
+ id,
750
+ nodeId,
751
+ index: branchIndex,
752
+ label: branchRecord.label,
753
+ condition:
754
+ typeof branchRecord.condition === 'string'
755
+ ? branchRecord.condition
756
+ : undefined,
757
+ }),
758
+ );
759
+ if (Array.isArray(branchRecord.steps)) {
760
+ appendNodes(branchRecord.steps, 'branch_steps', nodeId, id);
761
+ }
762
+ });
763
+ }
764
+ });
765
+ };
766
+
767
+ appendNodes(current.stages ?? [], 'stages');
768
+ appendNodes(current.substeps ?? [], 'substeps');
769
+ return pipelineId;
770
+ };
771
+
772
+ result.rootPipelineId = appendPipeline(bounded);
773
+ return result;
774
+ }
775
+
776
+ export const STATIC_PIPELINE_V2_INTEGRITY_ERROR_CODE =
777
+ 'PLAY_STATIC_PIPELINE_V2_INVALID';
778
+ export const STATIC_PIPELINE_V2_INTEGRITY_USER_MESSAGE =
779
+ "This Play's stored graph is inconsistent. Republish the Play to repair it before running it again.";
780
+
781
+ export class StoredStaticPipelineV2IntegrityError extends Error {
782
+ constructor(reason: string) {
783
+ super(
784
+ `${STATIC_PIPELINE_V2_INTEGRITY_ERROR_CODE}: ${STATIC_PIPELINE_V2_INTEGRITY_USER_MESSAGE} Internal reason: ${reason}.`,
785
+ );
786
+ this.name = 'StoredStaticPipelineV2IntegrityError';
787
+ }
788
+ }
789
+
790
+ export function isStoredStaticPipelineV2IntegrityError(
791
+ error: unknown,
792
+ ): boolean {
793
+ return (
794
+ error instanceof StoredStaticPipelineV2IntegrityError ||
795
+ (error instanceof Error &&
796
+ error.message.includes(STATIC_PIPELINE_V2_INTEGRITY_ERROR_CODE))
797
+ );
798
+ }
799
+
800
+ function invalidStoredStaticPipelineV2(reason: string): never {
801
+ throw new StoredStaticPipelineV2IntegrityError(reason);
802
+ }
803
+
804
+ /**
805
+ * Reads a present V2 envelope back into the existing in-memory pipeline shape.
806
+ * Missing V2 is a supported legacy state. Present but malformed V2 is durable
807
+ * corruption and fails loudly rather than running the lossy V1 projection.
808
+ */
809
+ export function hydrateStoredStaticPipelineV2(
810
+ value: unknown,
811
+ ): PlayStaticPipeline | null {
812
+ if (value === undefined || value === null) return null;
813
+ const record = asStaticRecord(value);
814
+ if (
815
+ !record ||
816
+ record.version !== 2 ||
817
+ typeof record.rootPipelineId !== 'string' ||
818
+ !Array.isArray(record.pipelines) ||
819
+ !Array.isArray(record.nodes) ||
820
+ !Array.isArray(record.branches)
821
+ ) {
822
+ return invalidStoredStaticPipelineV2('invalid envelope');
823
+ }
824
+ const pipelines = new Map<string, PlayStaticPipeline>();
825
+ for (const entry of record.pipelines) {
826
+ const pipeline = asStaticRecord(entry);
827
+ if (!pipeline || typeof pipeline.id !== 'string') {
828
+ return invalidStoredStaticPipelineV2('invalid pipeline record');
829
+ }
830
+ if (pipelines.has(pipeline.id)) {
831
+ return invalidStoredStaticPipelineV2(
832
+ `duplicate pipeline id ${pipeline.id}`,
833
+ );
834
+ }
835
+ const pipelineValue = asStaticRecord(pipeline.value);
836
+ if (
837
+ !pipelineValue ||
838
+ !Array.isArray(pipelineValue.fields) ||
839
+ pipelineValue.fields.some((field) => typeof field !== 'string')
840
+ ) {
841
+ return invalidStoredStaticPipelineV2(
842
+ `invalid pipeline value for ${pipeline.id}`,
843
+ );
844
+ }
845
+ pipelines.set(pipeline.id, {
846
+ ...(pipelineValue as unknown as PlayStaticPipeline),
847
+ stages: [],
848
+ substeps: [],
849
+ });
850
+ }
851
+ const root = pipelines.get(record.rootPipelineId);
852
+ if (!root) {
853
+ return invalidStoredStaticPipelineV2(
854
+ `missing root pipeline ${record.rootPipelineId}`,
855
+ );
856
+ }
857
+
858
+ const nodes = new Map<string, Record<string, unknown>>();
859
+ const nodeRecords = record.nodes.map((entry, position) => {
860
+ const node = asStaticRecord(entry);
861
+ if (!node) {
862
+ return invalidStoredStaticPipelineV2(
863
+ `invalid node record at position ${position}`,
864
+ );
865
+ }
866
+ return node;
867
+ });
868
+ nodeRecords.sort((a, b) => Number(a.index) - Number(b.index));
869
+ for (const node of nodeRecords) {
870
+ if (
871
+ typeof node.id !== 'string' ||
872
+ typeof node.pipelineId !== 'string' ||
873
+ typeof node.container !== 'string' ||
874
+ typeof node.index !== 'number' ||
875
+ !Number.isSafeInteger(node.index) ||
876
+ node.index < 0
877
+ )
878
+ return invalidStoredStaticPipelineV2('invalid node identity');
879
+ if (nodes.has(node.id)) {
880
+ return invalidStoredStaticPipelineV2(`duplicate node id ${node.id}`);
881
+ }
882
+ const nodeValue = asStaticRecord(node.value);
883
+ if (!nodeValue || typeof nodeValue.type !== 'string') {
884
+ return invalidStoredStaticPipelineV2(`invalid node value for ${node.id}`);
885
+ }
886
+ const hydratedNode = { ...nodeValue };
887
+ if (
888
+ hydratedNode.type === 'dataset' ||
889
+ hydratedNode.type === 'step_suite' ||
890
+ hydratedNode.type === 'control_flow'
891
+ ) {
892
+ hydratedNode.steps = [];
893
+ }
894
+ nodes.set(node.id, hydratedNode);
895
+ }
896
+ const branches = new Map<string, Record<string, unknown>>();
897
+ for (const entry of record.branches) {
898
+ const branch = asStaticRecord(entry);
899
+ if (
900
+ !branch ||
901
+ typeof branch.id !== 'string' ||
902
+ typeof branch.nodeId !== 'string' ||
903
+ typeof branch.label !== 'string' ||
904
+ typeof branch.index !== 'number' ||
905
+ !Number.isSafeInteger(branch.index) ||
906
+ branch.index < 0
907
+ )
908
+ return invalidStoredStaticPipelineV2('invalid branch record');
909
+ if (branches.has(branch.id)) {
910
+ return invalidStoredStaticPipelineV2(`duplicate branch id ${branch.id}`);
911
+ }
912
+ branches.set(
913
+ branch.id,
914
+ omitUndefinedProperties({
915
+ label: branch.label,
916
+ condition:
917
+ typeof branch.condition === 'string' ? branch.condition : undefined,
918
+ steps: [] as PlayStaticSubstep[],
919
+ }),
920
+ );
921
+ }
922
+ const nestedPipelineParents = new Map<string, string>();
923
+ for (const node of nodeRecords) {
924
+ if (typeof node.nestedPipelineId !== 'string') continue;
925
+ const parentPipelineId = node.pipelineId as string;
926
+ const nestedPipelineId = node.nestedPipelineId;
927
+ if (!pipelines.has(nestedPipelineId)) {
928
+ return invalidStoredStaticPipelineV2(
929
+ `node ${String(node.id)} references a missing nested pipeline`,
930
+ );
931
+ }
932
+ if (nestedPipelineParents.has(nestedPipelineId)) {
933
+ return invalidStoredStaticPipelineV2(
934
+ `nested pipeline ${nestedPipelineId} has multiple parents`,
935
+ );
936
+ }
937
+ nestedPipelineParents.set(nestedPipelineId, parentPipelineId);
938
+ let ancestor: string | undefined = parentPipelineId;
939
+ while (ancestor !== undefined) {
940
+ if (ancestor === nestedPipelineId) {
941
+ return invalidStoredStaticPipelineV2(
942
+ `nested pipeline ${nestedPipelineId} forms a cycle`,
943
+ );
944
+ }
945
+ ancestor = nestedPipelineParents.get(ancestor);
946
+ }
947
+ }
948
+ for (const node of nodeRecords) {
949
+ const nodeValue = nodes.get(node.id as string);
950
+ const pipeline = pipelines.get(node.pipelineId as string);
951
+ if (!nodeValue || !pipeline) {
952
+ return invalidStoredStaticPipelineV2(
953
+ `node ${String(node.id)} references a missing pipeline`,
954
+ );
955
+ }
956
+ const container = node.container as StoredStaticPipelineV2Node['container'];
957
+ if (container === 'stages' || container === 'substeps') {
958
+ (pipeline[container] as PlayStaticSubstep[]).push(
959
+ nodeValue as PlayStaticSubstep,
960
+ );
961
+ } else if (container === 'steps') {
962
+ const parent = nodes.get(node.parentNodeId as string);
963
+ if (!parent) {
964
+ return invalidStoredStaticPipelineV2(
965
+ `node ${String(node.id)} references a missing parent`,
966
+ );
967
+ }
968
+ const steps = (parent.steps ??= []) as PlayStaticSubstep[];
969
+ steps.push(nodeValue as PlayStaticSubstep);
970
+ } else if (container === 'branch_steps') {
971
+ const branch = branches.get(node.branchId as string);
972
+ if (!branch) {
973
+ return invalidStoredStaticPipelineV2(
974
+ `node ${String(node.id)} references a missing branch`,
975
+ );
976
+ }
977
+ (branch.steps as PlayStaticSubstep[]).push(
978
+ nodeValue as PlayStaticSubstep,
979
+ );
980
+ } else {
981
+ return invalidStoredStaticPipelineV2(
982
+ `node ${String(node.id)} has an invalid container`,
983
+ );
984
+ }
985
+ if (typeof node.nestedPipelineId === 'string') {
986
+ const nested = pipelines.get(node.nestedPipelineId)!;
987
+ nodeValue.pipeline = nested;
988
+ }
989
+ }
990
+ for (const branchRecord of record.branches) {
991
+ const branch = asStaticRecord(branchRecord);
992
+ if (!branch) continue;
993
+ const parent = nodes.get(branch.nodeId as string);
994
+ const hydrated = branches.get(branch.id as string);
995
+ if (!parent || !hydrated) {
996
+ return invalidStoredStaticPipelineV2(
997
+ `branch ${String(branch.id)} references a missing node`,
998
+ );
999
+ }
1000
+ const parentBranches = (parent.branches ??= []) as Record<
1001
+ string,
1002
+ unknown
1003
+ >[];
1004
+ parentBranches.push(hydrated);
1005
+ }
1006
+
1007
+ // V2 deliberately omits the recursive `columns[].producers[].substep`
1008
+ // structure from node values to stay below Convex's document-depth limit.
1009
+ // Rebuild that derived provenance from each dataset's hydrated steps before
1010
+ // returning the ordinary in-memory pipeline shape.
1011
+ for (const pipeline of [...pipelines.values()].reverse()) {
1012
+ const pipelineSubsteps = pipeline.substeps ?? [];
1013
+ const compile = (substep: PlayStaticSubstep) =>
1014
+ compileStaticGraphSubstep(substep, pipelineSubsteps, []);
1015
+ pipeline.stages = (pipeline.stages ?? []).map(compile);
1016
+ pipeline.substeps = pipelineSubsteps.map(compile);
1017
+ }
1018
+ return root;
1019
+ }
1020
+
1021
+ function storageObjectDepth(value: unknown): number {
1022
+ if (!value || typeof value !== 'object') return 0;
1023
+ const children = Array.isArray(value)
1024
+ ? value
1025
+ : Object.values(value as Record<string, unknown>);
1026
+ return (
1027
+ 1 +
1028
+ children.reduce((max, child) => Math.max(max, storageObjectDepth(child)), 0)
1029
+ );
1030
+ }
1031
+
1032
+ /**
1033
+ * V1 remains byte-for-byte shape compatible whenever it is safely shallow.
1034
+ * Only a graph that would leave too little room for its enclosing Convex
1035
+ * document receives the compact compatibility projection.
1036
+ */
1037
+ export function createStaticPipelineV1CompatibilityProjection(
1038
+ pipeline: PlayStaticPipeline | null | undefined,
1039
+ ): PlayStaticPipeline | null | undefined {
1040
+ if (!pipeline) return pipeline;
1041
+ const stored = truncateStaticPipelineForStorage(pipeline);
1042
+ if (!stored) return stored;
1043
+ // The containing `playRevisions` / `playRuns` document adds several object
1044
+ // levels. Keep the V1 graph below this conservative bound; V2 has the full
1045
+ // graph for all current readers.
1046
+ if (storageObjectDepth(stored) <= 11) return stored;
1047
+ const shallow = (substeps: PlayStaticSubstep[] | undefined) =>
1048
+ (substeps ?? []).map((substep) => {
1049
+ const value = storageSafeNodeValue(
1050
+ substep as unknown as Record<string, unknown>,
1051
+ );
1052
+ if (
1053
+ substep.type === 'dataset' ||
1054
+ substep.type === 'step_suite' ||
1055
+ substep.type === 'control_flow'
1056
+ ) {
1057
+ value.steps = [];
1058
+ }
1059
+ return value as PlayStaticSubstep;
1060
+ });
1061
+ return {
1062
+ ...storageSafePipelineValue(stored),
1063
+ stages: shallow(stored.stages),
1064
+ substeps: shallow(stored.substeps),
1065
+ } as PlayStaticPipeline;
1066
+ }
1067
+
1068
+ /**
1069
+ * Creates the two transport-safe fields persisted by Convex Play records.
1070
+ * Call this before crossing a Convex function boundary: the complete graph is
1071
+ * flat in V2, while V1 remains a deliberately bounded compatibility view.
1072
+ */
1073
+ export function createStaticPipelineStorageFields(
1074
+ pipeline: PlayStaticPipeline | null | undefined,
1075
+ ): {
1076
+ staticPipeline?: PlayStaticPipeline | null;
1077
+ staticPipelineV2?: StoredStaticPipelineV2 | null;
1078
+ } {
1079
+ if (pipeline === undefined) return {};
1080
+ return {
1081
+ staticPipeline: createStaticPipelineV1CompatibilityProjection(pipeline),
1082
+ staticPipelineV2: createStoredStaticPipelineV2(pipeline),
1083
+ };
1084
+ }
1085
+
1086
+ /** Prefers complete V2. Falls back only when a legacy row has no V2 value. */
1087
+ export function readStoredStaticPipeline(input: {
1088
+ staticPipeline?: unknown;
1089
+ staticPipelineV2?: unknown;
1090
+ }): PlayStaticPipeline | null {
1091
+ if (input.staticPipelineV2 !== undefined && input.staticPipelineV2 !== null) {
1092
+ return hydrateStoredStaticPipelineV2(input.staticPipelineV2);
1093
+ }
1094
+ return (
1095
+ (input.staticPipeline as PlayStaticPipeline | null | undefined) ?? null
1096
+ );
1097
+ }
1098
+
549
1099
  export interface PlayStaticSourceRange {
550
1100
  sourcePath?: string;
551
1101
  startLine: number;
@@ -923,15 +1473,20 @@ function columnProducerFromSubstep(
923
1473
  substep: PlayStaticSubstep,
924
1474
  field: string,
925
1475
  ): PlayStaticColumnProducer {
926
- const steps =
927
- substep.type === 'step_suite' || substep.type === 'control_flow'
928
- ? substep.steps
929
- .map((step) => {
930
- const stepField = fieldForColumnProducer(step) ?? field;
931
- return columnProducerFromSubstep(step, stepField);
932
- })
933
- .filter((producer) => producer.field.trim())
934
- : undefined;
1476
+ const nestedProducerSubsteps =
1477
+ substep.type === 'control_flow' && substep.steps.length === 0
1478
+ ? (substep.branches ?? []).flatMap((branch) => branch.steps)
1479
+ : substep.type === 'step_suite' || substep.type === 'control_flow'
1480
+ ? substep.steps
1481
+ : null;
1482
+ const steps = nestedProducerSubsteps
1483
+ ? nestedProducerSubsteps
1484
+ .map((step) => {
1485
+ const stepField = fieldForColumnProducer(step) ?? field;
1486
+ return columnProducerFromSubstep(step, stepField);
1487
+ })
1488
+ .filter((producer) => producer.field.trim())
1489
+ : undefined;
935
1490
  const kind: PlayStaticColumnProducerKind =
936
1491
  substep.type === 'tool'
937
1492
  ? 'tool'
package/dist/cli/index.js CHANGED
@@ -1044,7 +1044,7 @@ var SDK_RELEASE = {
1044
1044
  // 0.2.0 makes Dataset Handles uniformly async-only after 0.1.320 briefly
1045
1045
  // exposed storage-dependent synchronous access. This deliberate minor
1046
1046
  // release keeps lazy paging semantics independent of row residency.
1047
- version: "0.2.18",
1047
+ version: "0.2.19",
1048
1048
  contracts: {
1049
1049
  api: {
1050
1050
  name: "sdk-http-api",
@@ -1030,7 +1030,7 @@ var SDK_RELEASE = {
1030
1030
  // 0.2.0 makes Dataset Handles uniformly async-only after 0.1.320 briefly
1031
1031
  // exposed storage-dependent synchronous access. This deliberate minor
1032
1032
  // release keeps lazy paging semantics independent of row residency.
1033
- version: "0.2.18",
1033
+ version: "0.2.19",
1034
1034
  contracts: {
1035
1035
  api: {
1036
1036
  name: "sdk-http-api",
package/dist/index.js CHANGED
@@ -763,7 +763,7 @@ var SDK_RELEASE = {
763
763
  // 0.2.0 makes Dataset Handles uniformly async-only after 0.1.320 briefly
764
764
  // exposed storage-dependent synchronous access. This deliberate minor
765
765
  // release keeps lazy paging semantics independent of row residency.
766
- version: "0.2.18",
766
+ version: "0.2.19",
767
767
  contracts: {
768
768
  api: {
769
769
  name: "sdk-http-api",
package/dist/index.mjs CHANGED
@@ -689,7 +689,7 @@ var SDK_RELEASE = {
689
689
  // 0.2.0 makes Dataset Handles uniformly async-only after 0.1.320 briefly
690
690
  // exposed storage-dependent synchronous access. This deliberate minor
691
691
  // release keeps lazy paging semantics independent of row residency.
692
- version: "0.2.18",
692
+ version: "0.2.19",
693
693
  contracts: {
694
694
  api: {
695
695
  name: "sdk-http-api",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deepline",
3
- "version": "0.2.18",
3
+ "version": "0.2.19",
4
4
  "description": "Deepline SDK + CLI — B2B data enrichment powered by durable cloud execution",
5
5
  "license": "MIT",
6
6
  "repository": {