@dudousxd/nestjs-catalog 0.18.0 → 0.20.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.
@@ -27,6 +27,39 @@ export type ConnectorKind = (typeof CONNECTOR_KINDS)[number];
27
27
  * entirely. Anything narrowing a stored value narrows against *this*.
28
28
  */
29
29
  export declare function isConnectorKind(value: unknown): value is ConnectorKind;
30
+ /**
31
+ * How the bytes behind a `file` or `s3` connector are read as records.
32
+ *
33
+ * A list rather than a loose string, for the reason {@link CONNECTOR_KINDS} is
34
+ * one: this used to be compared against string literals in the parser and
35
+ * spelled out again in a dropdown, and the two had no way to disagree loudly.
36
+ * The parser's chain also *ended* in JSON, so a format it did not recognise was
37
+ * not refused — it was read as JSON, and a spreadsheet handed to `JSON.parse`
38
+ * fails with a syntax error that names a byte offset rather than the format.
39
+ *
40
+ * `xlsx` is the odd one and is named for what it is: the only member whose
41
+ * payload is binary. The other three are text, and everything that reads them
42
+ * decodes the bytes first. Anything deciding something *per format* narrows
43
+ * against this and answers {@link unreachableSourceFormat}.
44
+ */
45
+ export declare const SOURCE_FORMATS: readonly ["csv", "ndjson", "json", "xlsx"];
46
+ export type SourceFormat = (typeof SOURCE_FORMATS)[number];
47
+ /** Same reason as {@link isConnectorKind}: one list, no second copy to drift. */
48
+ export declare function isSourceFormat(value: unknown): value is SourceFormat;
49
+ /**
50
+ * The format that never compiles quietly.
51
+ *
52
+ * The {@link unreachableNodeKind} of formats, and it exists for the same reason:
53
+ * a member added to {@link SOURCE_FORMATS} without a branch in the parser should
54
+ * be a type error naming the file, not a connector that offers a format in a
55
+ * dropdown and then reads the file as JSON.
56
+ *
57
+ * It throws as well as failing to compile, because a connector config is JSON
58
+ * that outlives the build that wrote it: a `format` stored by a newer deployment
59
+ * and read by an older one is possible, and falling back to a default for it
60
+ * would be exactly the silent path this closes.
61
+ */
62
+ export declare function unreachableSourceFormat(format: never, where: string): never;
30
63
  /**
31
64
  * What a published workflow runs as. **Not an authored object.**
32
65
  *
@@ -1042,6 +1075,13 @@ export interface WorkflowSinkNode extends WorkflowNodeBase, ReusableNodeRef {
1042
1075
  * answers are two documented shapes, and anything else fails the node naming
1043
1076
  * the workflow, the version and the child run id.
1044
1077
  *
1078
+ * All of the paragraph above describes the **envelope** mode, which is the
1079
+ * default and what every stored call node is. {@link callMode} names the other
1080
+ * one: a plain call sends {@link config} verbatim, so a workflow that has never
1081
+ * heard of this catalog can be called without being edited — and gives up the
1082
+ * ability to hand rows back, because it is told no key to stage them under. See
1083
+ * {@link WORKFLOW_CALL_MODES}.
1084
+ *
1045
1085
  * ## `config` is not a credential store
1046
1086
  *
1047
1087
  * Named `config` rather than `input` so it travels the same path a source
@@ -1067,9 +1107,109 @@ export interface WorkflowCallNode extends WorkflowNodeBase {
1067
1107
  * to *this* graph and is a number. This one identifies somebody else's code.
1068
1108
  */
1069
1109
  callVersion: string;
1070
- /** Parameters the author typed, handed to the child under `input`. */
1110
+ /**
1111
+ * Parameters the author typed. Where they land depends on
1112
+ * {@link WorkflowCallNode.callMode}: under `input` in an envelope call, and as
1113
+ * the whole of the child's payload in a plain one.
1114
+ */
1071
1115
  config: Record<string, unknown>;
1116
+ /**
1117
+ * Whether the child is handed a {@link WorkflowCallEnvelope} or the bare
1118
+ * {@link config}. See {@link WORKFLOW_CALL_MODES}, which is where the choice
1119
+ * is argued.
1120
+ *
1121
+ * Absent means `'envelope'`, which is what every call node stored before this
1122
+ * field existed is and what every one of them has always done. Read it through
1123
+ * {@link workflowCallMode} rather than defaulting it a second time — one
1124
+ * default, no second copy to drift.
1125
+ */
1126
+ callMode?: WorkflowCallMode;
1072
1127
  }
1128
+ /**
1129
+ * What a {@link WorkflowCallNode} puts on the wire, and the whole of it.
1130
+ *
1131
+ * ## Why there is a second mode at all
1132
+ *
1133
+ * The catalog could not call a workflow that does not know about the catalog.
1134
+ * A `call` node wraps the author's `config` in a {@link WorkflowCallEnvelope},
1135
+ * so a workflow that already exists — one registered years before this package,
1136
+ * whose body reads `data["proc"]` — receives `{catalog: {...}, input: {proc:
1137
+ * ...}}` and dies on the first key it looks for. The only repair available was
1138
+ * to edit the callee, which inverts the dependency exactly the wrong way round:
1139
+ * every workflow anybody wanted to call would have to start depending on this
1140
+ * package's contract, and a Python workflow registered in another repository
1141
+ * would have to be changed to be reachable from a graph.
1142
+ *
1143
+ * ## Why the nesting is not being loosened instead
1144
+ *
1145
+ * The envelope nests for one stated reason, which is on {@link
1146
+ * WorkflowCallEnvelope}: an author's parameter called `runId` must not be able
1147
+ * to shadow the run id. That reason is sound and it is not being weakened. It
1148
+ * simply does not reach the plain mode, because a plain call sends **no catalog
1149
+ * metadata at all** — there is no `runId`, no `nodeId`, no contract number on
1150
+ * the wire, so there is nothing a parameter could shadow. The flat payload is
1151
+ * not the envelope with its guard removed; it is a different, smaller promise.
1152
+ *
1153
+ * ## What the plain mode costs, which is not small
1154
+ *
1155
+ * No `runId` and no `nodeId` means the callee has no key to stage rows under.
1156
+ * Rows travel through the stage store addressed by `(runId, nodeId, batch)` —
1157
+ * see {@link WorkflowStageRef} — and a callee that was told neither cannot
1158
+ * write where the next node would read. So a plain call **cannot return rows to
1159
+ * the graph**, and that is not a convention anybody could follow more carefully:
1160
+ * it is arithmetic. `validateWorkflow` refuses a plain call with an outbound
1161
+ * edge for exactly this reason (`call-plain-has-output`), and its return value
1162
+ * is not read as a row count — see {@link readWorkflowCallOutput} for the shape
1163
+ * that is deliberately *not* consulted on this path.
1164
+ *
1165
+ * A plain call is therefore for its **effect**: run the thing, and let something
1166
+ * else in the graph produce what gets committed.
1167
+ *
1168
+ * ## Why a mode on the node rather than a second node kind or a boolean
1169
+ *
1170
+ * A boolean is the shape {@link WORKFLOW_PREDICATE_KINDS} argues against one
1171
+ * level down, and for the reason it gives: a `plain?: boolean` beside a future
1172
+ * third wire format is two optional flags whose combinations nobody defined, and
1173
+ * every reader invents its own rule for which wins. A closed list with an
1174
+ * exhaustiveness guard ({@link unreachableCallMode}) makes a third format a
1175
+ * compile error naming the files that have to answer for it.
1176
+ *
1177
+ * A second node *kind* was the other candidate and it is too big. The kind list
1178
+ * is deliberately small and every entry earns it by doing something no wiring
1179
+ * can express (see {@link WORKFLOW_NODE_KINDS}). A plain call does the same
1180
+ * thing a call does at the level the graph reasons about — it hands this
1181
+ * position to a workflow somebody else registered, pinned by name and version.
1182
+ * What differs is the payload. Splitting the kind would duplicate `callName`,
1183
+ * `callVersion`, `config`, the pin check, the picker, the plan entry and the
1184
+ * canvas node for a difference of one field, and every place that today writes
1185
+ * `node.kind === 'call'` would have to remember to write both — which is the
1186
+ * hand-maintained list going quiet that {@link NODE_KIND_IS_REUSABLE} exists to
1187
+ * stop.
1188
+ */
1189
+ export declare const WORKFLOW_CALL_MODES: readonly ["envelope", "plain"];
1190
+ export type WorkflowCallMode = (typeof WORKFLOW_CALL_MODES)[number];
1191
+ /** Same reason as {@link isConnectorKind}: one list, no second copy to drift. */
1192
+ export declare function isWorkflowCallMode(value: unknown): value is WorkflowCallMode;
1193
+ /**
1194
+ * {@link unreachableNodeKind}, one level down, and for the identical reason.
1195
+ *
1196
+ * Every branch over {@link WorkflowCallMode} ends here, so a third wire format
1197
+ * added to the list without a rule for building its payload, hashing it,
1198
+ * validating it or reading its answer is a type error naming the file. It throws
1199
+ * as well, because a mode arrives as JSON out of a column and a build older than
1200
+ * the data is a thing that happens.
1201
+ */
1202
+ export declare function unreachableCallMode(mode: never, where: string): never;
1203
+ /**
1204
+ * The mode this call node runs in, with the default applied once.
1205
+ *
1206
+ * Absent means `'envelope'` — that is what every node stored before the field
1207
+ * existed is, and reading it as anything else would silently change what a
1208
+ * deployment's graphs already do. One function so that the store, the runner,
1209
+ * the hash and the canvas cannot each carry their own `?? 'envelope'` and have
1210
+ * one of them drift.
1211
+ */
1212
+ export declare function workflowCallMode(node: WorkflowCallNode): WorkflowCallMode;
1073
1213
  /**
1074
1214
  * The kinds of test an {@link WorkflowIfNode} can make.
1075
1215
  *
@@ -1919,6 +2059,29 @@ export interface VersionPinCopy {
1919
2059
  detail: string;
1920
2060
  }
1921
2061
  export declare function describeVersionPin(version: number | undefined, subject: string): VersionPinCopy;
2062
+ /**
2063
+ * The same question about a whole graph: does it follow its latest save, or does
2064
+ * it run a version somebody chose?
2065
+ *
2066
+ * Shares {@link VersionPinCopy} and its two labels with
2067
+ * {@link describeVersionPin} deliberately. A console that said "pinned to v6"
2068
+ * about a transform node and invented different words for a graph would be two
2069
+ * vocabularies for one idea, and the reader would have to work out whether they
2070
+ * meant the same thing — which is the confusion the whole notion of a pin exists
2071
+ * to remove.
2072
+ *
2073
+ * The *detail* differs, and only where the facts do. Two of them:
2074
+ *
2075
+ * - A node's pin can outlive the revision it names, because `catalog_revision`
2076
+ * is capped. A graph's cannot: releases are never evicted, precisely because
2077
+ * the one a live pointer names is the graph production is running. So this
2078
+ * copy makes no eviction caveat, and must not acquire one.
2079
+ * - Following the latest is *cheap* for a node — everybody moves together, which
2080
+ * is often the point. For a graph it means editing is deploying, which is the
2081
+ * hazard this field was added to remove. So the unpinned sentence here is a
2082
+ * warning where the node's is a trade-off.
2083
+ */
2084
+ export declare function describeLiveVersion(workflow: CatalogWorkflow): VersionPinCopy;
1922
2085
  /**
1923
2086
  * Which side of an {@link WorkflowIfNode} a wire leaves by.
1924
2087
  *
@@ -2062,6 +2225,51 @@ export interface CatalogWorkflow {
2062
2225
  version: number;
2063
2226
  /** Fingerprint of the graph at this version. See {@link workflowGraphHash}. */
2064
2227
  graphHash: string;
2228
+ /**
2229
+ * Which released version a run of this graph gets when nobody names one.
2230
+ *
2231
+ * **Absent means follow the latest**, which is what every graph in every
2232
+ * deployment does today and what every existing graph keeps doing until
2233
+ * somebody points this at something. Absence is a real answer rather than a
2234
+ * state waiting to be filled in — the same stance {@link WorkflowCallNode}
2235
+ * takes from the other side, where a call that named no version would run
2236
+ * whichever one is registered when the load happens.
2237
+ *
2238
+ * Present, it names a {@link CatalogWorkflowRelease} — and from that moment
2239
+ * **editing this graph stops being deploying it**. A save bumps
2240
+ * {@link version} as it always did; the next scheduled window still runs the
2241
+ * released version this names, because a cron tick that executed whatever the
2242
+ * canvas happened to hold is a deploy nobody performed. Moving it is
2243
+ * {@link CatalogWorkflowReleaseStore.setLiveWorkflowVersion}, which is a
2244
+ * deliberate act by a named principal, and moving it *backwards* is the whole
2245
+ * of rollback.
2246
+ *
2247
+ * ## Why this is a column here and not a row in an environments table
2248
+ *
2249
+ * Because the row is already per-environment. Environments in this catalog are
2250
+ * physically isolated — one database each, see `catalog.environment.ts` — so
2251
+ * this `catalog_workflow` row exists once per environment already, and a
2252
+ * second dimension keyed on the environment id would be a table whose every
2253
+ * query filtered on a constant. The environment is the connection, and it has
2254
+ * been since environments were added.
2255
+ *
2256
+ * That is also why the field is not called `productionVersion`. "Production"
2257
+ * in this catalog is the *name of an environment* — see `CatalogEnvironment`
2258
+ * in `catalog.environment.ts` — so a column called that, on a row which
2259
+ * already lives inside exactly one environment, would read as naming a
2260
+ * different one.
2261
+ *
2262
+ * ## Why it does not cross a promotion
2263
+ *
2264
+ * `planPromotion` is explicit that version numbers do not cross: a version
2265
+ * counts edits made in the environment it lives in, so dev's v7 and
2266
+ * production's v7 are unrelated numbers. A pointer *to* a version inherits
2267
+ * that argument whole — carrying this field would point the target's live
2268
+ * pointer at whatever its own seventh edit happened to be. So
2269
+ * `PromotableWorkflow` does not carry it, and a promoted graph arrives
2270
+ * following the latest, exactly as a newly created one does.
2271
+ */
2272
+ liveVersion?: number;
2065
2273
  /**
2066
2274
  * The type the sink writes.
2067
2275
  *
@@ -2296,6 +2504,18 @@ export interface WorkflowCallOutput {
2296
2504
  * schema for a workflow's output anywhere in the durable contract, and no way
2297
2505
  * to reach one if there were. So the check is here, at the one moment the
2298
2506
  * answer exists, and it names what it saw.
2507
+ *
2508
+ * ## Not called at all for a plain call, which is a decision and not an omission
2509
+ *
2510
+ * A plain call is handed no run id and no node id, so its callee could not have
2511
+ * staged anything under `(runId, nodeId, batch)` even if it wanted to. Running
2512
+ * this over its answer would be worse than pointless in both directions: a
2513
+ * callee that happens to return `{batches, rowCount}` meaning something else
2514
+ * entirely would have this graph go and read a stage that does not exist, and a
2515
+ * callee returning one of the two would fail the node over a key it was never
2516
+ * told about. So the plain path reports zero rows unconditionally and reads
2517
+ * nothing — see `WorkflowRunnerService.callOutput`, which is where that is
2518
+ * enforced rather than merely intended.
2299
2519
  */
2300
2520
  export declare function readWorkflowCallOutput(value: unknown): WorkflowCallOutput | undefined;
2301
2521
  /**
@@ -2421,7 +2641,7 @@ export interface CallableWorkflowBlock {
2421
2641
  }
2422
2642
  export declare function callableWorkflowBlock(ref: CallableWorkflowRef): CallableWorkflowBlock | undefined;
2423
2643
  /** Every way a graph can be refused. Exported so a canvas can key off the code. */
2424
- export declare const WORKFLOW_ISSUE_CODES: readonly ["empty", "invalid-node-id", "duplicate-node-id", "edge-endpoint-missing", "self-edge", "duplicate-edge", "cycle", "no-source", "source-has-input", "no-sink", "duplicate-sink-type", "sink-has-output", "unreachable", "dead-end", "transform-not-named", "call-not-named", "if-not-named", "if-threshold-invalid", "if-needs-one-input", "branch-not-labelled", "branch-on-plain-edge", "filter-predicate-invalid", "filter-narrows-unacknowledged", "filter-narrows-nothing", "version-pin-invalid"];
2644
+ export declare const WORKFLOW_ISSUE_CODES: readonly ["empty", "invalid-node-id", "duplicate-node-id", "edge-endpoint-missing", "self-edge", "duplicate-edge", "cycle", "no-source", "source-has-input", "no-sink", "duplicate-sink-type", "sink-has-output", "unreachable", "dead-end", "transform-not-named", "call-not-named", "call-plain-has-output", "if-not-named", "if-threshold-invalid", "if-needs-one-input", "branch-not-labelled", "branch-on-plain-edge", "filter-predicate-invalid", "filter-narrows-unacknowledged", "filter-narrows-nothing", "version-pin-invalid"];
2425
2645
  export type WorkflowIssueCode = (typeof WORKFLOW_ISSUE_CODES)[number];
2426
2646
  export interface WorkflowValidationIssue {
2427
2647
  code: WorkflowIssueCode;
@@ -2718,6 +2938,172 @@ export interface CatalogWorkflowStore {
2718
2938
  enabled?: boolean;
2719
2939
  }, changedBy: string): Promise<CatalogWorkflow>;
2720
2940
  }
2941
+ /**
2942
+ * One version of a graph, kept exactly as it was, because somebody said so.
2943
+ *
2944
+ * ## The archive `CatalogWorkflow` argues against, and why this is not it
2945
+ *
2946
+ * {@link CatalogWorkflow} states plainly that a graph is not revisioned, and the
2947
+ * decisive reason it gives is the counter: {@link CatalogWorkflow.version} is
2948
+ * bumped on **draft** edits by design, so archiving one body per version would
2949
+ * store every autosave of a canvas somebody is still dragging boxes around on —
2950
+ * and under a bounded archive that noise would evict the versions that actually
2951
+ * ran. **That argument is correct and nothing here weakens it.** It is an
2952
+ * argument against archiving *saves*, and this archives *releases*: a release is
2953
+ * minted only by `releaseWorkflow`, which is a route a person presses, and a
2954
+ * canvas autosave does not reach it. So the counter stays cheap to inflate, the
2955
+ * archive stays keyed on a deliberate act, and the two now compose because they
2956
+ * are counting different things.
2957
+ *
2958
+ * The second half of that docblock — that a graph is a structure and a line
2959
+ * differ over serialised JSON would report a dragged box as a change — is also
2960
+ * untouched. This is not a diff feature. It stores the graph so a version can be
2961
+ * *run*, and diffing graphs remains a graph problem deserving a screen that
2962
+ * draws one.
2963
+ *
2964
+ * ## Why not `catalog_revision`, which already archives versioned bodies
2965
+ *
2966
+ * Two reasons, and the second is the one that decides it.
2967
+ *
2968
+ * `CatalogRevision.body` is text a person typed — a transform's code, a saved
2969
+ * query's SQL — and every route over it is built to render text. A graph is
2970
+ * nodes and edges, and folding it into that column would put JSON nobody wrote
2971
+ * in front of a differ built for source.
2972
+ *
2973
+ * The one that decides it is {@link CATALOG_REVISION_LIMIT}. That cap is right
2974
+ * for code, for the reason its own docblock gives: revisions grow with how often
2975
+ * somebody edits, which nobody meters. A release does not grow that way — it
2976
+ * grows with how often somebody deliberately ships — and, crucially, a release
2977
+ * is the thing a live pointer names. An eviction rule over this table could
2978
+ * delete the graph that production is *currently running*, turning
2979
+ * {@link CatalogWorkflow.liveVersion} into a pin nothing can honour and stopping
2980
+ * a working pipeline on a retention policy. **So releases are never evicted.**
2981
+ * That makes this an unbounded table, which this codebase is careful about — and
2982
+ * it earns it on the same test `catalog_audit_event` and `catalog_connector_run`
2983
+ * pass: one row per thing a person deliberately did, at a rate an operator can
2984
+ * read off their own change process.
2985
+ *
2986
+ * ## Immutable, and there is no route that removes one
2987
+ *
2988
+ * Nothing edits a release and nothing deletes one. That is the strongest form of
2989
+ * "refuse to delete the version that is live": there is no operation to refuse.
2990
+ * The one exception is {@link CatalogWorkflowStore.deleteWorkflow}, which takes
2991
+ * the graph, its connector and its whole run history — releases go with it,
2992
+ * because nothing survives that could still name one. (The opposite call is made
2993
+ * for a transform, whose revisions outlive it precisely *because* runs that ran
2994
+ * them survive.)
2995
+ */
2996
+ export interface CatalogWorkflowRelease {
2997
+ /**
2998
+ * `{workflowId}:{version}`, derived rather than random.
2999
+ *
3000
+ * The same construction `revisionKey` uses in the MikroORM store and for the
3001
+ * same property: releasing the same version twice cannot append a second copy,
3002
+ * because the second write would collide with the first rather than land
3003
+ * beside it.
3004
+ */
3005
+ id: string;
3006
+ workflowId: string;
3007
+ /**
3008
+ * The {@link CatalogWorkflow.version} this release IS.
3009
+ *
3010
+ * The same number, not a parallel sequence. A release sequence of its own was
3011
+ * the alternative and it is worse in the one place it matters: a run records
3012
+ * `workflowVersion`, so a second numbering would mean a run naming "v3" and an
3013
+ * operator reading "release 3" could be two different graphs, which is exactly
3014
+ * the ambiguity the edit counter was made cheap to avoid.
3015
+ */
3016
+ version: number;
3017
+ /** Fingerprint of the graph as released. See {@link workflowGraphHash}. */
3018
+ graphHash: string;
3019
+ nodes: WorkflowNode[];
3020
+ edges: WorkflowEdge[];
3021
+ /** The type the sink committed at this version. */
3022
+ targetType: string;
3023
+ /** Whatever the releaser wanted to say about it. */
3024
+ notes?: string;
3025
+ releasedBy: string;
3026
+ releasedAt: string;
3027
+ }
3028
+ /**
3029
+ * Minting releases, and pointing at one.
3030
+ *
3031
+ * Its own interface mixed in optionally, for the reason {@link
3032
+ * CatalogWorkflowStore} is: a store written against the previous shape must keep
3033
+ * compiling, and "this deployment cannot hold releases" has to be a sentence a
3034
+ * UI can say rather than a method missing at run time.
3035
+ *
3036
+ * The split from `CatalogWorkflowStore` is not only compatibility. These four
3037
+ * are the only members in this file that can change *what a cron executes*
3038
+ * without touching a graph, and keeping them behind their own predicate means a
3039
+ * store can hold graphs without acquiring that power by accident.
3040
+ */
3041
+ export interface CatalogWorkflowReleaseStore {
3042
+ /**
3043
+ * Freeze the graph as it currently stands, under its current version.
3044
+ *
3045
+ * **The only thing that mints one.** Not `saveWorkflow`, which is the autosave
3046
+ * this whole feature exists to stop being a deploy; not `publishWorkflow`,
3047
+ * which is idempotent and is called by a promotion apply — a release minted as
3048
+ * a side effect of promoting configuration into an environment would be a
3049
+ * release nobody in that environment chose.
3050
+ *
3051
+ * Refuses a draft, for the reason `WORKFLOW_STATUSES` already gives about what
3052
+ * a connector may point at and what a promotion may carry: what gets shipped
3053
+ * should be something a person declared finished.
3054
+ *
3055
+ * **Idempotent per version.** Releasing a graph whose current version is
3056
+ * already released answers with the existing release rather than minting a
3057
+ * second or overwriting its notes — the graph has not changed, so a second
3058
+ * release would be a record of an event that did not happen, and re-attributing
3059
+ * the first one would erase who actually shipped it.
3060
+ */
3061
+ releaseWorkflow(id: string, releasedBy: string, options?: {
3062
+ notes?: string;
3063
+ }): Promise<CatalogWorkflowRelease>;
3064
+ /** Newest first. */
3065
+ listWorkflowReleases(id: string): Promise<CatalogWorkflowRelease[]>;
3066
+ /**
3067
+ * The graph as it was at a released version, or `undefined`.
3068
+ *
3069
+ * `undefined` for a version that was never released as much as for a workflow
3070
+ * that does not exist, and callers must treat the two the same way: **fail,
3071
+ * never fall back to the latest.** Running the current graph because the named
3072
+ * one could not be produced is precisely the substitution a pin is written
3073
+ * down to prevent.
3074
+ *
3075
+ * Answers with a whole {@link CatalogWorkflow} rather than the bare release,
3076
+ * because that is what a run needs: the released graph, carried on the row's
3077
+ * present identity. The graph fields — nodes, edges, `targetType`,
3078
+ * `graphHash`, `version` — come from the release. The operational ones —
3079
+ * `name`, `status`, `schedule`, `enabled`, `liveVersion` — come from the row
3080
+ * as it is now, because they are not part of what was released. A cron somebody
3081
+ * changed this morning applies to the version that is live, not to whatever
3082
+ * cron was on the row the day it was released.
3083
+ */
3084
+ getWorkflowAt(id: string, version: number): Promise<CatalogWorkflow | undefined>;
3085
+ /**
3086
+ * Point {@link CatalogWorkflow.liveVersion} at a released version — or clear it.
3087
+ *
3088
+ * One method for going live, for rolling back and for going back to following
3089
+ * the latest, because they are one act with a different argument. Rollback in
3090
+ * particular is not a separate mechanism to build and test: it is this call
3091
+ * with a smaller number, and it works because the older graph is still stored.
3092
+ *
3093
+ * Refuses a version with no release behind it, naming what there is. A pointer
3094
+ * accepted at a number nothing can produce would be a pipeline that stops at
3095
+ * its next window, discovered by a load failing rather than by the person who
3096
+ * typed it.
3097
+ *
3098
+ * `undefined` clears the pointer and takes the graph back to following the
3099
+ * latest. Allowed rather than refused — it is the state every graph in every
3100
+ * deployment is in, so refusing would strand anything that ever went live —
3101
+ * and stated as a decision because it hands back exactly the hazard the
3102
+ * pointer removes: from that moment, saving the graph changes what the next
3103
+ * window runs.
3104
+ */
3105
+ setLiveWorkflowVersion(id: string, version: number | undefined, changedBy: string): Promise<CatalogWorkflow>;
3106
+ }
2721
3107
  /**
2722
3108
  * Where the rows between two nodes actually sit.
2723
3109
  *
@@ -2767,6 +3153,36 @@ export interface CatalogStageStore {
2767
3153
  * does, because a flag is a claim and a method is the thing itself.
2768
3154
  */
2769
3155
  export declare function supportsWorkflows(store: CatalogPipelineStore): store is CatalogPipelineStore & CatalogWorkflowStore;
3156
+ /**
3157
+ * Whether this store can mint a release and be pointed at one.
3158
+ *
3159
+ * All four asked for by name, for the reason `supportsWorkflows` asks for
3160
+ * `publishWorkflow` and `saveWorkflowSchedule` by name rather than assuming they
3161
+ * arrive together: a store with the mint and not the pointer would narrow
3162
+ * cleanly here, let somebody release a graph, and then fail on the call that was
3163
+ * supposed to make it run.
3164
+ *
3165
+ * {@link getWorkflowAt} is the one whose absence is least visible and most
3166
+ * expensive. A store that could hold a `liveVersion` and not resolve it would
3167
+ * point a scheduled load at a version it cannot produce — and the only place
3168
+ * that shows up is a cron window that stops firing.
3169
+ */
3170
+ export declare function supportsWorkflowReleases(store: CatalogPipelineStore): store is CatalogPipelineStore & CatalogWorkflowReleaseStore;
3171
+ /**
3172
+ * Which version of this graph a run gets when the caller names none.
3173
+ *
3174
+ * The one implementation of "follow the latest unless something is live", shared
3175
+ * by the scheduler and by the manual run route so the two cannot disagree about
3176
+ * what a cron does and what the button next to it does. That divergence is not
3177
+ * hypothetical: the schedule used to live on the connector and on the workflow
3178
+ * at once, and the whole of `ConnectorScheduler`'s docblock is about what it
3179
+ * cost to have two copies of one answer.
3180
+ *
3181
+ * Not a fallback in the defensive sense. `liveVersion` absent is a stated
3182
+ * position — this graph follows its head — and this function is where that
3183
+ * position is turned into a number, not where a missing value is patched over.
3184
+ */
3185
+ export declare function liveWorkflowVersion(workflow: CatalogWorkflow): number;
2770
3186
  /**
2771
3187
  * Whether this store keeps a transform's history.
2772
3188
  *
@@ -2834,7 +3250,7 @@ export type CatalogLoadExpectationStore = Required<Pick<CatalogPipelineStore, 'l
2834
3250
  * an editor whose save has nowhere to go.
2835
3251
  */
2836
3252
  export declare function supportsLoadExpectations(store: CatalogPipelineStore): store is CatalogPipelineStore & CatalogLoadExpectationStore;
2837
- export interface CatalogPipelineStore extends Partial<CatalogWorkflowStore>, Partial<CatalogStageStore> {
3253
+ export interface CatalogPipelineStore extends Partial<CatalogWorkflowStore>, Partial<CatalogWorkflowReleaseStore>, Partial<CatalogStageStore> {
2838
3254
  listConnectors(): Promise<CatalogConnector[]>;
2839
3255
  getConnector(id: string): Promise<CatalogConnector | undefined>;
2840
3256
  saveConnector(input: Omit<CatalogConnector, 'id' | 'createdAt' | 'updatedAt' | 'createdBy'> & {