deepline 0.2.18 → 0.2.20

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.20',
164
164
  contracts: {
165
165
  api: {
166
166
  name: 'sdk-http-api',
@@ -5,34 +5,6 @@ const PRIVATE_KEY_PATTERN =
5
5
  const BEARER_LITERAL_PATTERN = /\bBearer\s+[A-Za-z0-9._~+/=-]{16,}/i;
6
6
  const ASSIGNMENT_SECRET_LITERAL_PATTERN =
7
7
  /\b(?:api[_-]?key|token|secret|password)\b\s*[:=]\s*['"][^'"]{12,}['"]/i;
8
- const HIGH_ENTROPY_LITERAL_PATTERN = /['"]([A-Za-z0-9+/=_-]{32,})['"]/g;
9
- const UUID_IDENTIFIER_PATTERN =
10
- /^((?:[A-Za-z0-9]+[-_])*)?([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/i;
11
- const BOOTSTRAP_RESOURCE_IDENTIFIER_PATTERN =
12
- /^bootstrap-[0-9a-f]{32}(?:\/[a-z0-9][a-z0-9_-]{0,127})?$/i;
13
- const SECRET_LABEL_PATTERN =
14
- /(?:^|[-_])(?:api|auth|access|secret|token|key|password|credential|bearer|sk|pk|live)(?:[-_]|$)/i;
15
-
16
- function shannonEntropy(value: string): number {
17
- const counts = new Map<string, number>();
18
- for (const char of value) counts.set(char, (counts.get(char) ?? 0) + 1);
19
- return [...counts.values()].reduce((entropy, count) => {
20
- const p = count / value.length;
21
- return entropy - p * Math.log2(p);
22
- }, 0);
23
- }
24
-
25
- function isNonSecretUuidIdentifier(value: string): boolean {
26
- const match = UUID_IDENTIFIER_PATTERN.exec(value);
27
- if (!match) return false;
28
- const label = match[1] ?? '';
29
- return !SECRET_LABEL_PATTERN.test(label);
30
- }
31
-
32
- function isNonSecretBootstrapResourceIdentifier(value: string): boolean {
33
- return BOOTSTRAP_RESOURCE_IDENTIFIER_PATTERN.test(value);
34
- }
35
-
36
8
  /**
37
9
  * Returns the inline-secret findings in a string (empty if none). The throwing
38
10
  * validator below and the workflows→plays migration validator both call this so
@@ -50,20 +22,6 @@ export function collectInlineSecretFindings(sourceCode: string): string[] {
50
22
  if (ASSIGNMENT_SECRET_LITERAL_PATTERN.test(sourceCode)) {
51
23
  findings.push('secret-looking assignment literal');
52
24
  }
53
- for (const match of sourceCode.matchAll(HIGH_ENTROPY_LITERAL_PATTERN)) {
54
- const literal = match[1] ?? '';
55
- // UUID-bearing resource names are structured identifiers, not opaque
56
- // credentials. Keep secret-looking labels on the conservative path.
57
- if (isNonSecretUuidIdentifier(literal)) continue;
58
- // Named CI orgs use a deterministic public `bootstrap-<sha256-prefix>`
59
- // slug. It is an address, not a credential, and owner-qualified ctx.runPlay
60
- // references must embed it as a literal for static child resolution.
61
- if (isNonSecretBootstrapResourceIdentifier(literal)) continue;
62
- if (literal.length >= 40 && shannonEntropy(literal) >= 4.2) {
63
- findings.push('high-entropy string literal');
64
- break;
65
- }
66
- }
67
25
  return [...new Set(findings)];
68
26
  }
69
27
 
@@ -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.20",
1048
1048
  contracts: {
1049
1049
  api: {
1050
1050
  name: "sdk-http-api",
@@ -6479,6 +6479,10 @@ function collectLocalEnvInfo() {
6479
6479
  function readCsvRows(csvPath) {
6480
6480
  const raw = (0, import_node_fs4.readFileSync)((0, import_node_path4.resolve)(csvPath), "utf-8");
6481
6481
  return (0, import_sync.parse)(raw, {
6482
+ // `csv-parse` otherwise treats a BOM before an opening quote as a field
6483
+ // value, then rejects the quote as invalid. A UTF-8 BOM is a valid file
6484
+ // prefix and must not become part of the first column name either.
6485
+ bom: true,
6482
6486
  columns: true,
6483
6487
  skip_empty_lines: true
6484
6488
  });
@@ -15923,27 +15927,6 @@ var SECRET_ENV_PATTERN = /\bprocess(?:\.env|\[['"]env['"]\])(?:\.|\[['"])([A-Z0-
15923
15927
  var PRIVATE_KEY_PATTERN = /-----BEGIN (?:RSA |EC |OPENSSH |PGP )?PRIVATE KEY-----/;
15924
15928
  var BEARER_LITERAL_PATTERN = /\bBearer\s+[A-Za-z0-9._~+/=-]{16,}/i;
15925
15929
  var ASSIGNMENT_SECRET_LITERAL_PATTERN = /\b(?:api[_-]?key|token|secret|password)\b\s*[:=]\s*['"][^'"]{12,}['"]/i;
15926
- var HIGH_ENTROPY_LITERAL_PATTERN = /['"]([A-Za-z0-9+/=_-]{32,})['"]/g;
15927
- var UUID_IDENTIFIER_PATTERN = /^((?:[A-Za-z0-9]+[-_])*)?([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/i;
15928
- var BOOTSTRAP_RESOURCE_IDENTIFIER_PATTERN = /^bootstrap-[0-9a-f]{32}(?:\/[a-z0-9][a-z0-9_-]{0,127})?$/i;
15929
- var SECRET_LABEL_PATTERN = /(?:^|[-_])(?:api|auth|access|secret|token|key|password|credential|bearer|sk|pk|live)(?:[-_]|$)/i;
15930
- function shannonEntropy(value) {
15931
- const counts = /* @__PURE__ */ new Map();
15932
- for (const char of value) counts.set(char, (counts.get(char) ?? 0) + 1);
15933
- return [...counts.values()].reduce((entropy, count) => {
15934
- const p = count / value.length;
15935
- return entropy - p * Math.log2(p);
15936
- }, 0);
15937
- }
15938
- function isNonSecretUuidIdentifier(value) {
15939
- const match = UUID_IDENTIFIER_PATTERN.exec(value);
15940
- if (!match) return false;
15941
- const label = match[1] ?? "";
15942
- return !SECRET_LABEL_PATTERN.test(label);
15943
- }
15944
- function isNonSecretBootstrapResourceIdentifier(value) {
15945
- return BOOTSTRAP_RESOURCE_IDENTIFIER_PATTERN.test(value);
15946
- }
15947
15930
  function collectInlineSecretFindings(sourceCode) {
15948
15931
  const findings = [];
15949
15932
  for (const match of sourceCode.matchAll(SECRET_ENV_PATTERN)) {
@@ -15955,15 +15938,6 @@ function collectInlineSecretFindings(sourceCode) {
15955
15938
  if (ASSIGNMENT_SECRET_LITERAL_PATTERN.test(sourceCode)) {
15956
15939
  findings.push("secret-looking assignment literal");
15957
15940
  }
15958
- for (const match of sourceCode.matchAll(HIGH_ENTROPY_LITERAL_PATTERN)) {
15959
- const literal = match[1] ?? "";
15960
- if (isNonSecretUuidIdentifier(literal)) continue;
15961
- if (isNonSecretBootstrapResourceIdentifier(literal)) continue;
15962
- if (literal.length >= 40 && shannonEntropy(literal) >= 4.2) {
15963
- findings.push("high-entropy string literal");
15964
- break;
15965
- }
15966
- }
15967
15941
  return [...new Set(findings)];
15968
15942
  }
15969
15943
 
@@ -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.20",
1034
1034
  contracts: {
1035
1035
  api: {
1036
1036
  name: "sdk-http-api",
@@ -6477,6 +6477,10 @@ function collectLocalEnvInfo() {
6477
6477
  function readCsvRows(csvPath) {
6478
6478
  const raw = readFileSync4(resolve2(csvPath), "utf-8");
6479
6479
  return parse(raw, {
6480
+ // `csv-parse` otherwise treats a BOM before an opening quote as a field
6481
+ // value, then rejects the quote as invalid. A UTF-8 BOM is a valid file
6482
+ // prefix and must not become part of the first column name either.
6483
+ bom: true,
6480
6484
  columns: true,
6481
6485
  skip_empty_lines: true
6482
6486
  });
@@ -15960,27 +15964,6 @@ var SECRET_ENV_PATTERN = /\bprocess(?:\.env|\[['"]env['"]\])(?:\.|\[['"])([A-Z0-
15960
15964
  var PRIVATE_KEY_PATTERN = /-----BEGIN (?:RSA |EC |OPENSSH |PGP )?PRIVATE KEY-----/;
15961
15965
  var BEARER_LITERAL_PATTERN = /\bBearer\s+[A-Za-z0-9._~+/=-]{16,}/i;
15962
15966
  var ASSIGNMENT_SECRET_LITERAL_PATTERN = /\b(?:api[_-]?key|token|secret|password)\b\s*[:=]\s*['"][^'"]{12,}['"]/i;
15963
- var HIGH_ENTROPY_LITERAL_PATTERN = /['"]([A-Za-z0-9+/=_-]{32,})['"]/g;
15964
- var UUID_IDENTIFIER_PATTERN = /^((?:[A-Za-z0-9]+[-_])*)?([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/i;
15965
- var BOOTSTRAP_RESOURCE_IDENTIFIER_PATTERN = /^bootstrap-[0-9a-f]{32}(?:\/[a-z0-9][a-z0-9_-]{0,127})?$/i;
15966
- var SECRET_LABEL_PATTERN = /(?:^|[-_])(?:api|auth|access|secret|token|key|password|credential|bearer|sk|pk|live)(?:[-_]|$)/i;
15967
- function shannonEntropy(value) {
15968
- const counts = /* @__PURE__ */ new Map();
15969
- for (const char of value) counts.set(char, (counts.get(char) ?? 0) + 1);
15970
- return [...counts.values()].reduce((entropy, count) => {
15971
- const p = count / value.length;
15972
- return entropy - p * Math.log2(p);
15973
- }, 0);
15974
- }
15975
- function isNonSecretUuidIdentifier(value) {
15976
- const match = UUID_IDENTIFIER_PATTERN.exec(value);
15977
- if (!match) return false;
15978
- const label = match[1] ?? "";
15979
- return !SECRET_LABEL_PATTERN.test(label);
15980
- }
15981
- function isNonSecretBootstrapResourceIdentifier(value) {
15982
- return BOOTSTRAP_RESOURCE_IDENTIFIER_PATTERN.test(value);
15983
- }
15984
15967
  function collectInlineSecretFindings(sourceCode) {
15985
15968
  const findings = [];
15986
15969
  for (const match of sourceCode.matchAll(SECRET_ENV_PATTERN)) {
@@ -15992,15 +15975,6 @@ function collectInlineSecretFindings(sourceCode) {
15992
15975
  if (ASSIGNMENT_SECRET_LITERAL_PATTERN.test(sourceCode)) {
15993
15976
  findings.push("secret-looking assignment literal");
15994
15977
  }
15995
- for (const match of sourceCode.matchAll(HIGH_ENTROPY_LITERAL_PATTERN)) {
15996
- const literal = match[1] ?? "";
15997
- if (isNonSecretUuidIdentifier(literal)) continue;
15998
- if (isNonSecretBootstrapResourceIdentifier(literal)) continue;
15999
- if (literal.length >= 40 && shannonEntropy(literal) >= 4.2) {
16000
- findings.push("high-entropy string literal");
16001
- break;
16002
- }
16003
- }
16004
15978
  return [...new Set(findings)];
16005
15979
  }
16006
15980
 
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.20",
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.20",
693
693
  contracts: {
694
694
  api: {
695
695
  name: "sdk-http-api",
@@ -3921,27 +3921,6 @@ var SECRET_ENV_PATTERN = /\bprocess(?:\.env|\[['"]env['"]\])(?:\.|\[['"])([A-Z0-
3921
3921
  var PRIVATE_KEY_PATTERN = /-----BEGIN (?:RSA |EC |OPENSSH |PGP )?PRIVATE KEY-----/;
3922
3922
  var BEARER_LITERAL_PATTERN = /\bBearer\s+[A-Za-z0-9._~+/=-]{16,}/i;
3923
3923
  var ASSIGNMENT_SECRET_LITERAL_PATTERN = /\b(?:api[_-]?key|token|secret|password)\b\s*[:=]\s*['"][^'"]{12,}['"]/i;
3924
- var HIGH_ENTROPY_LITERAL_PATTERN = /['"]([A-Za-z0-9+/=_-]{32,})['"]/g;
3925
- var UUID_IDENTIFIER_PATTERN = /^((?:[A-Za-z0-9]+[-_])*)?([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/i;
3926
- var BOOTSTRAP_RESOURCE_IDENTIFIER_PATTERN = /^bootstrap-[0-9a-f]{32}(?:\/[a-z0-9][a-z0-9_-]{0,127})?$/i;
3927
- var SECRET_LABEL_PATTERN = /(?:^|[-_])(?:api|auth|access|secret|token|key|password|credential|bearer|sk|pk|live)(?:[-_]|$)/i;
3928
- function shannonEntropy(value) {
3929
- const counts = /* @__PURE__ */ new Map();
3930
- for (const char of value) counts.set(char, (counts.get(char) ?? 0) + 1);
3931
- return [...counts.values()].reduce((entropy, count) => {
3932
- const p = count / value.length;
3933
- return entropy - p * Math.log2(p);
3934
- }, 0);
3935
- }
3936
- function isNonSecretUuidIdentifier(value) {
3937
- const match = UUID_IDENTIFIER_PATTERN.exec(value);
3938
- if (!match) return false;
3939
- const label = match[1] ?? "";
3940
- return !SECRET_LABEL_PATTERN.test(label);
3941
- }
3942
- function isNonSecretBootstrapResourceIdentifier(value) {
3943
- return BOOTSTRAP_RESOURCE_IDENTIFIER_PATTERN.test(value);
3944
- }
3945
3924
  function collectInlineSecretFindings(sourceCode) {
3946
3925
  const findings = [];
3947
3926
  for (const match of sourceCode.matchAll(SECRET_ENV_PATTERN)) {
@@ -3953,15 +3932,6 @@ function collectInlineSecretFindings(sourceCode) {
3953
3932
  if (ASSIGNMENT_SECRET_LITERAL_PATTERN.test(sourceCode)) {
3954
3933
  findings.push("secret-looking assignment literal");
3955
3934
  }
3956
- for (const match of sourceCode.matchAll(HIGH_ENTROPY_LITERAL_PATTERN)) {
3957
- const literal = match[1] ?? "";
3958
- if (isNonSecretUuidIdentifier(literal)) continue;
3959
- if (isNonSecretBootstrapResourceIdentifier(literal)) continue;
3960
- if (literal.length >= 40 && shannonEntropy(literal) >= 4.2) {
3961
- findings.push("high-entropy string literal");
3962
- break;
3963
- }
3964
- }
3965
3935
  return [...new Set(findings)];
3966
3936
  }
3967
3937
  function validatePlaySourceHasNoInlineSecrets(input) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deepline",
3
- "version": "0.2.18",
3
+ "version": "0.2.20",
4
4
  "description": "Deepline SDK + CLI — B2B data enrichment powered by durable cloud execution",
5
5
  "license": "MIT",
6
6
  "repository": {