@pipelex/sdk 0.26.0 → 0.27.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.
package/dist/models.d.ts CHANGED
@@ -64,7 +64,7 @@ export interface PipelexRunResultStart extends RunResultStart {
64
64
  }
65
65
  /**
66
66
  * The hosted tooling routes' own selector — the layer-3 extension the platform
67
- * adds on `POST /v1/validate`, `/v1/resolve`, and `/v1/codegen`: a stored
67
+ * adds on `POST /v1/validate`, `/v1/resolve`, `/v1/codegen`, and `/v1/pipe-io`: a stored
68
68
  * method's catalog id (`mt_…`), resolved server-side against the org's catalog
69
69
  * and injected as inline source before the runner sees the request. A **pass-
70
70
  * through to the hosted API**: nothing is expanded client-side, and it is
@@ -90,8 +90,9 @@ export interface PipelexHostedToolingExtensions {
90
90
  * exactly one of inline contents / `method_ref` / `method_id` per request.
91
91
  *
92
92
  * Lives here, beside the other wire shapes, rather than on the client: it is
93
- * the request's shape, and `prepareInputs` composes a `validate` call of its own
94
- * without importing the client back (which would close a module cycle).
93
+ * the request's shape, and a module that composes a `validate` call of its own
94
+ * can name it without importing the client back (which would close a module
95
+ * cycle).
95
96
  */
96
97
  export type ValidateMethodSelector = {
97
98
  method_ref: string;
@@ -207,7 +208,9 @@ export interface PipelexValidationReport extends ValidationReport {
207
208
  * `null` is the server's verdict that there is none, and a consumer stops there
208
209
  * rather than deriving one of its own; an ABSENT field means the server predates
209
210
  * the field, and only then does a blueprint-derived default stand. That is the rule
210
- * `prepareInputs` applies and the one `pipelex-mcp` states in its `SPEC.md`.
211
+ * `pipelex-mcp` states in its `SPEC.md`. It is NOT `/v1/pipe-io`'s field of the same
212
+ * name ({@link PipeIOValidReport.default_pipe_ref}), which refuses to choose among
213
+ * several declaring domains where this one names the first.
211
214
  */
212
215
  default_pipe_ref?: string | null;
213
216
  /** Best-effort execution graph of the main pipe; `null` with no `main_pipe` or on degrade. */
@@ -479,7 +482,7 @@ export interface MthdsFileItem {
479
482
  }
480
483
  /**
481
484
  * The closure selector every crate-family route shares — `/v1/resolve`, `/v1/codegen`,
482
- * and `/v1/build/*` (mirror of the server's `MthdsFilesRequest`).
485
+ * `/v1/pipe-io`, and `/v1/build/*` (mirror of the server's `MthdsFilesRequest`).
483
486
  *
484
487
  * Supply the closure EITHER as inline `files` OR as a `method_ref` — never both, and
485
488
  * never neither (both arms are a request-shape `422`). An **address-form** `method_ref`
@@ -510,7 +513,7 @@ export interface BuildRequestBase extends CrateRequestBase {
510
513
  pipe_ref?: string;
511
514
  /**
512
515
  * The `/v1/build/*` projections take NO `method_id` — the hosted platform's
513
- * tooling selector covers `validate`/`resolve`/`codegen` only, and the build
516
+ * tooling selector covers `validate`/`resolve`/`codegen`/`pipe-io` only, and the build
514
517
  * routes are deliberately excluded (they are frozen, being replaced by the
515
518
  * codegen surface). Pinned to `never` so a stored-method caller reaches for
516
519
  * `getMethodClosure` instead of a field no server resolves.
@@ -544,15 +547,16 @@ export interface PipeSpecRequest {
544
547
  }
545
548
  /**
546
549
  * The `is_valid: false` arm shared by every crate-family route — `/v1/build/*`,
547
- * `/v1/resolve`, and `/v1/codegen` (mirror of the server's one `CrateInvalidReport`).
550
+ * `/v1/resolve`, `/v1/codegen`, and `/v1/pipe-io` (mirror of the server's one
551
+ * `CrateInvalidReport`).
548
552
  *
549
553
  * They all follow `/validate`'s discipline: an unresolvable closure is the
550
554
  * *successful product* of the call (the request was well-formed, the library was
551
555
  * not), so it rides a **200** discriminated on `is_valid` — never a 4xx. Only a
552
556
  * no-verdict condition throws `ApiResponseError`: a request the route cannot act on
553
- * (an unknown `pipe_ref` on the build routes, an unknown `kind`/`target` on codegen),
554
- * the reserved `method_ref`, auth, a server fault. Branch on `is_valid`, never on the
555
- * transport.
557
+ * (an unknown `pipe_ref` on the build routes and `pipe-io`, an unknown `kind`/`target`
558
+ * on codegen), the reserved `method_ref`, auth, a server fault. Branch on `is_valid`,
559
+ * never on the transport.
556
560
  */
557
561
  export interface CrateInvalidReport {
558
562
  is_valid: false;
@@ -735,4 +739,90 @@ export interface CodegenValidReport {
735
739
  }
736
740
  /** The `POST /v1/codegen` 200 response — pattern-match `is_valid` before reading the arm. */
737
741
  export type CodegenResponse = CodegenValidReport | CrateInvalidReport;
742
+ /**
743
+ * `POST /v1/pipe-io` request — the crate envelope plus the hosted `method_id` selector
744
+ * (exactly one of `files` / `method_ref` / `method_id`; see
745
+ * {@link PipelexHostedToolingExtensions}), a pipe selector, and two opt-ins.
746
+ *
747
+ * There is no `views` field: the valid arm always carries all three artifacts.
748
+ */
749
+ export interface PipeIORequest extends CrateRequestBase, PipelexHostedToolingExtensions {
750
+ /**
751
+ * The pipe to describe, as a QUALIFIED `domain.pipe_code` ref. Omit it and the
752
+ * server's selection chain decides: a fetched package's manifest `main_pipe`, else
753
+ * the closure's single `main_pipe` declaration. A selection the server refuses is a
754
+ * `422` typed by what went wrong, with `error_domain: "input"` and the candidates, where
755
+ * there are some, in its `detail`: `EntryPipeNotFoundError` for a ref that names no pipe
756
+ * or (without `all_pipes`) a method with no entry pipe, `EntryPipeAmbiguousError` for a
757
+ * code that matches pipes in several domains or (without `all_pipes`) several `main_pipe`
758
+ * declarations. A malformed request stays a `ValidationError` `422`.
759
+ *
760
+ * The server does not refuse a bare code yet: a bare ref that matches one pipe is
761
+ * resolved across domains, and the valid arm reports the qualified ref.
762
+ */
763
+ pipe_ref?: string;
764
+ /**
765
+ * Describe every pipe the closure loads instead of the selected one. The artifact maps
766
+ * are then keyed by every pipe, and the route never refuses for want of an entry pipe:
767
+ * the valid arm's `pipe_ref` is the requested ref, else the method's own entry pipe,
768
+ * else `null`. Defaults to `false` server-side.
769
+ */
770
+ all_pipes?: boolean;
771
+ /**
772
+ * Echo the resolved closure's `.mthds` files on the valid arm as `files`, in the
773
+ * request's own `files[]` shape. Defaults to `false` server-side, and the field is then
774
+ * absent from the answer.
775
+ */
776
+ include_files?: boolean;
777
+ }
778
+ /**
779
+ * The `/v1/pipe-io` valid arm — a method's three I/O artifacts, with the selection and
780
+ * the runnability facts beside them.
781
+ *
782
+ * The three artifacts are the MTHDS standard's own types, imported from `mthds/protocol`
783
+ * and never restated here. The maps share one key set: the resolved `pipe_ref` alone by
784
+ * default, every pipe the closure loads under `all_pipes`. For a closure `/v1/validate`
785
+ * also accepts, each map equals validate's same-named field restricted to the same keys.
786
+ *
787
+ * `is_valid: true` means what it means on `/v1/resolve`: the closure parsed, loaded and
788
+ * passed static validation. No dry run ran; that verdict stays `validate`'s.
789
+ */
790
+ export interface PipeIOValidReport {
791
+ is_valid: true;
792
+ /**
793
+ * The qualified ref the selection resolved, read off the resolved pipe and never echoed
794
+ * from the request. `null` only under `all_pipes` when no pipe was requested and the
795
+ * method declares no single entry pipe.
796
+ */
797
+ pipe_ref: string | null;
798
+ /** The standard's pipe I/O contracts, keyed by qualified `pipe_ref`. */
799
+ pipe_io_contracts: PipeIOContracts;
800
+ /** The standard's input-form descriptors, keyed by qualified `pipe_ref`. */
801
+ input_form: InputForm;
802
+ /** The standard's output-form descriptors, keyed by qualified `pipe_ref`. */
803
+ output_form: OutputForm;
804
+ /**
805
+ * The method's own entry pipe: the selection chain without the request's `pipe_ref` —
806
+ * the fetched manifest's `main_pipe`, else the closure's single declaration. A request
807
+ * that omits `pipe_ref` always answers `pipe_ref === default_pipe_ref`. A stated `null`
808
+ * when the chain finds none or several.
809
+ *
810
+ * NOT `PipelexValidationReport.default_pipe_ref`, which is the run default and names
811
+ * the first of several declaring domains where this chain refuses to choose.
812
+ */
813
+ default_pipe_ref: string | null;
814
+ /** Qualified refs of every pipe of the closure still declared as a signature, as on `validate`. */
815
+ pending_signatures: string[];
816
+ /** `pending_signatures` is empty, as on `validate`. No dry run backs it. */
817
+ is_runnable: boolean;
818
+ /**
819
+ * The resolved closure's `.mthds` files, in the request's `files[]` shape. Present only
820
+ * when the request passed `include_files: true`: the request's files for inline
821
+ * `files`, the fetched package's `.mthds` files under their package-relative paths for
822
+ * a `method_ref`, the stored files under their stored names for a hosted `method_id`.
823
+ */
824
+ files?: MthdsFileItem[];
825
+ }
826
+ /** The `POST /v1/pipe-io` 200 response — pattern-match `is_valid` before reading the arm. */
827
+ export type PipeIOResponse = PipeIOValidReport | CrateInvalidReport;
738
828
  export {};
@@ -1,15 +1,17 @@
1
1
  /**
2
- * `prepareInputs` — signature-driven input preparation. Resolves the target
3
- * pipe's declared inputs from the **input-form descriptor** on the validate
4
- * report, interprets the caller's inputs top-down against it, uploads the
5
- * file-bearing values, and returns rewritten inputs (canonical content carrying
2
+ * `prepareInputs` — signature-driven input preparation. Reads the target pipe's
3
+ * declared inputs from the **input-form descriptor** `POST /v1/pipe-io` returns,
4
+ * interprets the caller's inputs top-down against it, uploads the file-bearing
5
+ * values, and returns rewritten inputs (canonical content carrying
6
6
  * `pipelex-storage://` in `url`) plus one upload record per prepared asset.
7
7
  *
8
8
  * The method is named the same three ways every other method-taking operation
9
9
  * takes it — inline `files`, a `method_ref` address, or a stored `method_id` —
10
- * exactly one per call. All three are pass-throughs to `POST /v1/validate`,
11
- * which resolves an address on the runner and an id on the platform, so nothing
12
- * is expanded client-side.
10
+ * exactly one per call. All three are pass-throughs to `POST /v1/pipe-io`, which
11
+ * resolves an address on the runner and an id on the platform, so nothing is
12
+ * expanded client-side. The route also SELECTS the pipe: the caller's qualified
13
+ * `pipe_ref`, else the method's own entry pipe, and the ref it answers with is
14
+ * the one prepared. It runs no dry run, so preparing inputs costs one static load.
13
15
  *
14
16
  * Per input, the caller may submit EITHER the **compact** value (a bare source /
15
17
  * canonical `{url}` content) OR the explicit `{ concept, content }` envelope —
@@ -19,26 +21,27 @@
19
21
  * `input_normalizer.py`).
20
22
  *
21
23
  * **The descriptor is the classifier, never the value's shape.** `input_form`
22
- * (the MTHDS standard's artifact, opted into with `views: ["input_form"]`)
23
- * states the kind at every depth — `document` / `image` mark a file position,
24
- * `object` recurses through `fields`, `list` through `item`, everything else
25
- * passes through. That is what makes an OPTIONAL nested file field prepare like
26
- * a required one, and a `text` field merely *named* `url` stay untouched: both
27
- * were misread while the signature came from the rendered inputs template, whose
28
- * file signal was a `url`-bearing dict. See `docs/input-preparation.md` and the
29
- * shared behavior matrix (`wip/upload/behavior-matrix.md`).
24
+ * (the MTHDS standard's artifact) states the kind at every depth — `document` /
25
+ * `image` mark a file position, `object` recurses through `fields`, `list`
26
+ * through `item`, everything else passes through. That is what makes an
27
+ * OPTIONAL nested file field prepare like a required one, and a `text` field
28
+ * merely *named* `url` stay untouched: both were misread while the signature
29
+ * came from the rendered inputs template, whose file signal was a `url`-bearing
30
+ * dict. See `docs/input-preparation.md`, which `pipelex-sdk-python` mirrors case
31
+ * for case.
30
32
  */
31
- import type { MthdsFileItem, PipelexValidationResult, ValidateMethodSelector } from "./models.js";
33
+ import type { MthdsFileItem, PipeIORequest, PipeIOResponse } from "./models.js";
32
34
  import type { UploadCapableClient, UploadRecord } from "./upload.js";
33
35
  /** The shared half of the request: the target pipe and the caller's inputs. */
34
36
  export interface PrepareInputsBase {
35
37
  /**
36
38
  * The pipe to prepare inputs for, as a QUALIFIED `domain.pipe_code` ref. Omit
37
- * it to default — see "Pipe selection" in `docs/input-preparation.md`. A bare
38
- * `pipe_code` is refused: the descriptor is keyed by qualified refs, and search
39
- * is a run-route affordance this helper deliberately does not grow. So is an
39
+ * it and the server picks the method's own entry pipe — see "Pipe selection" in
40
+ * `docs/input-preparation.md`. A bare `pipe_code` is refused before any request:
41
+ * a request names a pipe by its qualified ref, and search is a run-route
42
+ * affordance this helper deliberately does not grow. So is an
40
43
  * `alias->domain.pipe_code` ref: the alias names a dependency package's pipe,
41
- * which the descriptor does not describe (it covers the method's own pipes).
44
+ * and preparation covers the method's own pipes.
42
45
  */
43
46
  pipe_ref?: string;
44
47
  /** The caller's inputs (variable name → value), compact or explicit-envelope per input. */
@@ -47,7 +50,7 @@ export interface PrepareInputsBase {
47
50
  /**
48
51
  * How the method is named — exactly one of the three selectors, each pinning the
49
52
  * other two to `never` so a second one is a compile error (the same XOR
50
- * `ValidateMethodSelector` states for the tooling routes). All three reach the
53
+ * `ValidateMethodSelector` states for `validate`). All three reach the
51
54
  * server as-is: `files` inline, `method_ref` resolved by the runner, `method_id`
52
55
  * resolved by the platform.
53
56
  */
@@ -74,15 +77,12 @@ export interface PreparedInputs {
74
77
  uploads: UploadRecord[];
75
78
  }
76
79
  /**
77
- * The client surface `prepareInputs` needs: raw `upload`, and `validate` as the
78
- * signature source. Typed as the client's own `validate` signature so
80
+ * The client surface `prepareInputs` needs: raw `upload`, and `pipeIo` as the
81
+ * signature source. Typed as the client's own `pipeIo` signature so
79
82
  * `PipelexApiClient` satisfies it structurally.
80
83
  */
81
84
  export interface PrepareCapableClient extends UploadCapableClient {
82
- validate(source: string[] | ValidateMethodSelector, allowSignatures?: boolean, mthdsSources?: string[], render?: string[], views?: string[], options?: {
83
- timeoutMs?: number;
84
- signal?: AbortSignal;
85
- }): Promise<PipelexValidationResult>;
85
+ pipeIo(request: PipeIORequest): Promise<PipeIOResponse>;
86
86
  }
87
87
  /**
88
88
  * Prepare a pipe's inputs: upload local/byte/data-URL assets at the signature's
@@ -90,12 +90,13 @@ export interface PrepareCapableClient extends UploadCapableClient {
90
90
  * records. HTTP(S) URLs and existing `pipelex-storage://` URIs pass through
91
91
  * unchanged. All failures are raised before any run is created.
92
92
  *
93
- * The signature comes from one `POST /v1/validate` with `views: ["input_form"]`,
94
- * whatever the selector — inline `files`, a `method_ref` the runner resolves, or
95
- * a `method_id` the platform resolves. A closure that does not load, an unknown
96
- * `pipe_ref`, no default pipe, or a report with no descriptor throw
97
- * {@link InputPreparationError}; a no-verdict condition from the route (a
98
- * malformed selector, an unknown or foreign-org id, no package at the address,
99
- * auth, a server fault) surfaces as `ApiResponseError`, unchanged.
93
+ * The pipe and its signature come from one `POST /v1/pipe-io`, whatever the
94
+ * selector — inline `files`, a `method_ref` the runner resolves, or a
95
+ * `method_id` the platform resolves. A bare or alias-qualified `pipe_ref`, a
96
+ * closure that does not load, and a selection the route refuses (an unknown
97
+ * `pipe_ref`, no entry pipe, several) throw {@link InputPreparationError}; any
98
+ * other failure from the route (a malformed selector, an unknown or foreign-org
99
+ * id, no package at the address, auth, a server fault, a deployment that does not
100
+ * serve the route) surfaces as `ApiResponseError`, unchanged.
100
101
  */
101
102
  export declare function prepareInputs(client: PrepareCapableClient, request: PrepareInputsRequest): Promise<PreparedInputs>;
@@ -1,15 +1,17 @@
1
1
  /**
2
- * `prepareInputs` — signature-driven input preparation. Resolves the target
3
- * pipe's declared inputs from the **input-form descriptor** on the validate
4
- * report, interprets the caller's inputs top-down against it, uploads the
5
- * file-bearing values, and returns rewritten inputs (canonical content carrying
2
+ * `prepareInputs` — signature-driven input preparation. Reads the target pipe's
3
+ * declared inputs from the **input-form descriptor** `POST /v1/pipe-io` returns,
4
+ * interprets the caller's inputs top-down against it, uploads the file-bearing
5
+ * values, and returns rewritten inputs (canonical content carrying
6
6
  * `pipelex-storage://` in `url`) plus one upload record per prepared asset.
7
7
  *
8
8
  * The method is named the same three ways every other method-taking operation
9
9
  * takes it — inline `files`, a `method_ref` address, or a stored `method_id` —
10
- * exactly one per call. All three are pass-throughs to `POST /v1/validate`,
11
- * which resolves an address on the runner and an id on the platform, so nothing
12
- * is expanded client-side.
10
+ * exactly one per call. All three are pass-throughs to `POST /v1/pipe-io`, which
11
+ * resolves an address on the runner and an id on the platform, so nothing is
12
+ * expanded client-side. The route also SELECTS the pipe: the caller's qualified
13
+ * `pipe_ref`, else the method's own entry pipe, and the ref it answers with is
14
+ * the one prepared. It runs no dry run, so preparing inputs costs one static load.
13
15
  *
14
16
  * Per input, the caller may submit EITHER the **compact** value (a bare source /
15
17
  * canonical `{url}` content) OR the explicit `{ concept, content }` envelope —
@@ -19,19 +21,33 @@
19
21
  * `input_normalizer.py`).
20
22
  *
21
23
  * **The descriptor is the classifier, never the value's shape.** `input_form`
22
- * (the MTHDS standard's artifact, opted into with `views: ["input_form"]`)
23
- * states the kind at every depth — `document` / `image` mark a file position,
24
- * `object` recurses through `fields`, `list` through `item`, everything else
25
- * passes through. That is what makes an OPTIONAL nested file field prepare like
26
- * a required one, and a `text` field merely *named* `url` stay untouched: both
27
- * were misread while the signature came from the rendered inputs template, whose
28
- * file signal was a `url`-bearing dict. See `docs/input-preparation.md` and the
29
- * shared behavior matrix (`wip/upload/behavior-matrix.md`).
24
+ * (the MTHDS standard's artifact) states the kind at every depth — `document` /
25
+ * `image` mark a file position, `object` recurses through `fields`, `list`
26
+ * through `item`, everything else passes through. That is what makes an
27
+ * OPTIONAL nested file field prepare like a required one, and a `text` field
28
+ * merely *named* `url` stay untouched: both were misread while the signature
29
+ * came from the rendered inputs template, whose file signal was a `url`-bearing
30
+ * dict. See `docs/input-preparation.md`, which `pipelex-sdk-python` mirrors case
31
+ * for case.
30
32
  */
31
- import { InputPreparationError } from "./errors.js";
33
+ import { ApiResponseError, InputPreparationError } from "./errors.js";
32
34
  import { uploadFile } from "./upload.js";
33
35
  const PIPELEX_STORAGE_SCHEME = "pipelex-storage://";
34
36
  const HTTP_URL_RE = /^https?:\/\//i;
37
+ /**
38
+ * The `error_type`s of the route's pipe-selection refusals — the only `422`s
39
+ * `prepareInputs` turns into an `InputPreparationError`. They are the runtime's
40
+ * entry-lookup classes, the vocabulary the run routes speak too: a ref that names
41
+ * no pipe, or no entry pipe at all, is `EntryPipeNotFoundError`; a code that
42
+ * matches pipes in several domains, or several `main_pipe` declarations, is
43
+ * `EntryPipeAmbiguousError`. Every other `422` (a malformed body, an over-limit
44
+ * file, a fetched package with no `.mthds` file, a stored method with no source)
45
+ * shares the generic `ValidationError` type and stays an `ApiResponseError`.
46
+ */
47
+ const PIPE_SELECTION_ERROR_TYPES = new Set([
48
+ "EntryPipeNotFoundError",
49
+ "EntryPipeAmbiguousError",
50
+ ]);
35
51
  /** Strict plain-object test — excludes arrays, `Uint8Array`, `Blob`, and other exotics. */
36
52
  function isPlainObject(value) {
37
53
  return Object.prototype.toString.call(value) === "[object Object]";
@@ -198,7 +214,7 @@ async function resolveNode(ctx, node, callerValue) {
198
214
  * Normalize and check the three selectors. Empty is absent (`files: []`,
199
215
  * `method_ref: ""`, `method_id: " "`), mirroring the run options' rule and the
200
216
  * Python `CrateRequestBase` normalisers, and exactly one must remain — the check
201
- * lives here because this helper is the one that composes the `validate` call.
217
+ * lives here because this helper is the one that composes the `pipeIo` call.
202
218
  * The illegal shapes are compile errors for typed callers; this backs them up for
203
219
  * untyped (JS) ones with a typed `InputPreparationError`.
204
220
  */
@@ -222,7 +238,8 @@ function resolveSelector(request) {
222
238
  "catalog id).");
223
239
  }
224
240
  if (given.length > 1) {
225
- throw new InputPreparationError(`Cannot prepare inputs: ${given.join(" and ")} were both given. Supply exactly one method ` +
241
+ throw new InputPreparationError(`Cannot prepare inputs: ${given.join(" and ")} were ${given.length === 2 ? "both" : "all"} ` +
242
+ "given. Supply exactly one method " +
226
243
  "selector — `files`, `method_ref` or `method_id`.");
227
244
  }
228
245
  if (files !== undefined)
@@ -232,113 +249,84 @@ function resolveSelector(request) {
232
249
  return { method_id: methodId };
233
250
  }
234
251
  /**
235
- * Ask `validate` for the signature, whatever the selector, and hand back the
236
- * valid report.
252
+ * Normalize the caller's `pipe_ref` and refuse, before any request, the two
253
+ * spellings preparation cannot honour. Empty is absent, so the route's selection
254
+ * chain decides.
237
255
  *
238
- * `allowSignatures: true` on purpose: preparation needs a pipe's DECLARED inputs,
239
- * and a bundle mid-authoring with an unresolved signature elsewhere must not be
240
- * refused inputs for a pipe whose inputs are declared. Whether the bundle runs is
241
- * the run's verdict, not preparation's. The `is_valid: false` arm still means the
242
- * closure does not load, which is a preparation failure.
256
+ * - A **bare** `pipe_code` is refused because a request names a pipe by its
257
+ * qualified ref. The route will refuse it too once the runner's shared
258
+ * selection enforces that rule; until then it would resolve a bare code across
259
+ * domains, and preparation does not lean on that fallback.
260
+ * - An **`alias->domain.pipe_code`** ref is refused because the alias names a
261
+ * dependency package's pipe, and preparation covers the method's own pipes: the
262
+ * crate routes do not load an address-based dependency at all. The run route
263
+ * takes such a ref; preparation refuses it, and that asymmetry is deliberate.
243
264
  */
244
- async function fetchSignature(client, selector) {
245
- let result;
246
- if ("files" in selector) {
247
- const contents = selector.files.map((file) => file.content);
248
- // `validateFiles`' rule: label every content once any file names a source, so
249
- // the server never sees a length-mismatched `mthds_sources` array.
250
- const hasAnySource = selector.files.some((file) => file.source !== undefined);
251
- const sources = hasAnySource
252
- ? selector.files.map((file, index) => file.source ?? `inline://file-${index + 1}.mthds`)
253
- : undefined;
254
- result = await client.validate(contents, true, sources, undefined, ["input_form"]);
265
+ function normalizePipeRef(raw) {
266
+ const pipeRef = nonEmptyString(raw);
267
+ if (pipeRef === undefined)
268
+ return undefined;
269
+ if (pipeRef.includes("->")) {
270
+ throw new InputPreparationError(`Cannot prepare inputs: \`pipe_ref\` "${pipeRef}" names a dependency package's pipe. ` +
271
+ "Preparation covers the method's own pipes: name one as `domain.pipe_code`.");
255
272
  }
256
- else {
257
- result = await client.validate(selector, true, undefined, undefined, ["input_form"]);
273
+ if (!pipeRef.includes(".")) {
274
+ throw new InputPreparationError(`Cannot prepare inputs: \`pipe_ref\` must be qualified (\`domain.pipe_code\`), got the bare ` +
275
+ `"${pipeRef}".`);
258
276
  }
259
- if (!result.is_valid) {
260
- const first = result.validation_errors[0]?.message ?? result.message;
261
- throw new InputPreparationError(`Cannot prepare inputs: the method signature did not resolve — ${first}`);
262
- }
263
- return result;
277
+ return pipeRef;
264
278
  }
265
279
  /**
266
- * Pick the pipe whose descriptor guides the walk, in the order
267
- * `docs/input-preparation.md` documents: an explicit qualified `pipe_ref`, then
268
- * the report's resolved default — read on the field's PRESENCE, never on its
269
- * truthiness — and, behind an ABSENT field only, the bundle's declared
270
- * `main_pipe` then the single pipe.
280
+ * Ask `POST /v1/pipe-io` for the pipe and its signature, whatever the selector,
281
+ * and hand back the valid report.
271
282
  *
272
- * `default_pipe_ref` answers the very question this function asks: the qualified
273
- * ref of the pipe a selector-less run of THIS request would execute. A stated
274
- * `null` is that answer too — the server determined no entry pipe, and such a run
275
- * is refused by the run route — so reading it as silence and falling through would
276
- * prepare a pipe the run will not execute. Only a field the report does not carry
277
- * at all (a runner predating it) leaves the blueprint and single-pipe arms
278
- * standing. A JSON body cannot carry an own property holding `undefined`, and the
279
- * parsed body is handed through untouched, so strict `=== undefined` is the whole
280
- * absence test.
283
+ * The route runs no dry run and does not refuse a method with pending
284
+ * signatures elsewhere: preparation needs a pipe's DECLARED inputs, and whether
285
+ * the method runs is the run's verdict, not preparation's. The `is_valid: false`
286
+ * arm means the closure does not load, which is a preparation failure. So is a
287
+ * selection the route refuses — an unknown `pipe_ref`, no entry pipe, several —
288
+ * which it answers with a `422` typed by one of {@link PIPE_SELECTION_ERROR_TYPES};
289
+ * every other failure is re-thrown unchanged.
281
290
  */
282
- function selectPipeRef(report, inputForm, requested) {
283
- const refs = Object.keys(inputForm);
284
- const candidates = refs.length > 0 ? refs.join(", ") : "(none — the closure declares no pipes)";
285
- if (requested !== undefined) {
286
- if (!requested.includes(".")) {
287
- throw new InputPreparationError(`Cannot prepare inputs: \`pipe_ref\` must be qualified (\`domain.pipe_code\`), got the bare ` +
288
- `"${requested}". The method declares: ${candidates}.`);
289
- }
290
- if (!(requested in inputForm)) {
291
- throw new InputPreparationError(`Cannot prepare inputs: the method declares no pipe "${requested}". It declares: ${candidates}.`);
292
- }
293
- return requested;
291
+ async function fetchSignature(client, selector, pipeRef) {
292
+ const request = pipeRef === undefined ? { ...selector } : { ...selector, pipe_ref: pipeRef };
293
+ let result;
294
+ try {
295
+ result = await client.pipeIo(request);
294
296
  }
295
- // The resolved default, when the runner serves the field at all (manifest-aware
296
- // for a `method_ref` package, which is why it outranks the blueprint read below).
297
- if (report.default_pipe_ref !== undefined) {
298
- const statedDefault = nonEmptyString(report.default_pipe_ref);
299
- if (statedDefault === undefined) {
300
- // A stated `null` — or anything else that is not a non-empty string — is the
301
- // server's verdict, not a gap: no entry pipe was determined, so a run naming
302
- // no pipe would not resolve one either. Neither fallback stands behind it.
303
- throw new InputPreparationError(`Cannot prepare inputs: the server determined no entry pipe for this method, so a run that ` +
304
- `names no pipe would not resolve one (no \`main_pipe\` is declared, or the package manifest ` +
305
- `names a pipe the closure does not declare or declares in several domains). Pass ` +
306
- `\`pipe_ref\`. It declares: ${candidates}.`);
297
+ catch (error) {
298
+ if (error instanceof ApiResponseError &&
299
+ error.status === 422 &&
300
+ error.errorType !== undefined &&
301
+ PIPE_SELECTION_ERROR_TYPES.has(error.errorType)) {
302
+ const detail = error.serverMessage ?? error.message;
303
+ throw new InputPreparationError(`Cannot prepare inputs: ${detail}`, { cause: error });
307
304
  }
308
- if (!(statedDefault in inputForm)) {
309
- // The default and the descriptor come from one report keyed by one pipe set, so
310
- // a miss is the report contradicting itself — falling through would silently
311
- // prepare a different pipe than the one the run would execute.
312
- throw new InputPreparationError(`Cannot prepare inputs: the validate report names "${statedDefault}" as the default pipe, but ` +
313
- `its \`input_form\` descriptor does not describe it. Pass \`pipe_ref\`. It declares: ${candidates}.`);
314
- }
315
- return statedDefault;
305
+ throw error;
306
+ }
307
+ if (!result.is_valid) {
308
+ const first = result.validation_errors[0]?.message ?? result.message;
309
+ throw new InputPreparationError(`Cannot prepare inputs: the method signature did not resolve — ${first}`);
316
310
  }
317
- // Behind an ABSENT field only. `bundle_blueprint` is opaque transport in this SDK
318
- // on purpose, so read it defensively and fall through rather than trust it.
319
- const blueprintDefault = readBlueprintMainPipeRef(report.bundle_blueprint);
320
- if (blueprintDefault !== undefined && blueprintDefault in inputForm)
321
- return blueprintDefault;
322
- if (refs.length === 1)
323
- return refs[0];
324
- throw new InputPreparationError(`Cannot prepare inputs: the method declares no single default pipe, so \`pipe_ref\` is required. ` +
325
- `It declares: ${candidates}.`);
311
+ return result;
326
312
  }
327
313
  /**
328
- * The bundle blueprint's declared `main_pipe`, qualified by its `domain` when it
329
- * is authored bare. Every read is defensive: the blueprint is typed
330
- * `Record<string, unknown>` because its schema is the runtime's, not this SDK's.
314
+ * The descriptor of the pipe the route selected. The route answers a
315
+ * single-pipe request with `input_form` keyed by exactly the `pipe_ref` it
316
+ * resolved, so a missing ref or a missing key is the answer contradicting itself
317
+ * — preparing any other pipe would silently walk the wrong signature.
331
318
  */
332
- function readBlueprintMainPipeRef(blueprint) {
333
- if (!isPlainObject(blueprint))
334
- return undefined;
335
- const mainPipe = nonEmptyString(blueprint["main_pipe"]);
336
- if (mainPipe === undefined)
337
- return undefined;
338
- if (mainPipe.includes("."))
339
- return mainPipe;
340
- const domain = nonEmptyString(blueprint["domain"]);
341
- return domain === undefined ? undefined : `${domain}.${mainPipe}`;
319
+ function selectedDescriptor(report) {
320
+ const pipeRef = nonEmptyString(report.pipe_ref);
321
+ const inputForm = report.input_form;
322
+ if (pipeRef === undefined || !isPlainObject(inputForm) || !Object.hasOwn(inputForm, pipeRef)) {
323
+ const described = isPlainObject(inputForm)
324
+ ? Object.keys(inputForm).join(", ") || "none"
325
+ : "none";
326
+ throw new InputPreparationError(`Cannot prepare inputs: the pipe I/O answer selected ${pipeRef === undefined ? "no pipe" : `"${pipeRef}"`}, ` +
327
+ `but its \`input_form\` does not describe it (it describes: ${described}).`);
328
+ }
329
+ return inputForm[pipeRef];
342
330
  }
343
331
  /**
344
332
  * Prepare a pipe's inputs: upload local/byte/data-URL assets at the signature's
@@ -346,27 +334,20 @@ function readBlueprintMainPipeRef(blueprint) {
346
334
  * records. HTTP(S) URLs and existing `pipelex-storage://` URIs pass through
347
335
  * unchanged. All failures are raised before any run is created.
348
336
  *
349
- * The signature comes from one `POST /v1/validate` with `views: ["input_form"]`,
350
- * whatever the selector — inline `files`, a `method_ref` the runner resolves, or
351
- * a `method_id` the platform resolves. A closure that does not load, an unknown
352
- * `pipe_ref`, no default pipe, or a report with no descriptor throw
353
- * {@link InputPreparationError}; a no-verdict condition from the route (a
354
- * malformed selector, an unknown or foreign-org id, no package at the address,
355
- * auth, a server fault) surfaces as `ApiResponseError`, unchanged.
337
+ * The pipe and its signature come from one `POST /v1/pipe-io`, whatever the
338
+ * selector — inline `files`, a `method_ref` the runner resolves, or a
339
+ * `method_id` the platform resolves. A bare or alias-qualified `pipe_ref`, a
340
+ * closure that does not load, and a selection the route refuses (an unknown
341
+ * `pipe_ref`, no entry pipe, several) throw {@link InputPreparationError}; any
342
+ * other failure from the route (a malformed selector, an unknown or foreign-org
343
+ * id, no package at the address, auth, a server fault, a deployment that does not
344
+ * serve the route) surfaces as `ApiResponseError`, unchanged.
356
345
  */
357
346
  export async function prepareInputs(client, request) {
358
347
  const selector = resolveSelector(request);
359
- const report = await fetchSignature(client, selector);
360
- const inputForm = report.input_form;
361
- if (inputForm === undefined) {
362
- // Never a silent degrade to "no uploads": without the descriptor there is no
363
- // signature to prepare against.
364
- throw new InputPreparationError("Cannot prepare inputs: the validate report carries no `input_form` descriptor — the signature " +
365
- 'preparation reads. The descriptor rides `views: ["input_form"]` on pipelex-api >= 0.18.0; ' +
366
- "point the client at a runner that serves it.");
367
- }
368
- const pipeRef = selectPipeRef(report, inputForm, nonEmptyString(request.pipe_ref));
369
- const descriptor = inputForm[pipeRef];
348
+ const pipeRef = normalizePipeRef(request.pipe_ref);
349
+ const report = await fetchSignature(client, selector, pipeRef);
350
+ const descriptor = selectedDescriptor(report);
370
351
  const declared = new Map(descriptor.fields.map((field) => [field.name, field]));
371
352
  const ctx = { client, uploads: [], dedup: new Map() };
372
353
  const rewritten = { ...request.inputs };