@dudousxd/nestjs-catalog 0.22.0 → 0.24.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.
@@ -425,6 +425,65 @@ export declare const TRANSFORM_LANGUAGES: readonly ["javascript", "typescript",
425
425
  export type TransformLanguage = (typeof TRANSFORM_LANGUAGES)[number];
426
426
  /** Same reason as {@link isConnectorKind}: one list, no second copy to drift. */
427
427
  export declare function isTransformLanguage(value: unknown): value is TransformLanguage;
428
+ /**
429
+ * Whether a transform is a function over the whole batch or over one record.
430
+ *
431
+ * ## Why this is declared and never inferred
432
+ *
433
+ * The two are not interchangeable and the difference is invisible in the code.
434
+ * `records.map(...)` and a body that returns one object read almost identically,
435
+ * and a detector that guessed from destructuring — `{ records }` versus
436
+ * `{ record }` — would be reading a *parameter name*, which is the author's to
437
+ * choose and which minification, a rename, or a rest parameter changes without
438
+ * changing what the function computes. Guess wrong towards `record` and an
439
+ * aggregation is called 102,520 times and returns 102,520 partial answers, none
440
+ * of which fails; guess wrong towards `batch` and a per-record function is handed
441
+ * an array and reads `undefined` off every property. Both commit. Neither errors.
442
+ * So the mode is a field somebody set, and the cost of setting it is one control
443
+ * in the editor.
444
+ *
445
+ * ## Why a closed list rather than `streaming?: boolean`
446
+ *
447
+ * The identical argument {@link WORKFLOW_CALL_MODES} makes one level up. A flag
448
+ * beside a future third calling convention — a windowed transform, a keyed one —
449
+ * is two optional booleans whose combinations nobody defined, and each reader
450
+ * invents its own rule for which wins. A closed list with an exhaustiveness guard
451
+ * ({@link unreachableTransformMode}) makes a third convention a compile error
452
+ * naming the files that have to answer for it: the harness that generates the
453
+ * call, the runner that chooses a transport, and the two runners that consume
454
+ * the result.
455
+ *
456
+ * ## What the default has to be, and why it is not a choice
457
+ *
458
+ * Absent means {@link CatalogTransform.mode} was never set, which is every
459
+ * transform stored before this field existed, and every one of them is a function
460
+ * over the whole batch — the harness handed it `records` and there was no other
461
+ * shape to write. Reading absence as anything else would silently change what a
462
+ * deployment's existing loads compute. Read it through {@link transformMode}
463
+ * rather than defaulting it a second time.
464
+ */
465
+ export declare const TRANSFORM_MODES: readonly ["batch", "record"];
466
+ export type TransformMode = (typeof TRANSFORM_MODES)[number];
467
+ /** Same reason as {@link isConnectorKind}: one list, no second copy to drift. */
468
+ export declare function isTransformMode(value: unknown): value is TransformMode;
469
+ /**
470
+ * {@link unreachableCallMode}, for transforms, and for the identical reason.
471
+ *
472
+ * Every branch over {@link TransformMode} ends here, so a third calling
473
+ * convention added to the list without a harness to generate it, a transport to
474
+ * carry it and a consumer to read its output is a type error naming the file. It
475
+ * throws as well, because a mode arrives as JSON out of a column and a build
476
+ * older than the data is a thing that happens.
477
+ */
478
+ export declare function unreachableTransformMode(mode: never, where: string): never;
479
+ /**
480
+ * The mode this transform runs in, with the default applied once.
481
+ *
482
+ * Absent means `'batch'` — see {@link TRANSFORM_MODES}. One function so that the
483
+ * store, the runner, the two consumers, the editor and the try pane cannot each
484
+ * carry their own `?? 'batch'` and have one of them drift.
485
+ */
486
+ export declare function transformMode(transform: Pick<CatalogTransform, 'mode'>): TransformMode;
428
487
  /**
429
488
  * User code that maps a source record to a row.
430
489
  *
@@ -481,11 +540,40 @@ export interface CatalogTransform {
481
540
  * a new field costs one generated line rather than an edit to stored code.
482
541
  */
483
542
  code: string;
543
+ /**
544
+ * Whether {@link code} is called once with the batch or once per record.
545
+ *
546
+ * Absent means `'batch'`, which is what every transform stored before this
547
+ * field existed is. See {@link TRANSFORM_MODES} for why it is declared rather
548
+ * than inferred, and read it through {@link transformMode}.
549
+ *
550
+ * A `'record'` transform is constrained in two ways the mode alone does not
551
+ * say, and both are refused rather than discovered at run time — see
552
+ * {@link recordModeRefusal}: it must be a **module**, because a bare body has
553
+ * `records` in scope by the harness's own construction, and it cannot be
554
+ * **Python**, because that harness writes the `def` and has no second one yet.
555
+ */
556
+ mode?: TransformMode;
484
557
  version: number;
485
558
  createdBy: string;
486
559
  createdAt: string;
487
560
  updatedAt: string;
488
561
  }
562
+ /**
563
+ * Why this transform cannot run in the mode it declares, if it cannot.
564
+ *
565
+ * Two combinations are representable and neither can work, so both are refused
566
+ * at the point somebody presses save rather than at three in the morning when a
567
+ * schedule fires. `undefined` means there is nothing wrong.
568
+ *
569
+ * Asked in both places on purpose. The controller asks so the author is told
570
+ * while they are still looking at the code; the runner asks because a row can
571
+ * reach it that no controller in this build ever validated — promoted from
572
+ * another environment, restored from a backup, written by an older version — and
573
+ * the failure a runner must never have is the silent one where a per-record
574
+ * module is handed an array and quietly reads `undefined` off every property.
575
+ */
576
+ export declare function recordModeRefusal(transform: Pick<CatalogTransform, 'language' | 'code' | 'mode'>): string | undefined;
489
577
  /**
490
578
  * The single argument a module-shaped transform is called with.
491
579
  *
@@ -557,6 +645,77 @@ export interface CatalogTransformInput<TRecord = Record<string, unknown>> {
557
645
  * ```
558
646
  */
559
647
  export type CatalogTransformFunction<TRecord = Record<string, unknown>> = (input: CatalogTransformInput<TRecord>) => Array<Record<string, unknown>> | Promise<Array<Record<string, unknown>>>;
648
+ /**
649
+ * The single argument a `'record'`-mode transform is called with, once per
650
+ * record.
651
+ *
652
+ * One object, for the reason {@link CatalogTransformInput} gives and not a
653
+ * second time: a field can be added later without redefining what any signature
654
+ * already written means.
655
+ *
656
+ * ## `record` rather than `records`, deliberately one letter apart
657
+ *
658
+ * Which is a real risk and was weighed against the alternatives. A name like
659
+ * `row` or `item` would be further from its sibling and would be *wrong*: what
660
+ * arrives is a record exactly as the source produced it, and a row is what the
661
+ * transform returns — the two words already mean different things everywhere
662
+ * else in this package, and borrowing one of them here to reduce a typo would
663
+ * make the vocabulary lie.
664
+ *
665
+ * The typo it invites is also the one mistake in this area that cannot go quiet.
666
+ * `{ records }` in a per-record transform destructures `undefined`, and the first
667
+ * thing anybody does with it — `.map`, `.length`, `.filter` — throws on the very
668
+ * first record, with a stack frame in the author's own file. The dangerous
669
+ * direction is the other one, and that is exactly what {@link TRANSFORM_MODES}
670
+ * refuses to guess about.
671
+ *
672
+ * ## What is not on it
673
+ *
674
+ * No index, no total, no `isFirst`. Each of those is a way to write a transform
675
+ * whose answer depends on where a record fell in the stream, which is the
676
+ * property this mode exists to *not* have — a record's row must be a function of
677
+ * that record. `context.rowCount` is a count of what reached the node and is
678
+ * already there for anything that legitimately needs the size of the load.
679
+ */
680
+ export interface CatalogRecordTransformInput<TRecord = Record<string, unknown>> {
681
+ /** One record, exactly as the source produced it. */
682
+ record: TRecord;
683
+ /** The run, the node, the counts, and the admitted environment variables. */
684
+ context: CatalogCodeContext;
685
+ }
686
+ /**
687
+ * The function a `'record'`-mode transform exports.
688
+ *
689
+ * **For the editor and nothing else**, exactly as {@link CatalogTransformFunction}
690
+ * is: TypeScript transforms run through Node's own type *stripping*, so the
691
+ * annotations are erased on the way in and a wrong one is a squiggle rather than
692
+ * a failed run.
693
+ *
694
+ * The return type is the whole contract of the mode and it is deliberately four
695
+ * things at once:
696
+ *
697
+ * - **an object** — one row, the ordinary case;
698
+ * - **an array** — several rows, so one record can fan out;
699
+ * - **an empty array** — no rows, so a record can be dropped;
700
+ * - **`null` or `undefined`** — no rows either, because that is what a function
701
+ * with a bare `return` or a missed branch produces and reading it as anything
702
+ * else would invent a row nobody wrote.
703
+ *
704
+ * Map, filter and flatMap under one rule, and no ambiguity between the first two
705
+ * cases: an array is never a row, because a row is a plain object everywhere in
706
+ * this package and the runners have always dropped anything else.
707
+ *
708
+ * ```ts
709
+ * import type { CatalogRecordTransformFunction } from '@dudousxd/nestjs-catalog/client';
710
+ *
711
+ * const transform: CatalogRecordTransformFunction<{ 'Mgmt Cd': string }> = ({ record }) => ({
712
+ * mgmtCd: record['Mgmt Cd'],
713
+ * });
714
+ *
715
+ * export default transform;
716
+ * ```
717
+ */
718
+ export type CatalogRecordTransformFunction<TRecord = Record<string, unknown>> = (input: CatalogRecordTransformInput<TRecord>) => Record<string, unknown> | Array<Record<string, unknown>> | null | undefined | Promise<Record<string, unknown> | Array<Record<string, unknown>> | null | undefined>;
560
719
  export interface TransformResult {
561
720
  rows: Array<Record<string, unknown>>;
562
721
  /**
@@ -595,11 +754,87 @@ export interface TransformResult {
595
754
  * people who are not already trusted with the database needs a container or a
596
755
  * sandboxed runtime, and this interface is where that gets plugged in.
597
756
  */
757
+ /**
758
+ * What a per-record run produced, asked **only after {@link TransformStream.rows}
759
+ * is exhausted**.
760
+ *
761
+ * The stream equivalent of {@link TransformResult}, and it is a separate shape
762
+ * rather than the same one because the fields genuinely differ in *when they are
763
+ * knowable*. `rows` is not a value here — the whole point is that nothing holds
764
+ * them — so what is left is the counts and the log, and neither is final until
765
+ * the last record has gone past. `recordsIn` is new and is the one number a
766
+ * batch call never needed: a caller that streamed its source has no `.length` to
767
+ * report as `fetched`.
768
+ */
769
+ export interface TransformStreamSummary {
770
+ /** How many records the runner fed the code. */
771
+ recordsIn: number;
772
+ /** How many rows came back, over every record. */
773
+ rowsOut: number;
774
+ /** {@link TransformResult.logs}, bounded by the runner in exactly the same way. */
775
+ logs: string[];
776
+ elapsedMs: number;
777
+ }
778
+ /**
779
+ * A per-record run in progress: the rows as they arrive, and the counts once
780
+ * they have.
781
+ *
782
+ * The same two-part shape `StreamedFetchResult` uses in the pipeline package —
783
+ * an iterable plus a function asked afterwards — and copied from it on purpose
784
+ * rather than invented. The reason it gives is the reason here: a stream is not
785
+ * complete until it has been drained, so anything computed *over* it is not yet
786
+ * known when the call returns, and a field would hand a caller a number that
787
+ * stops short of the rows they have already written.
788
+ *
789
+ * {@link summary} before {@link rows} is exhausted is a programming error and
790
+ * the bundled runner throws rather than answering with a running total, because
791
+ * a running total is exactly what somebody would then record as `fetched`.
792
+ */
793
+ export interface TransformStream {
794
+ /**
795
+ * The rows, in record order, in the order the code emitted them.
796
+ *
797
+ * Pulled, not pushed: the next record is not fed to the code until the row
798
+ * before it has been taken, so a consumer that writes to a database
799
+ * back-pressures all the way to the source. That is the property the mode
800
+ * exists for and it is the caller's to keep — a consumer that collects this
801
+ * into an array has re-created the whole-batch memory profile with extra
802
+ * steps.
803
+ *
804
+ * Throws where the code threw, naming the record. See the runner.
805
+ */
806
+ rows: AsyncIterable<Record<string, unknown>>;
807
+ /** The counts and the log. Call only after {@link rows} is exhausted. */
808
+ summary(): TransformStreamSummary;
809
+ }
598
810
  export interface TransformRunner {
599
811
  run(transform: Pick<CatalogTransform, 'language' | 'code'>, records: unknown[], options?: {
600
812
  timeoutMs?: number;
601
813
  context?: CatalogCodeContext;
602
814
  }): Promise<TransformResult>;
815
+ /**
816
+ * Run a `'record'`-mode transform over a stream of records, streaming the rows
817
+ * back.
818
+ *
819
+ * **Optional**, mixed in for the reason every optional member of
820
+ * {@link CatalogPipelineStore} is: a runner written against the previous shape
821
+ * of this interface still satisfies it, and a purely additive capability must
822
+ * not turn that into a compile error. {@link supportsTransformStreaming} is how
823
+ * a caller asks; a deployment whose runner cannot stream runs a per-record
824
+ * transform through {@link run} against a buffered batch instead, which is
825
+ * slower and holds more but computes the identical rows.
826
+ *
827
+ * Not a widening of {@link run}. The two differ in what the caller must hand
828
+ * over (an array against an iterable), in what comes back (rows against a
829
+ * stream of them), in when the counts are knowable, and in what the timeout
830
+ * measures — see the bundled runner, where a stream is bounded by a stall
831
+ * rather than by total wall clock. One method doing both would have four
832
+ * optional fields and a reader could not tell which combination was legal.
833
+ */
834
+ runStream?(transform: Pick<CatalogTransform, 'language' | 'code' | 'mode'>, records: AsyncIterable<unknown>, options?: {
835
+ timeoutMs?: number;
836
+ context?: CatalogCodeContext;
837
+ }): Promise<TransformStream>;
603
838
  /** Languages this runner can actually execute in this environment. */
604
839
  available(): Promise<TransformLanguage[]>;
605
840
  /**
@@ -611,6 +846,16 @@ export interface TransformRunner {
611
846
  */
612
847
  pythonPackages?(): Promise<string[]>;
613
848
  }
849
+ /**
850
+ * Whether this runner can stream a per-record transform.
851
+ *
852
+ * The method rather than a flag, exactly as {@link supportsTransformRevisions}
853
+ * argues one interface along: a flag is a claim and a method is the thing
854
+ * itself. A runner that answers `false` still runs `'record'` transforms — the
855
+ * consumers buffer and call {@link TransformRunner.run} — so this is a question
856
+ * about *how much is held*, never about whether the load works.
857
+ */
858
+ export declare function supportsTransformStreaming(runner: TransformRunner): runner is TransformRunner & Required<Pick<TransformRunner, 'runStream'>>;
614
859
  export declare const TRANSFORM_RUNNER: unique symbol;
615
860
  /**
616
861
  * The number in {@link CatalogCodeContext.contract}.
@@ -918,7 +1163,7 @@ export interface WorkflowNodeOutcome {
918
1163
  * outside a run, which is why `call` names one and not a step. If a step is
919
1164
  * what you want, the thing to call is a one-step workflow wrapping it.
920
1165
  */
921
- export declare const WORKFLOW_NODE_KINDS: readonly ["source", "transform", "sink", "call", "if", "filter"];
1166
+ export declare const WORKFLOW_NODE_KINDS: readonly ["source", "transform", "sink", "call", "if", "filter", "rename"];
922
1167
  export type WorkflowNodeKind = (typeof WORKFLOW_NODE_KINDS)[number];
923
1168
  /** Same reason as {@link isConnectorKind}: one list, no second copy to drift. */
924
1169
  export declare function isWorkflowNodeKind(value: unknown): value is WorkflowNodeKind;
@@ -1876,13 +2121,196 @@ export interface WorkflowFilterNode extends WorkflowNodeBase {
1876
2121
  */
1877
2122
  narrows?: string[];
1878
2123
  }
2124
+ /**
2125
+ * What happens to a column the rename does not name.
2126
+ *
2127
+ * Two words rather than a boolean, because the two are genuinely different
2128
+ * nodes and a boolean called `drop` would read as a modifier on one node. See
2129
+ * {@link WorkflowRenameNode.unnamed} for what each costs.
2130
+ */
2131
+ export declare const WORKFLOW_RENAME_UNNAMED: readonly ["keep", "drop"];
2132
+ export type WorkflowRenameUnnamed = (typeof WORKFLOW_RENAME_UNNAMED)[number];
2133
+ /** Same reason as {@link isConnectorKind}: one list, no second copy to drift. */
2134
+ export declare function isWorkflowRenameUnnamed(value: unknown): value is WorkflowRenameUnnamed;
2135
+ /**
2136
+ * The exhaustiveness guard for {@link WORKFLOW_RENAME_UNNAMED}.
2137
+ *
2138
+ * {@link unreachableNodeKind}, one level down, and for the identical reason: the
2139
+ * two words decide whether a batch is rewritten or only re-labelled, and a third
2140
+ * one added without a branch would silently pick whichever the last `if` was.
2141
+ */
2142
+ export declare function unreachableRenameUnnamed(value: never, where: string): never;
2143
+ /**
2144
+ * How many columns one rename may name.
2145
+ *
2146
+ * The same argument {@link WORKFLOW_FILTER_MAX_VALUES} makes: the map travels in
2147
+ * the graph and into the graph fingerprint, and past a few hundred entries the
2148
+ * thing being expressed is a schema mapping that belongs in a stored object
2149
+ * rather than in a node. It is also the bound that keeps
2150
+ * {@link isWorkflowRenameColumns} — which is run on JSON out of a column — from
2151
+ * being a place to hand a service a million-key object.
2152
+ */
2153
+ export declare const WORKFLOW_RENAME_MAX_COLUMNS = 500;
2154
+ /**
2155
+ * Renames columns, and does nothing else, ever.
2156
+ *
2157
+ * ## Why this is a node kind and not a flag, and why it stays small
2158
+ *
2159
+ * The generic {@link WorkflowTransformNode} continues to exist for everything
2160
+ * else, and **that is what lets this node stay deliberately narrow**. The usual
2161
+ * objection to a declarative shortcut is that it grows — rename, then cast, then
2162
+ * default, then trim, and then it is a small language nobody designed and
2163
+ * everybody has to learn. With a real code node sitting beside it, the answer to
2164
+ * "I need more than renaming" is always *use a transform*, and never *add a
2165
+ * field here*. That is the constraint that keeps this honest, and it is the
2166
+ * reason to refuse the next field rather than a reason to feel bad about
2167
+ * refusing it.
2168
+ *
2169
+ * ## What it buys, measured rather than asserted
2170
+ *
2171
+ * **1. It streams by construction.** A rename is per record. It cannot
2172
+ * aggregate, deduplicate, sort or look at a neighbour, so there is no batch it
2173
+ * has to hold. A transform node cannot make that promise — an author's function
2174
+ * is handed the whole batch and may legitimately reduce over it — which is why
2175
+ * `ConnectorRunnerService` has to log *"Held all N records in memory"* when a
2176
+ * transform is present. This node never contributes that line.
2177
+ *
2178
+ * **2. It needs no child process.** A transform round trip for 103,087 rows
2179
+ * costs ~338 ms, of which the author's `.map` is ~5 ms. The transport — encode,
2180
+ * write, decode, run, encode, read — is the entire bill. A rename does not need
2181
+ * transport, and the same rename in process is ~14 ms.
2182
+ *
2183
+ * **3. On staged data it is metadata-only, and exactly when.** A staged batch is
2184
+ * a shape dictionary (see `catalog.stage-encoding.ts`): `shapes` holds each
2185
+ * distinct key-set once, `shapeOf[i]` indexes it, and `values[i]` is a
2186
+ * positional array parallel to that shape. A positional array does not care what
2187
+ * the key is called, so renaming a key is a rewrite of `shapes` and nothing
2188
+ * else — tens of strings for a hundred thousand rows.
2189
+ *
2190
+ * **That last claim holds for `unnamed: 'keep'` and does not hold for
2191
+ * `unnamed: 'drop'`.** Dropping a column removes a position, so every `values`
2192
+ * row has to be rebuilt and the cost is back to O(rows). Both are worth having
2193
+ * and they are not the same operation, so the node says which one it did:
2194
+ * `renameStagePayload` reports `metadataOnly`, and the run log prints it.
2195
+ *
2196
+ * ## The config
2197
+ *
2198
+ * {@link columns} is a map of **old name → new name**, applied
2199
+ * **simultaneously** rather than in sequence. `{"a": "b", "b": "c"}` maps `a` to
2200
+ * `b` and `b` to `c`; it does not chain `a → b → c`. Sequential application
2201
+ * would make the result depend on the iteration order of a JSON object, which is
2202
+ * not a thing to build a load on.
2203
+ *
2204
+ * A `Record` rather than a list of pairs, and the reason is a refusal it buys
2205
+ * for free: a key cannot appear twice in an object, so *two renames of the same
2206
+ * source column* is unrepresentable. The mirror mistake — two renames **onto**
2207
+ * the same target — is representable and is refused by `validateWorkflow`, which
2208
+ * can see it from the config alone with nothing to run.
2209
+ *
2210
+ * The four remaining edge cases, all decided rather than discovered:
2211
+ *
2212
+ * - **A column the map does not name** — {@link unnamed} decides, and it
2213
+ * defaults to `keep`.
2214
+ * - **A named column that is not in a given record** — nothing happens to that
2215
+ * record. This is not an error, because a batch legitimately holds rows with
2216
+ * different key-sets; that is the entire reason the stage encoding is a shape
2217
+ * *dictionary* and not one column list. A source column that turns out to be
2218
+ * in **no** row of the whole run is reported loudly in the run log, because it
2219
+ * is almost always a typo in a header and the symptom otherwise is a column of
2220
+ * NULLs and a green run.
2221
+ * - **A rename onto a name the record already holds** — refused, at run time,
2222
+ * naming both columns. There are two columns and one name, and every rule for
2223
+ * picking a winner is arbitrary. It is detected per *shape* rather than per
2224
+ * row, so it fails on the first batch rather than at row ninety thousand.
2225
+ * Under `unnamed: 'drop'` there is no collision to have: a column the map does
2226
+ * not name does not exist in the output, so it cannot occupy anything.
2227
+ * - **`a → a`** — allowed. Under `keep` it is a no-op; under `drop` it is how a
2228
+ * column is *selected*, which is a real use.
2229
+ *
2230
+ * ## What the target names have to be
2231
+ *
2232
+ * {@link WORKFLOW_FILTER_COLUMN_PATTERN}, checked at authoring time. Two
2233
+ * separate reasons, and both are about a failure that reports success:
2234
+ *
2235
+ * - `property-names.ts` refuses a *published property* whose name cannot become
2236
+ * a column, and its docblock is the record of what happens when a name and the
2237
+ * key in the record disagree: the load looks every field up as `row[name]`, so
2238
+ * the column takes NULL in every row and the run is green. Thirteen types went
2239
+ * in that way. A rename is the tool that makes the record's key match the
2240
+ * property, so a rename that produces a name no property can carry is the
2241
+ * trap re-armed one node upstream.
2242
+ * - The filter node's columns follow the same pattern precisely so a predicate
2243
+ * could one day be pushed into a `WHERE`. A rename whose target cannot be
2244
+ * named by a filter would author a graph today that could never be pushed down
2245
+ * tomorrow.
2246
+ *
2247
+ * The *source* names are deliberately unconstrained. `Mgmt Cd`, `VEH Type Name`
2248
+ * and `Reg Number` are exactly what real drops are keyed by, and being able to
2249
+ * name them is the entire point of the node.
2250
+ */
2251
+ export interface WorkflowRenameNode extends WorkflowNodeBase {
2252
+ kind: 'rename';
2253
+ /**
2254
+ * Old name → new name, applied simultaneously. Never empty; at most
2255
+ * {@link WORKFLOW_RENAME_MAX_COLUMNS} entries; every target matches
2256
+ * {@link WORKFLOW_FILTER_COLUMN_PATTERN} and no two share one.
2257
+ *
2258
+ * Empty is refused rather than treated as a no-op, for the reason an empty
2259
+ * filter group is: under `keep` it is a node that draws as configured and does
2260
+ * nothing, and under `drop` it deletes every column of every row and commits
2261
+ * the result. Two silent opposites reached by deleting the last row of a form.
2262
+ */
2263
+ columns: Record<string, string>;
2264
+ /**
2265
+ * What happens to the columns this node does not name. Absent means `keep`.
2266
+ *
2267
+ * Absent is `keep` rather than being required, because `keep` is the node this
2268
+ * one is called after: a rename that also deleted everything it did not
2269
+ * mention would be a projection wearing the word "rename", and somebody would
2270
+ * find that out by looking at a committed snapshot.
2271
+ *
2272
+ * `drop` is here because the shape it replaces is real —
2273
+ * `records.map((r) => ({ mgmtCd: r["Mgmt Cd"] }))` is a rename *and* a
2274
+ * projection, and it is what a drop of Air Force fleet data forces today. It
2275
+ * costs the metadata-only property (see the docblock above), and the run says
2276
+ * so rather than leaving the difference to be guessed at.
2277
+ */
2278
+ unnamed?: WorkflowRenameUnnamed;
2279
+ }
2280
+ /** {@link WorkflowRenameNode.unnamed}, resolved. One reader of the default. */
2281
+ export declare function workflowRenameUnnamed(node: WorkflowRenameNode): WorkflowRenameUnnamed;
2282
+ /**
2283
+ * Every reason a rename map cannot be stored, as sentences, or empty.
2284
+ *
2285
+ * One function, called by {@link validateWorkflow}, by the HTTP boundary and by
2286
+ * the canvas, for the reason `validateWorkflow` itself is shared: a screen that
2287
+ * checked a target name against its own copy of the pattern is a screen that
2288
+ * eventually accepts something the server refuses, halfway through a save.
2289
+ *
2290
+ * All of them rather than the first, exactly as
2291
+ * {@link refuseUnpublishablePropertyNames} argues: a map of forty columns typed
2292
+ * in one sitting is usually wrong about several in the same way.
2293
+ */
2294
+ export declare function renameColumnRefusals(columns: Record<string, string>): string[];
2295
+ /**
2296
+ * Whether a stored rename map is one this build can run.
2297
+ *
2298
+ * Refused rather than repaired, the stance {@link isWorkflowFilterPredicate}
2299
+ * takes about a predicate and for the same reason one step further along: a
2300
+ * rename read back with one entry silently dropped is a graph that commits a
2301
+ * column of NULLs under a name nobody can now explain.
2302
+ *
2303
+ * `Object.entries` rather than a `for…in`, so an inherited key cannot enter the
2304
+ * map, and the values are checked one by one rather than trusted from the type.
2305
+ */
2306
+ export declare function isWorkflowRenameColumns(value: unknown): value is Record<string, string>;
1879
2307
  /**
1880
2308
  * A discriminated union, so narrowing a node is `node.kind === "sink"` and
1881
2309
  * never a type assertion. This is why the kind list is not simply a string on
1882
2310
  * one node shape with every field optional: that shape lets a source node carry
1883
2311
  * a `transformId` and nothing catches it.
1884
2312
  */
1885
- export type WorkflowNode = WorkflowSourceNode | WorkflowTransformNode | WorkflowSinkNode | WorkflowCallNode | WorkflowIfNode | WorkflowFilterNode;
2313
+ export type WorkflowNode = WorkflowSourceNode | WorkflowTransformNode | WorkflowSinkNode | WorkflowCallNode | WorkflowIfNode | WorkflowFilterNode | WorkflowRenameNode;
1886
2314
  /**
1887
2315
  * The node kinds that can be saved once and used in several graphs.
1888
2316
  *
@@ -1934,6 +2362,11 @@ export declare function isReusableNodeKind(value: unknown): value is ReusableNod
1934
2362
  * may not have, and a filter is worse: {@link WorkflowFilterNode.narrows} is
1935
2363
  * an acknowledgement about *this* graph's sinks, so a shared one would carry
1936
2364
  * somebody else's acknowledgement into a graph they never saw.
2365
+ * - `rename` — the same argument as `if` and `filter`, and the sharpest version
2366
+ * of it: a rename map names the source's own spelling of its columns, so it is
2367
+ * *about* one drop of one file. `Mgmt Cd → mgmtCd` saved under a name and
2368
+ * dropped into a graph reading a different system renames nothing at all, and
2369
+ * the symptom is a column of NULLs rather than a failure.
1937
2370
  */
1938
2371
  export declare const NODE_KIND_IS_REUSABLE: {
1939
2372
  readonly source: true;
@@ -1942,6 +2375,7 @@ export declare const NODE_KIND_IS_REUSABLE: {
1942
2375
  readonly call: false;
1943
2376
  readonly if: false;
1944
2377
  readonly filter: false;
2378
+ readonly rename: false;
1945
2379
  };
1946
2380
  /** Whether this kind can be saved as a reusable node. Reads {@link NODE_KIND_IS_REUSABLE}. */
1947
2381
  export declare function nodeKindIsReusable(kind: WorkflowNodeKind): boolean;
@@ -2740,7 +3174,7 @@ export interface CallableWorkflowBlock {
2740
3174
  }
2741
3175
  export declare function callableWorkflowBlock(ref: CallableWorkflowRef): CallableWorkflowBlock | undefined;
2742
3176
  /** Every way a graph can be refused. Exported so a canvas can key off the code. */
2743
- 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"];
3177
+ 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", "rename-invalid", "column-not-produced", "version-pin-invalid"];
2744
3178
  export type WorkflowIssueCode = (typeof WORKFLOW_ISSUE_CODES)[number];
2745
3179
  export interface WorkflowValidationIssue {
2746
3180
  code: WorkflowIssueCode;
@@ -2896,6 +3330,62 @@ export declare function workflowNodeRuns(entry: {
2896
3330
  * a thing to plan for.
2897
3331
  */
2898
3332
  export declare function workflowGraphHash(graph: WorkflowGraph): string;
3333
+ /**
3334
+ * Every column a filter predicate names, once each, in the order they appear.
3335
+ *
3336
+ * Its own function rather than a walk inlined into the validator, because two
3337
+ * things want it — the refusal below and anything on a screen that wants to say
3338
+ * which columns a node depends on — and a second copy of a tree walk is a second
3339
+ * copy that forgets the `oneOf` branch.
3340
+ */
3341
+ export declare function workflowFilterColumns(predicate: WorkflowFilterPredicate): string[];
3342
+ /**
3343
+ * The columns that can reach this node, when the graph knows — and `undefined`
3344
+ * when it does not.
3345
+ *
3346
+ * ## What this is for
3347
+ *
3348
+ * It is the one thing a declarative rename buys that a transform cannot, and it
3349
+ * is worth being precise about how far it reaches rather than overselling it.
3350
+ *
3351
+ * With a JS transform, the catalog cannot know what columns come out — the
3352
+ * answer is inside a function body — which is why the property-name rule in
3353
+ * `property-names.ts` fires at publish time and why a mismatch between a
3354
+ * property and a record key is discovered as a column of NULLs. A rename is
3355
+ * **data**, so for one arrangement the answer is exact:
3356
+ *
3357
+ * > A rename with `unnamed: 'drop'` produces its targets and **nothing else**,
3358
+ * > whatever it was handed.
3359
+ *
3360
+ * That set is *closed* — an upper bound that holds regardless of what is
3361
+ * upstream — and it survives every node that does not touch columns. So a filter
3362
+ * or a second rename downstream of one can be told, at authoring time, that it
3363
+ * names a column which cannot be there.
3364
+ *
3365
+ * ## What it deliberately does not claim
3366
+ *
3367
+ * - **It is an upper bound, not the output.** A target only appears in a row
3368
+ * whose input actually held the source column. So a column *inside* the set
3369
+ * may still be absent, and nothing here says otherwise.
3370
+ * - **A `keep` rename tells you nothing on its own.** Its output is its input
3371
+ * with some keys re-labelled, and its input is unknown unless something
3372
+ * upstream closed it. So `undefined` propagates, and that is the honest
3373
+ * answer rather than an empty set.
3374
+ * - **A source, a transform and a call are always unknown.** A source's shape is
3375
+ * discovered against the live system rather than declared in the graph; a
3376
+ * transform is a function body; a call is a workflow this graph does not own.
3377
+ * - **It says nothing about a sink's declared properties.** That is the check
3378
+ * worth wanting — "this sink writes a property no upstream node produces" —
3379
+ * and it is *not* available here: a {@link WorkflowSinkNode} carries a
3380
+ * `targetType` and nothing else, so the property list would have to be
3381
+ * threaded into a validator that is pure and dependency-free on purpose. What
3382
+ * is built instead is the run log, which prints the columns a rename produced.
3383
+ *
3384
+ * Cycles answer `undefined` rather than looping. `validateWorkflow` refuses a
3385
+ * cyclic graph before it gets here, but the canvas calls this while a graph is
3386
+ * being drawn and is entitled to a wrong-but-terminating answer.
3387
+ */
3388
+ export declare function workflowKnownColumns(graph: WorkflowGraph, nodeId: string): ReadonlySet<string> | undefined;
2899
3389
  /**
2900
3390
  * Narrow a stored node, loudly.
2901
3391
  *
@@ -3244,6 +3734,39 @@ export interface CatalogStageStore {
3244
3734
  * as long as something might still resume onto them.
3245
3735
  */
3246
3736
  dropStages(runId: string): Promise<number>;
3737
+ /**
3738
+ * The batch exactly as it is stored, without decoding it into rows.
3739
+ *
3740
+ * Optional, and the only thing that reads it is the `rename` node. See
3741
+ * {@link renameStagePayload}: a staged batch names its columns once, in
3742
+ * `shapes`, and carries the data in positional arrays — so renaming a column
3743
+ * is a rewrite of tens of strings rather than a rebuild of a hundred thousand
3744
+ * objects. `readStage` cannot express that, because decoding to
3745
+ * `Record<string, unknown>` *is* the rebuild.
3746
+ *
3747
+ * Optional rather than required so that a store written against the shipped
3748
+ * interface keeps working: {@link supportsStagePayloads} is what asks, and a
3749
+ * store that answers no gets the row path, which produces the same rows more
3750
+ * slowly. It is deliberately `unknown` — the encoding is
3751
+ * `catalog.stage-encoding.ts`'s business and a store's job is to hand back
3752
+ * what it was given.
3753
+ */
3754
+ readStagePayload?(ref: {
3755
+ runId: string;
3756
+ nodeId: string;
3757
+ batch: number;
3758
+ }): Promise<unknown>;
3759
+ /** The other half. Idempotent per `(runId, nodeId, batch)`, exactly like {@link writeStage}. */
3760
+ writeStagePayload?(input: {
3761
+ runId: string;
3762
+ nodeId: string;
3763
+ batch: number;
3764
+ payload: unknown;
3765
+ /** How many rows the payload holds. The store does not decode it to count. */
3766
+ rows: number;
3767
+ }): Promise<{
3768
+ written: number;
3769
+ }>;
3247
3770
  }
3248
3771
  /**
3249
3772
  * Whether this store can hold workflows at all.
@@ -3318,6 +3841,21 @@ export type CatalogReusableNodeStore = Required<Pick<CatalogPipelineStore, 'list
3318
3841
  */
3319
3842
  export declare function supportsReusableNodes(store: CatalogPipelineStore): store is CatalogPipelineStore & CatalogReusableNodeStore;
3320
3843
  export declare function supportsWorkflowStages(store: CatalogPipelineStore): store is CatalogPipelineStore & CatalogStageStore;
3844
+ /**
3845
+ * Whether this store will hand a staged batch over without decoding it.
3846
+ *
3847
+ * Both methods, never one: a rename that could read the payload and not write
3848
+ * one back would have to decode its own output to store it, which is the rebuild
3849
+ * the pair exists to avoid. The methods rather than a flag, the same argument
3850
+ * {@link supportsWorkflows} makes.
3851
+ *
3852
+ * A store that answers no is not broken and nothing degrades except speed — the
3853
+ * rename node falls back to `readStage`/`writeStage` and produces identical
3854
+ * rows. Which path ran is said in the run log, because "this rename was
3855
+ * metadata-only" is a claim, and a claim that could quietly stop being true is
3856
+ * worse than no claim.
3857
+ */
3858
+ export declare function supportsStagePayloads(store: CatalogPipelineStore): store is CatalogPipelineStore & CatalogStageStore & Required<Pick<CatalogStageStore, 'readStagePayload' | 'writeStagePayload'>>;
3321
3859
  /**
3322
3860
  * A store that really does hold operator-set expectations, all four members
3323
3861
  * present.
@@ -3384,6 +3922,13 @@ export interface CatalogPipelineStore extends Partial<CatalogWorkflowStore>, Par
3384
3922
  saveTransform(input: Pick<CatalogTransform, 'name' | 'language' | 'code'> & {
3385
3923
  id?: string;
3386
3924
  description?: string;
3925
+ /**
3926
+ * Absent leaves the stored mode alone rather than resetting it to
3927
+ * `'batch'`, which is what a caller written before this field existed
3928
+ * means and the only reading under which such a caller cannot silently
3929
+ * change what a transform computes. See {@link TRANSFORM_MODES}.
3930
+ */
3931
+ mode?: TransformMode;
3387
3932
  }, createdBy: string): Promise<CatalogTransform>;
3388
3933
  deleteTransform(id: string): Promise<boolean>;
3389
3934
  /**