@pipelex/sdk 0.46.1 → 0.48.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/README.md +20 -9
- package/dist/cli/script.d.ts +1 -1
- package/dist/cli/source.d.ts +8 -7
- package/dist/cli/source.js +11 -5
- package/dist/cli/source.js.map +1 -1
- package/dist/client.d.ts +455 -237
- package/dist/client.js +1067 -522
- package/dist/client.js.map +1 -1
- package/dist/errors.d.ts +49 -19
- package/dist/errors.js +50 -21
- package/dist/errors.js.map +1 -1
- package/dist/hooks/bundle-gather.d.ts +28 -40
- package/dist/hooks/bundle-gather.js +55 -230
- package/dist/hooks/bundle-gather.js.map +1 -1
- package/dist/hooks/claude-mthds-check.d.ts +7 -5
- package/dist/hooks/claude-mthds-check.js +8 -53
- package/dist/hooks/claude-mthds-check.js.map +1 -1
- package/dist/hooks/validate-stage.d.ts +27 -0
- package/dist/hooks/validate-stage.js +57 -0
- package/dist/hooks/validate-stage.js.map +1 -0
- package/dist/index.d.ts +7 -7
- package/dist/index.js +6 -3
- package/dist/index.js.map +1 -1
- package/dist/method-trees.d.ts +344 -0
- package/dist/method-trees.js +876 -0
- package/dist/method-trees.js.map +1 -0
- package/dist/models.d.ts +162 -59
- package/dist/prepare-inputs.d.ts +46 -27
- package/dist/prepare-inputs.js +123 -42
- package/dist/prepare-inputs.js.map +1 -1
- package/dist/product-models.d.ts +97 -75
- package/dist/runs.d.ts +5 -3
- package/dist/runs.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/docs/architecture.md +20 -21
- package/docs/cli.md +1 -1
- package/docs/crate-routes.md +42 -20
- package/docs/errors.md +7 -6
- package/docs/hook-bundle.md +1 -1
- package/docs/input-preparation.md +15 -40
- package/docs/method-trees.md +199 -0
- package/docs/run-results.md +5 -2
- package/package.json +2 -2
- package/dist/bundle-resources.d.ts +0 -62
- package/dist/bundle-resources.js +0 -165
- package/dist/bundle-resources.js.map +0 -1
- package/dist/method-source.d.ts +0 -19
- package/dist/method-source.js +0 -50
- package/dist/method-source.js.map +0 -1
- package/docs/bundle-resources.md +0 -107
package/docs/errors.md
CHANGED
|
@@ -43,7 +43,7 @@ On an error from a request that may have started a run, `retryable` is `false` w
|
|
|
43
43
|
| Class | `errorDomain` | `retryable` | Why |
|
|
44
44
|
|---|---|---|---|
|
|
45
45
|
| `ApiResponseError` | the server's, else the fallback | the server's, else no for a sent `input` or `config` domain, else the fallback | See [below](#a-refused-requests-verdict). |
|
|
46
|
-
| `RequestArgumentError` | `input` | no | The client refused a call's arguments before sending any request: no run source given to `execute` or `start`, run sources or method selectors that exclude each other, a selector rule of `validate`,
|
|
46
|
+
| `RequestArgumentError` | `input` | no | The client refused a call's arguments before sending any request: no run source given to `execute` or `start`, run sources or method selectors that exclude each other, a selector rule of `validate`, the method's tree given in two forms, holding no file, or in a directory the standard's lister refuses (its `MethodTreeError` as the `cause`), a tree past the inline limit of an API that keeps no blob store, a reserved key in `extra`, a `publishMethod` without its token, a `getMethodVersion` version that is not a positive integer, an id a route puts in its path (a method's, an example's, a run's, an organization's or an API key's) that is empty, `.` or `..` or is not a string, since the URL would resolve it into another route's path, or that holds a lone surrogate, which a URL cannot carry, a `createMethodExample` without its `name` or `inputs` or whose `run_id` is given but empty, an `updateMethodExample` naming neither `name` nor `inputs`, a `recordMethodExampleLatestRun` without a run id or a positive integer `input_revision`, a selector `parseMethodSelector` refuses, an artifact selection that is empty or names an unknown artifact, a start's `idempotencyKey` that is not 1 to 255 visible ASCII characters ([below](#a-starts-idempotency-key--idempotencykey)), or a request body JSON cannot carry, such as one holding a cycle, or a `bigint` on a runtime without `JSON.rawJSON` (the body is written with `stringifyExactJson`, which writes a `bigint` as its exact digits; see [exact-integers.md](./exact-integers.md)), with the failure as `cause`. Its options take an optional `verdict`, and the client declares `config` for a base URL that is not host-only, since it typically comes from `PIPELEX_BASE_URL`. That refusal shows the base URL's scheme and host alone and names the other parts it carried (credentials, a path, a query, a fragment) without their text, since those are where a secret travels in a URL; a value that is not an http or https URL is not shown at all. It is a `PipelineRequestError`, as the bare refusal it replaced was, and a refusal the standard's own run-source check raises keeps that original as `cause`. |
|
|
47
47
|
| `ApiUnreachableError` | `config` | yes | The address or the network must be checked, and a later attempt can get through. With the code `ABORT_TIMEOUT`, the SDK's own request timeout, it is `runtime`: the API took the request and did not answer in time. |
|
|
48
48
|
| `PipelineExecuteTimeoutError` | `input` | no | The blocking `execute` cannot outlast the gateway's limit; start the run and poll instead. The run goes on, so its `runMayHaveStarted` is always `true`. |
|
|
49
49
|
| `RunFailedError` | the report's | the report's | See [a failed run](#a-failed-run--runerrorreport). |
|
|
@@ -51,9 +51,10 @@ On an error from a request that may have started a run, `retryable` is `false` w
|
|
|
51
51
|
| `RunLifecycleUnavailableError` | `config` | no | The base URL points at a bare runner without the run lifecycle. |
|
|
52
52
|
| `MissingMainStuffError` | `runtime` | no | The API broke its own contract on a completed run. |
|
|
53
53
|
| `PagingNotTerminatingError` | `runtime` | no | A server minting cursors forever; `pageLimit` is the ceiling `iterateMethods`, `iterateRuns` or `iterateMethodExamples` stopped at. |
|
|
54
|
-
| `InputPreparationError` and `
|
|
54
|
+
| `InputPreparationError` and `InvalidLocalSourceError`, `InvalidInputValueError`, `UploadNotAllowedError`, `MethodLoadError`, `RejectedAssetError` | `input` | no | The asset, the inputs or the method selector must change. The family's base takes an optional `verdict` in its options, so a subclass of your own can declare another; `prepareInputs` declares `runtime` for a pipe I/O answer that breaks the route's contract: no boolean `is_valid`, an `input_form` that does not describe the pipe it selected, or a descriptor that is malformed or names a kind this SDK does not know. |
|
|
55
55
|
| `UnsupportedUploadCapabilityError`, `UploadAuthenticationError` | `config` | no | The deployment or the credential must change. |
|
|
56
56
|
| `UploadTransportError` | the cause's, else by `code` | the cause's, else by `code` | `uploadFile` wraps the `ApiResponseError` or `ApiUnreachableError` the client threw, and takes its verdict, so a wrapped `402` is `config` and not retryable. Without such a cause, which is always the case from `uploadWithGrant`: `timeout`, `storage_timeout` and `conflict` are `runtime` and retryable, `conflict` because storage documents its `409 ConditionalRequestConflict` as retryable and the grant is not spent by it (a spent grant is the `412` of a `RejectedAssetError` with code `grant_used`); `server_error` is `runtime`, and retryable when [the fallback table](#a-refused-requests-verdict) would call its `status` retryable, any `5xx` but the `501` storage answers for a request it does not implement, or when it carries no status; `unexpected` is `runtime`, and retryable when that table would call its `status` retryable, which is the `408` or `429` storage answers when it times out or throttles the request rather than refusing the file, and not retryable for any other status or none; `unreachable` is `config` and retryable; `redirected` is `config` and not retryable; `invalid_grant_url` is `runtime` and not retryable. |
|
|
57
|
+
| `MethodFileTransferError` | `runtime` | by `code` | A method's file could not be moved between this machine and storage: uploaded by `createMethod` or `writeDraft`, so the method was not saved, or by a run, `validate`, `resolve`, `codegen`, `pipeIo` or `prepareInputs` sending a `directory`, `tree` or `contents` by hash, so the request was not sent, or downloaded by `writeMethodTree`. `unreachable` (no answer), `storage_error` (`408`, `429` or a `5xx` other than `501`) and `mismatch` (downloaded bytes that are not the file listed) are retryable; `refused`, any other status, an expired link among them, is not. It carries `operation`, `code`, `sha256`, `path` and `status`, and neither the link, nor storage's error body, nor a cause ([method-trees.md](./method-trees.md#when-a-transfer-fails)). |
|
|
57
58
|
| `ArtifactOperationError`, `ScopeUnavailableError` | `runtime` | no | The family's base also takes an optional `verdict`: the SDK declares `input` where it refuses an argument (a `scope`, a bound, a location) and `config` where the environment refuses the download (a runtime with no filesystem, a directory that cannot be created). |
|
|
58
59
|
| `ArtifactAuthenticationError` | `config` | no | The credential must change. Its `verdict` member is the download's result so far, not the error's. |
|
|
59
60
|
| `ArtifactFetchError` | by `code` | by `code` | `invalid_storage_uri`, `forbidden`, `unsupported_url`, `not_found` and `too_large` are `input` and not retryable; `plain_http_refused` is `config` and not retryable; `redirect_refused` and `store_refused` are `runtime` and not retryable; `timeout` and `network` are `runtime` and retryable; `store_error` is `runtime`, and retryable when [the fallback table](#a-refused-requests-verdict) would call its `status` retryable: a `408`, a `429`, or a `5xx` other than `501`. |
|
|
@@ -259,7 +260,7 @@ So `caller_facing_message` is how a consumer tells the author's copy from the pl
|
|
|
259
260
|
|
|
260
261
|
Every non-2xx answer from a `/v1` route is a problem document, and the SDK throws an `ApiResponseError` whose fields are its members. Each field is `undefined` when the document did not carry it, and a member of the wrong type (a numeric `type`, a `retryable` of `"no"`, a `user_action` without a `detail`) reads as absent rather than as a wrong value. The nested members are checked one level down, exactly as a [stored report's](#a-failed-run--runerrorreport) are: `providerMetadata` and `migration` keep each named field only when it has its type, `validationErrors` keeps only the items that are objects with a string `category` and `message`, and each item of `errors` keeps its `field`, `code` and `detail` only when they are strings; members the SDK does not name are relayed as sent. The verdict is the exception: `errorDomain` and `retryable` are always decided, the server's value when it sent a valid one and otherwise [as the verdict section says](#a-refused-requests-verdict).
|
|
261
262
|
|
|
262
|
-
**An answer the SDK could not read is an `ApiResponseError` too.** A `2xx` whose body is not JSON, such as a gateway's HTML page or an empty body where the route answers one, is no result the SDK can hand back, so every route throws an `ApiResponseError` for it instead of letting the runtime's `SyntaxError` escape. Every body is read by the rules the protocol reads a problem body by (`parseJsonBody` in `mthds/protocol`, as `pipelex-sdk` reads it), so JSON nested deeper than `MAX_BODY_NESTING`, or holding a number past the range of a double, such as `1e999` or an integer that long, which `JSON.parse` reads as `Infinity`, is a body that is not JSON too; a refusal whose body is such JSON carries no hosted member and no `problemDocument`. So is a `2xx` read exactly, such as a run's results or a method example, that holds a number past `Number.MAX_SAFE_INTEGER` on a runtime whose `JSON.parse` hands a reviver no source text, which could only read it rounded; its `cause` is the reader's `ExactJsonUnsupportedError` ([exact-integers.md](./exact-integers.md)). So is a `2xx` whose body is JSON but not the object the route answers — `null`, a number, a string, an array — on every protocol, extension and run-lifecycle route, on `health()`, and on the product routes whose answer the SDK or its caller reads before anything else (`listMethods`, `getMethod`, `createMethod`, `writeDraft`, `renameMethod`, `publishMethod`, `listMethodVersions`, `getMethodVersion`, the method example routes, `listRuns`, `getRunDetail`, `upload`, `requestUploadGrant`, `resolveStorageUrls`), instead of the `TypeError` that reading it would raise. So is an object missing the member those reads depend on: a page of `listMethods`, `listMethodVersions`, `listMethodExamples` or `listRuns` whose `items` is not an array or whose `next_cursor` is neither a string nor `null`, which the iterators could not walk or would follow forever; a publish result whose `outcome` is not `published`, `unchanged` or `refused`, or whose `method` is not an object, and a latest-run result whose `outcome` is not `recorded` or `stale`, or whose `example` is not an object, which a caller branching on `outcome` could not read; a method example, as `createMethodExample`, `getMethodExample` and `updateMethodExample` answer it, as a row of `listMethodExamples` and as a latest-run result's `example`, that misses a field the Python SDK's model of an example requires or holds one of another type, its message naming the field; an `upload` answer with no non-empty string `uri`, which names no stored file;
|
|
263
|
+
**An answer the SDK could not read is an `ApiResponseError` too.** A `2xx` whose body is not JSON, such as a gateway's HTML page or an empty body where the route answers one, is no result the SDK can hand back, so every route throws an `ApiResponseError` for it instead of letting the runtime's `SyntaxError` escape. Every body is read by the rules the protocol reads a problem body by (`parseJsonBody` in `mthds/protocol`, as `pipelex-sdk` reads it), so JSON nested deeper than `MAX_BODY_NESTING`, or holding a number past the range of a double, such as `1e999` or an integer that long, which `JSON.parse` reads as `Infinity`, is a body that is not JSON too; a refusal whose body is such JSON carries no hosted member and no `problemDocument`. So is a `2xx` read exactly, such as a run's results or a method example, that holds a number past `Number.MAX_SAFE_INTEGER` on a runtime whose `JSON.parse` hands a reviver no source text, which could only read it rounded; its `cause` is the reader's `ExactJsonUnsupportedError` ([exact-integers.md](./exact-integers.md)). So is a `2xx` whose body is JSON but not the object the route answers — `null`, a number, a string, an array — on every protocol, extension and run-lifecycle route, on `health()`, and on the product routes whose answer the SDK or its caller reads before anything else (`listMethods`, `getMethod`, `createMethod`, `writeDraft`, `renameMethod`, `publishMethod`, `listMethodVersions`, `getMethodVersion`, the method example routes, `listRuns`, `getRunDetail`, `upload`, `requestUploadGrant`, `resolveStorageUrls`), instead of the `TypeError` that reading it would raise. So is an object missing the member those reads depend on: a page of `listMethods`, `listMethodVersions`, `listMethodExamples` or `listRuns` whose `items` is not an array or whose `next_cursor` is neither a string nor `null`, which the iterators could not walk or would follow forever; a publish result whose `outcome` is not `published`, `unchanged` or `refused`, or whose `method` is not an object, and a latest-run result whose `outcome` is not `recorded` or `stale`, or whose `example` is not an object, which a caller branching on `outcome` could not read; a method example, as `createMethodExample`, `getMethodExample` and `updateMethodExample` answer it, as a row of `listMethodExamples` and as a latest-run result's `example`, that misses a field the Python SDK's model of an example requires or holds one of another type, its message naming the field; an `upload` answer with no non-empty string `uri`, which names no stored file; a stored method or a published version, as `getMethod` and `getMethodVersion` answer it, whose `files` is not a listing of files, each with its string `path`, `url` and `expires_at`, a `sha256` of 64 lower-case hex digits and a non-negative integer `size`; and an answer of `POST /v1/blobs/missing` asking for a blob the tree being saved does not hold: that is server data no change to the call fixes, not an argument the caller passed. Its `status` is the `2xx`, `responseBody` the raw text, `cause` what made the answer unreadable (the parse failure; nothing for a body that parsed to something the route does not answer, nor for JSON the protocol refuses), `problemDocument` and the hosted problem members are `undefined`, `problem` is the standard's reading of such an answer (`client.response_unreadable`, or `client.outcome_unknown` on `start` and `execute`), an answer that carries signed links (a stored method, a published version, the upload targets of `POST /v1/blobs/missing`) has every link redacted in `responseBody`, each `url` member and each link's query replaced by `[redacted]`, and keeps no `cause` at all, since a link is a bearer credential; and its verdict is the fallback's for a status no refusal carries: `runtime`, not retryable. On `start` and `execute` its `runMayHaveStarted` is `true`, since the server accepted the request; that includes a start acknowledgement whose `pipeline_run_id` is not a string, and an `execute` `202` that names no run, which leave the caller a run it could not follow, and an `execute` answer that is not the completed run: one whose `pipeline_run_id` is not a string, or whose `pipe_output` carries no string `pipeline_run_id` or no `working_memory` with `root` and `aliases` objects, the members the Python SDK's model of the answer requires. A product route that answers a `2xx` with no body at all, such as `revokePipelexApiKey`, still resolves to `undefined`, and the other product routes hand their decoded body back as it came.
|
|
263
264
|
|
|
264
265
|
| Field | Wire member | Meaning |
|
|
265
266
|
|---|---|---|
|
|
@@ -326,15 +327,15 @@ A method refused for what it says, rather than for how its run went, comes back
|
|
|
326
327
|
| `message` | What is wrong, in words. |
|
|
327
328
|
| `error_type` | The error's code, such as `unknown_model`. |
|
|
328
329
|
| `pipe_code`, `concept_code`, `domain_code` | The pipe, concept and domain the error is in. |
|
|
329
|
-
| `source`, `field_path`, `field_name` | The file the error is in (
|
|
330
|
+
| `source`, `field_path`, `field_name` | The file the error is in (its tree path in the tree the request sent), and the field, as a path such as `pipe.summarize.model` and by name. |
|
|
330
331
|
| `missing_concept_code`, `missing_pipe_code`, `declared_concepts` | The concept or pipe a reference names that the bundle does not declare, and the concepts the domain does declare. |
|
|
331
332
|
| `variable_names` | The input variables an input error concerns. |
|
|
332
333
|
| `model_reference`, `model_type`, `suggestions` | On an `unknown_model` item: the model reference exactly as the author wrote it, the kind of model the field takes (`llm`, `img_gen`, …), and the close matches, each spelled as a reference the field accepts. |
|
|
333
|
-
| `template_file`, `template_location` | On a `template_check_warning` item, which rides the valid verdict's `warnings[]`: the template file as the `PipeDocGen` step names it, `null` for a step of the server's own library, and the place in it the template check's finding names, in the engine's own words, `null` when it names none ([`
|
|
334
|
+
| `template_file`, `template_location` | On a `template_check_warning` item, which rides the valid verdict's `warnings[]`: the template file as the `PipeDocGen` step names it, `null` for a step of the server's own library, and the place in it the template check's finding names, in the engine's own words, `null` when it names none ([`method-trees.md`](./method-trees.md#the-template-checks-warnings)). |
|
|
334
335
|
| `suggested_fix` | The runner's deterministic correction, when it has one: a `description` in words, `ops` that patch the `.mthds` document by table and key rather than by text, `safety` (`safe` to apply without asking, or `unsafe`) and the `source` file the ops apply to. An unknown model with exactly one close match carries a `rename-model` fix that remaps the reference to it. |
|
|
335
336
|
|
|
336
337
|
```ts
|
|
337
|
-
const report = await client.validate(
|
|
338
|
+
const report = await client.validate({ directory: "methods/receipts" });
|
|
338
339
|
if (!report.is_valid) {
|
|
339
340
|
for (const item of report.validation_errors) {
|
|
340
341
|
console.error(`${item.source ?? "?"} ${item.pipe_code ?? ""}: ${item.message}`);
|
package/docs/hook-bundle.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
Each file goes through three stages: a local lint that blocks on any diagnostic, a local format that rewrites the file in place when its layout changes, and a validation by the hosted API (`POST /v1/validate`) that blocks on an invalid verdict and adds a non-blocking note about pending pipe signatures. The hook fails open: when the engine cannot load, when `PIPELEX_API_KEY` is unset or the API cannot be reached, the stage that could not run passes silently and the others stand. When several files are checked, one block among them wins and the notes are joined, and each file is read when its turn comes, so an edit made to it while an earlier file was being validated is checked rather than overwritten. The hook always exits 0, since its verdict is on stdout.
|
|
8
8
|
|
|
9
|
-
The validation sends the
|
|
9
|
+
The validation sends the method's tree around the file, the tree a run of the method would send: the edited file's directory listed by the `mthds` package's lister under the MTHDS standard's membership rule, so it is rooted at the nearest `METHODS.toml` at or above that directory, else at the directory itself, and holds every file the rule keeps — every bundle of the method, and every file a step reads, such as the template a `PipeDocGen` step names in `template_file`, under its tree path — while hidden entries, `node_modules`, `__pycache__`, what a `.gitignore` keeps out and a run's inputs file never travel ([`method-trees.md`](./method-trees.md#the-four-forms-of-a-tree)). A method that fills a template thus validates as it would load from disk, its template checked by the dry run, and a step whose template file the tree does not hold makes the verdict say so, which blocks until the file is written. The client sends the tree inline when small and by hash otherwise, uploading the files the API lacks. The whole validation runs under one ten-second deadline, the version handshake, the blob route, the uploads and the validation request together, and the deadline stops whichever of them is under way when it runs out. Validation passes silently, as when the API cannot be reached, when that deadline runs out, when the tree holds a symbolic link or a directory the lister cannot read, when the edited file is not in its own tree because the rule keeps it out, when a file of the tree fails to upload, and past the hook's own caps: 100 files, 5 MiB for one of them or 12 MiB in all, each checked while the tree is listed, before any file is read.
|
|
10
10
|
|
|
11
11
|
## Which file it checks
|
|
12
12
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
> **Status: implemented** (`src/upload.ts`, `src/upload-grant.ts`, `src/prepare-inputs.ts`). This document records the contract, and is the public account of it. The raw `upload()` primitive described in [architecture.md](./architecture.md) is the wire call `uploadFile` and `prepareInputs` build on.
|
|
4
4
|
>
|
|
5
|
-
> **Current scope.** `prepareInputs` names the method the same three ways every other method-taking operation does —
|
|
5
|
+
> **Current scope.** `prepareInputs` names the method the same three ways every other method-taking operation does — the method's tree, a `method_ref` address, or a stored `method_id` — exactly one per call, all three resolved server-side by the one `POST /v1/pipe-io` call it composes. One piece remains deliberately deferred and additive (it does not change this contract): the opt-in ingest of `http(s)` URLs into storage — for now an `http(s)` URL at a file position always passes through unchanged.
|
|
6
6
|
|
|
7
7
|
## Why this exists
|
|
8
8
|
|
|
@@ -93,7 +93,7 @@ It sends the file as the raw body of a `PUT` to `grant.url`, with `grant.headers
|
|
|
93
93
|
### `prepareInputs` — signature-driven input preparation
|
|
94
94
|
|
|
95
95
|
```
|
|
96
|
-
client.prepareInputs({ files,
|
|
96
|
+
client.prepareInputs({ directory | files | tree | contents, dependencies?, pipe_ref?, inputs }) → PreparedInputs
|
|
97
97
|
client.prepareInputs({ method_ref, pipe_ref?, inputs }) → PreparedInputs
|
|
98
98
|
client.prepareInputs({ method_id, pipe_ref?, inputs }) → PreparedInputs
|
|
99
99
|
```
|
|
@@ -105,33 +105,33 @@ Takes the **method** as exactly one of three selectors, the optional target **pi
|
|
|
105
105
|
|
|
106
106
|
The prepared `inputs` are passed to the existing run lifecycle unchanged.
|
|
107
107
|
|
|
108
|
-
**Stopping it.** An optional `signal` stops the preparation between its steps: once it has aborted, neither the pipe I/O request nor any upload starts, and `prepareInputs` throws the abort, as the run wait does. It is read again once each file's bytes are in hand, right before its upload request, so a file whose read outlasted the caller's patience is not sent; `uploadFile` takes the same `signal` in its options for that check. A request already sent runs to its end, so an upload under way finishes, but the file after it is not sent. The signal is not sent to the server.
|
|
108
|
+
**Stopping it.** An optional `signal` stops the preparation between its steps: once it has aborted, neither the pipe I/O request nor any upload starts, and `prepareInputs` throws the abort, as the run wait does. It is read again once each file's bytes are in hand, right before its upload request, so a file whose read outlasted the caller's patience is not sent; `uploadFile` takes the same `signal` in its options for that check. A request already sent runs to its end, so an upload under way finishes, but the file after it is not sent. A `directory`, `tree` or `contents` adds steps before the pipe I/O request — the version handshake, the directory's listing, and for a tree sent by hash the blob route and each read and upload of a missing blob — and the signal is read before each of them too and handed to each, through the route's `CrateRouteOptions`, so an abort stops the one under way, a blob's upload included: one during the handshake stops it and leaves the blob route, the uploads and the pipe I/O request unasked. The signal is not sent to the server.
|
|
109
109
|
|
|
110
110
|
#### The three selectors
|
|
111
111
|
|
|
112
|
-
Exactly one per call. The type pins the other two to `never`, so a second selector is a compile error; an untyped caller gets an `InputPreparationError` naming the three forms. **Empty is absent** — `files:
|
|
112
|
+
Exactly one per call. The type pins the other two to `never`, so a second selector is a compile error; an untyped caller gets an `InputPreparationError` naming the three forms. **Empty is absent** — `files: {}`, `contents: {}`, `directory: ""`, `method_ref: ""`, `method_id: " "` — mirroring the run options' rule, so an empty selector may sit beside a real one without tripping the XOR. A `files` that is not a map, such as the retired list of `{ content, source }` items, is an `InputPreparationError` rather than an absent selector, and so, before any request, is a `directory` that is not a string, a `contents` that is not a map of text or bytes, a `dependencies` that is not a map of file maps, and a `tree` that is not a listed tree (its `files`, its `digest`, a `readFile`) or holds no file.
|
|
113
113
|
|
|
114
114
|
| Selector | What it is | Who resolves it |
|
|
115
115
|
| --- | --- | --- |
|
|
116
|
-
| `files`
|
|
116
|
+
| the tree | the method's tree, in exactly one of the forms the crate routes take: `files`, the protocol's map from tree path to file entry; `directory`, the method's directory on disk, listed by the `mthds` package's lister (Node only); `tree`, a listed `MethodTreeSource`; or `contents`, each file's bytes or text by tree path ([crate-routes.md](./crate-routes.md#the-shared-envelope)) | nobody — sent with the request, a `directory`, `tree` or `contents` inline when small and by hash otherwise |
|
|
117
117
|
| `method_ref` | a published method's address, `github.com/<owner>/<repo>[/<selector>][@<tag>]` | the runner, server-side (pipelex-api >= 0.21.0 fetches the repository at the tag) |
|
|
118
|
-
| `method_id` | a stored method's catalog id (`mt_…`, `mt_…@<n>` or `mt_…@draft`) | the hosted platform, which injects the
|
|
118
|
+
| `method_id` | a stored method's catalog id (`mt_…`, `mt_…@<n>` or `mt_…@draft`) | the hosted platform, which injects the tree of the version it names before the runner sees the request |
|
|
119
119
|
|
|
120
|
-
|
|
120
|
+
The tree carries every file the method's steps read, such as a `PipeDocGen` step's `template_file`, at the tree path the step names ([method-trees.md](./method-trees.md#the-files-a-step-reads)), and it may bring `dependencies`, the method packages it calls by address, each a `files` map keyed by its address. They ride the one pipe I/O request, which loads the closure and fails to load it, as a `MethodLoadError`, when a step's template file is not in the tree or a call reaches a package nothing shipped. A published or stored method's packages are its own, so `dependencies` beside `method_ref` or `method_id` is an `InputPreparationError` before any request, and so is the tree given in two forms.
|
|
121
121
|
|
|
122
|
-
Nothing is expanded client-side: `method_id` here is a **pass-through**, the same rule every other id-taking operation follows
|
|
122
|
+
Nothing is expanded client-side: `method_id` here is a **pass-through**, the same rule every other id-taking operation follows (see "[A method's files in hand](#a-methods-files-in-hand)").
|
|
123
123
|
|
|
124
124
|
#### Where the signature comes from
|
|
125
125
|
|
|
126
126
|
One `POST /v1/pipe-io` per call, whatever the selector, through the client's own `pipeIo` (see [crate-routes.md](./crate-routes.md#pipeio--a-methods-io-artifacts-without-a-validation)):
|
|
127
127
|
|
|
128
128
|
```
|
|
129
|
-
pipeIo({ <files [+
|
|
129
|
+
pipeIo({ <directory | files | tree | contents [+ dependencies] | method_ref | method_id>, pipe_ref? })
|
|
130
130
|
```
|
|
131
131
|
|
|
132
132
|
The route resolves the closure, **selects the pipe**, and answers with that pipe's **input-form descriptor** under the qualified ref it resolved. It runs no dry run, so preparing inputs costs one static load rather than a validation of the whole method. It also refuses nothing for a pending signature elsewhere in the method: preparation needs a pipe's *declared* inputs, and whether the method runs is the run's verdict, not preparation's. An `is_valid: false` verdict still means the closure does not load, which is a preparation failure.
|
|
133
133
|
|
|
134
|
-
A `
|
|
134
|
+
A `directory`, `tree` or `contents` too large to send inline is uploaded first, by hash, and a blob that fails to upload throws `MethodFileTransferError` before the route is asked. A `method_ref` makes the server clone a repository first, so the call gets the crate routes' fetch-sized budget. On the hosted API the gateway caps a request at about 30 seconds whatever the client allows, so a cold clone can answer a `502` `ApiResponseError`; a retry clears it once the runner has cached the clone.
|
|
135
135
|
|
|
136
136
|
`prepareInputs` needs an API that serves `POST /v1/pipe-io`. Against one that does not, the route's `404` (a runner) or `403` (a hosted origin whose gateway does not list it yet) propagates as an `ApiResponseError`. It also needs the route's pipe-selection refusals typed, as step 3 of "Pipe selection" below describes, which `pipelex-api` does from v0.33.1: v0.33.0, the release that added the route, typed them as the generic `ValidationError`, so against it a refused selection reaches the caller as that `ApiResponseError` rather than an `InputPreparationError`.
|
|
137
137
|
|
|
@@ -139,8 +139,8 @@ A `method_ref` makes the server clone a repository first, so the call gets the c
|
|
|
139
139
|
|
|
140
140
|
The route selects the pipe, and the ref it answers with is the one prepared — the helper derives no default of its own:
|
|
141
141
|
|
|
142
|
-
1. **`pipe_ref` when given.** Qualified-only: `domain.pipe_code`, normalized (trimmed, and blank is absent) and checked **before any request**. A bare code is refused with an `InputPreparationError` asking for the qualified form, since a request names a pipe by its qualified ref and search is a run-route affordance this helper never grows. An `alias->domain.pipe_code` ref is refused for a different reason: the alias names a dependency package's pipe, and preparation covers the method's *own* pipes
|
|
143
|
-
2. **Otherwise the method's own entry pipe**, chosen by the route:
|
|
142
|
+
1. **`pipe_ref` when given.** Qualified-only: `domain.pipe_code`, normalized (trimmed, and blank is absent) and checked **before any request**. A bare code is refused with an `InputPreparationError` asking for the qualified form, since a request names a pipe by its qualified ref and search is a run-route affordance this helper never grows. An `alias->domain.pipe_code` ref is refused for a different reason: the alias names a dependency package's pipe, and preparation covers the method's *own* pipes, whose input form the pipe I/O route describes. The run route takes such a ref; preparation cannot.
|
|
143
|
+
2. **Otherwise the method's own entry pipe**, chosen by the route: the `main_pipe` of the `METHODS.toml` at the tree's root, whether the tree was sent or fetched with a package, else the closure's `main_pipe` when exactly one domain declares one. A method that names its entry pipe in `METHODS.toml` alone — the published `github.com/Pipelex/methods/documents` does — therefore prepares with no `pipe_ref`.
|
|
144
144
|
3. **A selection the route refuses** — a `pipe_ref` that names no pipe of the method, a method with no entry pipe, one whose domains declare several, a manifest `main_pipe` the closure does not declare or declares in several domains — comes back as a `422` whose `errorType` is `EntryPipeNotFoundError` or `EntryPipeAmbiguousError`. The helper turns exactly those into an `InputPreparationError` carrying the server's detail, which names the candidates where there are some, with the `ApiResponseError` as its `cause`. The run route refuses a selector-less run in the same cases, so preparation never succeeds for a pipe the run would not take.
|
|
145
145
|
|
|
146
146
|
There is no single-pipe fallback: a method that declares one pipe and no `main_pipe` is refused a selector-less run, and a selector-less preparation with it.
|
|
@@ -160,35 +160,11 @@ client.prepareInputs({ method_id, inputs, allowUploads: false }) → PreparedInp
|
|
|
160
160
|
|
|
161
161
|
The error names the input as `inputName` and where inside it the value sits as `inputPath`: the input's name for a top-level file, then `.field` for each declared field and `[index]` for each list element (`dossier.attachments[1]`). An explicit envelope adds nothing to the path, since its `content` is the input's value. `valueKind` says what the value is, `local_path`, `data_url` or `bytes`, so a consumer can tell its caller what to send instead; the value itself is never in the message or on the error, since it is the caller's data. The option is `true` when omitted, must be a boolean when given — anything else is refused with an `InputPreparationError` before any request, since reading a stray `"false"` as permission would upload exactly what the caller meant to keep — and is not sent to the server. The option covers `prepareInputs` alone: `uploadFile` is an upload by definition.
|
|
162
162
|
|
|
163
|
-
##
|
|
163
|
+
## A method's files in hand
|
|
164
164
|
|
|
165
|
-
|
|
166
|
-
client.getMethodClosure(methodId) → MthdsFileItem[]
|
|
167
|
-
```
|
|
168
|
-
|
|
169
|
-
`getMethodClosure` is the public **local expansion utility** — a client-side semantic layer over `getMethod` (the platform has no route that returns a parsed closure). It fetches the method, parses its **draft's** polymorphic `mthds` source with [`methodSourceToContents`](#methodsourcetocontents--the-canonical-source-parser), and labels each resulting file with the `method_id` as its `source` provenance.
|
|
170
|
-
|
|
171
|
-
**It reads the draft, which is not what a bare id runs.** `GET /v1/methods/{id}` answers the method with its draft, whatever the publish state, while a run or a tooling call by a bare `method_id` resolves to the latest *published* version, which the draft may be ahead of. For the closure of a version, read it with `getMethodVersion(methodId, n)` and parse its `mthds` with `methodSourceToContents`. Like `getMethod`, it takes a bare id: a caller holding `mt_…@3` strips the suffix with `parseMethodSelector` first.
|
|
172
|
-
|
|
173
|
-
Reach for it whenever you want the files **in hand** — to edit, to diff, or to send to a bare runner, which has no catalog to resolve an id against:
|
|
174
|
-
|
|
175
|
-
```
|
|
176
|
-
runner.resolve({ files: await hosted.getMethodClosure("mt_…") })
|
|
177
|
-
```
|
|
178
|
-
|
|
179
|
-
Every method-taking operation — `execute` / `start`, `validate` / `resolve` / `codegen` / `pipeIo`, and `prepareInputs` — takes a `method_id` **natively** on the hosted API, as a server pass-through, its version suffix included (`mt_…`, `mt_…@<n>`, `mt_…@draft`): the platform resolves it and injects the source of the version it names before the runner sees the request (see [architecture.md → hosted run extensions](./architecture.md#hosted-run-extensions-method_id)). That is the uniform rule — *an id option is always a server pass-through, and any client-side expansion is the caller's own explicit call*.
|
|
180
|
-
|
|
181
|
-
- **Requires an API key.** The methods catalog is org-scoped to the key's org, so resolution only works with an authenticated Pipelex-product key.
|
|
182
|
-
- **Unknown or foreign-org id → the `getMethod` `404`** (`ApiResponseError`, `code: "not_found"`), propagated unchanged. An id from another org is indistinguishable from a nonexistent one — both 404.
|
|
183
|
-
- **A real, in-org method whose source parses to nothing → `EmptyMethodSourceError`** — the row exists but has no runnable MTHDS source yet. This is a distinct outcome from the 404 (see "[Error and capability behavior](#error-and-capability-behavior)"), and `getMethodClosure` is now its only raiser: `prepareInputs` passes the id to the server, which answers a sourceless method with a `422`.
|
|
184
|
-
|
|
185
|
-
### `methodSourceToContents` — the canonical source parser
|
|
186
|
-
|
|
187
|
-
```
|
|
188
|
-
methodSourceToContents(mthds: string) → string[]
|
|
189
|
-
```
|
|
165
|
+
Every method-taking operation — `execute` / `start`, `validate` / `resolve` / `codegen` / `pipeIo`, and `prepareInputs` — takes a `method_id` **natively** on the hosted API, as a server pass-through, its version suffix included (`mt_…`, `mt_…@<n>`, `mt_…@draft`): the platform resolves it and injects the tree of the version it names before the runner sees the request (see [architecture.md → hosted run extensions](./architecture.md#hosted-run-extensions-method_id)). That is the uniform rule — *an id option is always a server pass-through, and any client-side expansion is the caller's own explicit call*.
|
|
190
166
|
|
|
191
|
-
A
|
|
167
|
+
A caller that wants a method's files in hand, to edit, to diff, or to send to a bare runner, which has no catalog to resolve an id against, reads them explicitly. `getMethod(id)` lists the draft's tree and `getMethodVersion(id, n)` a published version's, each file with a short-lived link to its bytes, and `writeMethodTree(files, { destination })` writes that listing to disk, checked against the listing ([`method-trees.md`](./method-trees.md)). The draft is not what a bare id runs: a run or a tooling call by a bare `method_id` resolves to the latest published version, which the draft may be ahead of.
|
|
192
168
|
|
|
193
169
|
## Compact or explicit-envelope inputs
|
|
194
170
|
|
|
@@ -248,7 +224,6 @@ Upload is a **hosted Pipelex-product capability**, even though the SDK can be po
|
|
|
248
224
|
The contract distinguishes these semantic outcomes, each a typed subclass of `InputPreparationError` (catch the base to handle any preparation failure, a subclass to branch on category):
|
|
249
225
|
|
|
250
226
|
- **an unresolvable signature** (`InputPreparationError`) — a bare or alias-qualified `pipe_ref`, refused before any request; a closure that does not load (`MethodLoadError`, below); or a pipe selection the route refused (its `422` typed `EntryPipeNotFoundError` or `EntryPipeAmbiguousError`, carrying the server's detail). Any other failure from `/v1/pipe-io` — a malformed selector, an over-limit file, an unknown or foreign-org `method_id`, no package at the address or one with no `.mthds` file, auth, a server fault, a deployment that does not serve the route — stays an `ApiResponseError` and propagates unchanged;
|
|
251
|
-
- **empty method source** (`EmptyMethodSourceError`, carries `methodId`) — `getMethodClosure` found the stored method but its `mthds` source parses to nothing (the row exists, no runnable source yet). Distinct from the `getMethod` `404` for an unknown/foreign id, which stays an `ApiResponseError`. `prepareInputs` never raises it: it hands the id to the server, which answers a sourceless method with a `422`;
|
|
252
227
|
- **a method that does not load** (`MethodLoadError`) — the pipe I/O answer's `is_valid: false` arm. Its message names the first validation item's message, else the answer's own `message`; `validationErrors` holds the answer's items, each kept when it carries a string `category` and `message`, and `serverMessage` the answer's own `message` when it is a string, so a consumer lists every item rather than reading the first from a sentence. The method must change, not the inputs;
|
|
253
228
|
- **invalid local source** (`InvalidLocalSourceError`) — missing, unreadable, or a path string outside Node;
|
|
254
229
|
- **invalid input value** (`InvalidInputValueError`, carries `inputName` and `inputPath`) — a value at a file input that cannot be turned into a file to upload: a `data:` URL that does not decode (no comma, bad base64, bad percent-encoding), or a value of a type no file input takes, such as a number or an object that is neither bytes nor `{url}` content. It is raised while the inputs are read, before anything is uploaded. It names the input and the path inside it as `UploadNotAllowedError` does (`Input "dossier.attachments[1]" holds a number, which no file input takes: …`), describes the value by its kind and never quotes it, and lists the forms a file input takes in the mode it ran in. With `InvalidLocalSourceError`, it is how a consumer tells the inputs' own mistakes from a method that does not load, though all three are `InputPreparationError`s with the same verdict;
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# A method's tree
|
|
2
|
+
|
|
3
|
+
A method is a **method tree**: the files it is made of, each at its tree path — its bundles, its manifest when it has one, and every file a step reads or that travels with the method, such as a `PipeDocGen` step's template. A tree is named by its **tree digest**, `sha256:` and the SHA-256 of its listing, so two methods holding the same files hold the same digest. Both are defined by the MTHDS standard (`mthds/docs/spec/method-tree.md`), and on the wire a tree is the protocol's `files` map, each key a tree path and each value the file's text, `{ base64 }` for bytes that are not text, or `{ sha256, size }` for bytes the receiver already holds.
|
|
4
|
+
|
|
5
|
+
Every call that takes a method takes its tree the same way: the runs (`execute`, `start`, `startAndWaitForResult`), `validate`, the crate routes (`resolve`, `codegen`, `pipeIo`), `prepareInputs`, and the saves (`createMethod`, `writeDraft`). A saved method holds one too: the draft holds one, and each published version holds the one it was published from. The platform keeps each file once per organization, as a blob named by its SHA-256, so a request sends only the files the organization does not hold yet, and a read lists the files with a short-lived link to each one's bytes.
|
|
6
|
+
|
|
7
|
+
## The four forms of a tree
|
|
8
|
+
|
|
9
|
+
A call takes the tree in exactly one of four forms (`MethodTreeForms`):
|
|
10
|
+
|
|
11
|
+
| Form | What it is | How it is sent |
|
|
12
|
+
|---|---|---|
|
|
13
|
+
| `directory` | The method's directory on disk. **Node only.** | Listed, then inline when small and by hash otherwise (below). |
|
|
14
|
+
| `contents` | Each file's bytes (`Uint8Array`) or its text, by tree path, a map held in memory. | Inline when small, by hash otherwise. |
|
|
15
|
+
| `tree` | A `MethodTreeSource`: a tree's `files` (each with its `path`, `size` and `sha256`), its `digest`, and `readFile(path)` giving each file's bytes. The `MethodTree` the `mthds` package's `listMethodTree` returns is one. | Inline when small, by hash otherwise. |
|
|
16
|
+
| `files` | The protocol's map of file entries (`MethodFiles` from `mthds/protocol`). | As given. |
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
// A run of the method in a directory: the client lists it, and sends what the API lacks.
|
|
20
|
+
const results = await client.startAndWaitForResult({
|
|
21
|
+
directory: "methods/receipts",
|
|
22
|
+
inputs: { receipt: "https://example.com/receipt.png" },
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// The same method validated from memory, its template among its files.
|
|
26
|
+
const verdict = await client.validate({
|
|
27
|
+
contents: {
|
|
28
|
+
"receipts.mthds": source,
|
|
29
|
+
"templates/receipt.docx": await readFile("methods/receipts/templates/receipt.docx"),
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
**A directory is listed by the standard's lister.** The client loads the `mthds` package's `listMethodTree` when a call gives a `directory`, and lists it under the MTHDS standard's membership rule and Manifest Discovery: the tree is rooted at the nearest `METHODS.toml` at or above the directory, else at the directory itself, and holds every file the rule keeps. Hidden entries, `node_modules`, `__pycache__`, what a `.gitignore` or the manifest's `exclude` keeps out, and **a run's inputs file** (`inputs.json`, `inputs.toml`) never travel with the method. A tree holding a symbolic link or a special file is refused rather than followed. The directory is listed under the limits the deployment announces (below), so a tree the deployment would refuse for its size is refused while it is listed, before any file is hashed. Every refusal is a `RequestArgumentError` naming the call and the directory, whose `cause` is `mthds`'s `MethodTreeError`, with its `code` (`not_found`, `symbolic_link`, `limit_exceeded`, `empty_tree` and the rest). Listing needs Node: a browser gives `contents`, `tree` or `files`.
|
|
35
|
+
|
|
36
|
+
**A map in memory.** A text in `contents` is encoded in UTF-8, and a text a file cannot hold, one carrying a lone surrogate, is refused, as is a value that is neither a string nor a `Uint8Array`. The keys are sent as they are, and the platform refuses one that is not a tree path with the `422` `invalid_method_files` naming it.
|
|
37
|
+
|
|
38
|
+
**One form, and never an empty tree.** Two forms at once are refused with a `RequestArgumentError` before anything is read or sent. A `files` map, `contents` or a `tree` holding no file is refused too on the run routes and on `validate`, as the standard refuses it: a tree holds at least one file. On the crate routes and in `prepareInputs`, an empty map is absent instead, so it never reaches the wire as a second selector, and in a save `files: null` empties the draft. An empty `directory` string is absent everywhere.
|
|
39
|
+
|
|
40
|
+
## How a tree travels
|
|
41
|
+
|
|
42
|
+
A tree given as a `directory`, `contents` or `tree` is sent by the rule the `mthds` package's `MthdsApiClient` follows, so the tree a save sends is the tree a run would send. The client reads the API's version handshake first (`GET /v1/version`) and keeps its answer for five minutes, so every call in that time reads the same answer and a deployment that changes is read afresh soon after ([below](#the-deployments-limits)):
|
|
43
|
+
|
|
44
|
+
- **When the API keeps a blob store** (`blob_store: true`), a tree whose inline map weighs no more than the inline threshold travels **inline**, with no further round trip. The map is weighed as the request spends it, its JSON with no whitespace, in UTF-8, and the threshold is the lower of `INLINE_TREE_MAX_BYTES` (256 KiB) and the `max_inline_bytes` the handshake announces. Inline, a file whose bytes are UTF-8 travels as its text and any other as `{ base64 }`, so a binary file weighs its Base64. A heavier tree travels **by hash**: the client asks `POST /v1/blobs/missing` which files the receiver lacks, naming the tree's digest, uploads each one it lacks once, at most eight at a time, straight to the upload target the platform names, under the client's request time limit (`requestTimeoutMs`, twenty minutes unless the client was built with another), and sends every file as `{ sha256, size }`. A file uploaded once is never uploaded again, by this method or any other of the organization. A `404` from the blob route says the kept handshake is stale, the API having lost its blob store since it was read: the client forgets the handshake and prepares the tree once more on a fresh one.
|
|
45
|
+
- **When the API keeps no blob store**, which a bare runner usually does, the whole tree travels inline whatever its weight, held to the `max_inline_bytes` the handshake announces: a heavier tree is refused with a `RequestArgumentError` before anything is sent, since the API would refuse it. The files' listed sizes are checked first, since the inline map weighs at least that much, so a tree whose files alone pass the limit is refused before any of them is read; the message says which was measured, the files alone or the inline map, with its weight and the limit.
|
|
46
|
+
|
|
47
|
+
A `files` map is sent as given, with no handshake. A run's `start` carrying a tree, a run that names a `method_ref`, and a start whose `extra` carries a method bundle (`mthds_contents` or `bundle_b64`, as the `pipelex-sdk` command sends one) get the client's request ceiling rather than the 30-second start budget, since the body can be large. On the crate routes, a request whose `files` carry more than `INLINE_TREE_MAX_BYTES` inline, as a tree sent to an API keeping no blob store or a `files` map given as is may, gets the request ceiling too rather than the 30-second budget of a static route ([crate-routes.md](./crate-routes.md)). A save carrying a tree (`createMethod`, `writeDraft`), inline or by hash, gets the request ceiling as well, rather than the 30-second budget of a save sending none, since a tree sent inline to an API keeping no blob store can be large and its whole upload is charged against the budget.
|
|
48
|
+
|
|
49
|
+
**A refusal of the tree's size is sent once more.** When the API refuses a tree the client prepared, a `directory`, `contents` or `tree`, as past one of its limits (a `422` coded `method_tree_too_large`, or `method.package_invalid` naming a limit, or of type `MethodTreeTooLarge`), the handshake that announced the limits may be stale: the client forgets it, prepares the tree again on a fresh one and sends the request once more, as `mthds`'s client does. A `files` map given as is was not prepared from the handshake, so it is not sent again, and neither is a `start` under an idempotency key, since the platform binds the key to the refusal it stores.
|
|
50
|
+
|
|
51
|
+
**Stopping a call.** A call that takes a `signal` (`validate`, `startAndWaitForResult`, the crate routes, `prepareInputs`) reads it before each step a `directory`, `contents` or `tree` costs and hands it to the step's request, so an abort stops the version handshake, the directory's listing, the blob route, a file's read and an upload already under way, and nothing further starts.
|
|
52
|
+
|
|
53
|
+
A `MethodTree` reads a file again whenever its bytes are sent, inline or as an upload, and checks it against the hash its listing recorded, so a file changed since the listing is refused by `mthds` rather than sent as another tree than the one listed. Apart from listing a directory, sending a tree reads no Node module: hashing is Web Crypto's, and Base64 is written by the SDK.
|
|
54
|
+
|
|
55
|
+
**What is uploaded is what the listing names.** Before each upload the client hashes the bytes a tree's `readFile` gives and refuses them, with a `RequestArgumentError`, unless they have the size and the SHA-256 the tree lists for them: a blob is stored under its hash for the whole organization, so a hand-built `MethodTreeSource` whose `readFile` disagrees with its listing never stores other bytes under that hash. An answer of `POST /v1/blobs/missing` asking for a blob the tree does not hold is the platform's fault, not the caller's: it is an `ApiResponseError` of an answer the SDK cannot read, verdict `runtime` and not retryable, thrown before any upload. An upload that fails throws `MethodFileTransferError` with `operation` `upload`, and the request is not sent or the method not saved ([below](#when-a-transfer-fails)).
|
|
56
|
+
|
|
57
|
+
**A blob that went missing.** A request whose hash entries name a blob the API does not hold is refused with a `422` `missing_blobs` listing them all, which also releases a start's idempotency key. A request built from a `directory`, `contents` or `tree` uploads every blob it names first, so it meets this only if a blob was removed between the upload and the request. The client answers it as `mthds`'s client does: a `422` problem coded `request.blobs_missing`, whose `params.missing_blobs` names blobs the tree holds, has those blobs asked of the blob route again, uploaded and the request sent once more, while the platform's `missing_blobs` code is not read as one and is thrown, so sending the same call again asks the blob route afresh and uploads what is missing.
|
|
58
|
+
|
|
59
|
+
On the crate routes, `read_scope` is the scope the blob route is asked under, so the blobs are uploaded into the store the request's hash entries are read from; the hosted platform reads the caller's organization whatever the request says ([crate-routes.md](./crate-routes.md#the-shared-envelope)).
|
|
60
|
+
|
|
61
|
+
## The files a step reads
|
|
62
|
+
|
|
63
|
+
A file a step reads, such as a `PipeDocGen` step's `template_file` (the Word, Excel or PowerPoint template it fills, or the HTML template it prints as a PDF), is a file of the tree like any other, at the tree path the step names from the root. A `directory` sends it with the bundles; a `contents` or `files` map carries it under its tree path. A step that names a file the tree does not hold cannot load: on `validate`, `pipeIo`, `resolve` and `codegen` that is an `is_valid: false` verdict whose validation error says the file is missing, `prepareInputs` throws it as a `MethodLoadError`, and on `execute` and `start` it is a `422` `ValidateBundleError`, whose `validationErrors` say the same.
|
|
64
|
+
|
|
65
|
+
### The template check's warnings
|
|
66
|
+
|
|
67
|
+
A dry run hands an office template file of the tree to the document engine's template check. An error finding fails the step's dry run, and so the verdict, as a `dry_run` validation error on the step. A warning finding, such as a field of the template that matches no input, rides the valid verdict's `warnings` as a `template_check_warning` item:
|
|
68
|
+
|
|
69
|
+
| Member | Value |
|
|
70
|
+
| --- | --- |
|
|
71
|
+
| `category` | `dry_run` |
|
|
72
|
+
| `error_type` | `template_check_warning` |
|
|
73
|
+
| `pipe_code`, `domain_code` | The `PipeDocGen` step |
|
|
74
|
+
| `template_file` | The template file as the step names it; `null` for a step of the server's own library, whose path is never shown |
|
|
75
|
+
| `template_location` | The place in the file the finding names, in the engine's words (a defined name, a table, a cell, a shape, a tag); `null` when it names none |
|
|
76
|
+
| `message` | The finding in words |
|
|
77
|
+
|
|
78
|
+
The warnings come in families, always in this order: `optional_force_redundant`, `input_presence_vacuous`, the `hint_*` lints, then `template_check_warning`. None of them flips `is_valid`.
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
if (verdict.is_valid) {
|
|
82
|
+
for (const warning of verdict.warnings) {
|
|
83
|
+
if (warning.error_type === "template_check_warning") {
|
|
84
|
+
console.warn(`${warning.template_file ?? "?"} ${warning.template_location ?? ""}: ${warning.message}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Which step reads which file
|
|
91
|
+
|
|
92
|
+
Whatever form the method was sent in, its tree in any of the four forms, a `method_ref` or a `method_id`, the valid verdict names the files of the method's tree each step reads. `tree_files_read` is keyed by the same namespaced `pipe_ref` as `pipe_io_contracts`, each entry a list of `TreeFileRead` items, and a step that reads no file, a controller included, has an empty list. `tree_file_readers` states the same facts the other way: keyed by tree path in sorted order, it lists for each file the refs of the steps that read it, each once, and a file of the tree with no entry, such as a bundle itself or a README, is read by no step.
|
|
93
|
+
|
|
94
|
+
| Member | Value |
|
|
95
|
+
| --- | --- |
|
|
96
|
+
| `tree_path` | The file's path from the root of the method's tree, with forward slashes, never a path on the server |
|
|
97
|
+
| `use` | `template` for a `PipeDocGen` step's `template_file`; `template_asset` for an image, a stylesheet or a font its HTML template names by a literal relative URL, or one a stylesheet it names refers to; `python_module` for the module holding a `PipeFunc` step's function, when that module is a file of the tree |
|
|
98
|
+
|
|
99
|
+
The paths come from what each step already names, so a client can show which step uses a file, or warn before one a step reads is renamed or removed, without parsing the method.
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
if (verdict.is_valid && verdict.tree_file_readers) {
|
|
103
|
+
for (const [treePath, pipeRefs] of Object.entries(verdict.tree_file_readers)) {
|
|
104
|
+
console.log(`${treePath} is read by ${pipeRefs.join(", ")}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
`use` is typed `TreeFileUse | (string & {})`: a runner of a later version may add a use as another kind of step comes to read files of its tree, so a consumer switching on it says what a use it does not know does. Both fields are optional. A `pipelex-api` runner reports them from the first pipelex release after 0.90.0, and the hosted API once it runs such a release; an older runner sends neither, which reads as `undefined`, where an empty list says that the step reads no file.
|
|
110
|
+
|
|
111
|
+
## Saving a tree
|
|
112
|
+
|
|
113
|
+
`createMethod(input)` and `writeDraft(methodId, input)` take the tree in the four forms (`MethodTreeInput`), and send it by the rule above.
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
const method = await client.createMethod({ name: "Receipts", directory: "methods/receipts" });
|
|
117
|
+
|
|
118
|
+
const draft = await client.writeDraft(method.method_id, {
|
|
119
|
+
contents: { "main.mthds": source, "helpers/fx.py": helper },
|
|
120
|
+
expected_updated_at: method.updated_at,
|
|
121
|
+
});
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
**A draft write replaces the whole tree.** `writeDraft` refuses a draft that gives no tree, and `files: null` empties the draft. `createMethod` with no tree creates a method with an empty draft. A `contents` or `tree` holding no file is refused before anything is sent.
|
|
125
|
+
|
|
126
|
+
The save is refused, with the `ApiResponseError` of its problem, when the platform will not take the tree:
|
|
127
|
+
|
|
128
|
+
| Status and `code` | When |
|
|
129
|
+
|---|---|
|
|
130
|
+
| `422` `invalid_method_files` | The map breaks the standard's rules, a path that is not a tree path or two paths that collide among them; the problem's `tree_refusal` names the rule. |
|
|
131
|
+
| `422` `method_tree_too_large` | The tree passes one of the deployment's limits, which the problem's `limit` names. |
|
|
132
|
+
| `422` `blob_mismatch` | A hash entry states another size than the blob the organization holds under that hash. |
|
|
133
|
+
| `422` `missing_blobs` | A hash entry names bytes the organization does not hold (above). |
|
|
134
|
+
| `413` `payload_too_large` | The method would leave no room for the summary a publish adds. |
|
|
135
|
+
|
|
136
|
+
## The deployment's limits
|
|
137
|
+
|
|
138
|
+
The version handshake (`GET /v1/version`) announces the limits the deployment holds a tree to under `limits.method_tree`: `max_files`, `max_file_bytes`, `max_total_bytes` and `max_inline_bytes`. A limit the server does not announce is absent, a value that is not a positive integer is left out rather than trusted, and none is ever assumed, so a bare runner, which announces none, yields no limit. The client keeps the handshake's answer for five minutes (`HANDSHAKE_TTL_MS` in `src/method-trees.ts`), so every call in that time reads the same answer, and asks again once it is older; a handshake that fails is asked again on the next call. A `404` from the blob route and a refusal of the tree's size each drop the kept answer at once, since each says it may be stale ([above](#how-a-tree-travels)).
|
|
139
|
+
|
|
140
|
+
`methodTreeLimits()` returns the three listing limits, `{ maxFileCount, maxFileSize, maxTotalSize }`, in the shape `listMethodTree` takes as its `limits` option, for a caller listing a tree of its own; a `directory` given to a call is listed under them already. Listed under those limits, a tree the platform would refuse for its size is refused while it is listed, with `mthds`'s `TreeLimitExceededError` naming the limit, before any file is hashed. The inline limit, `max_inline_bytes`, is no listing limit: the client applies it when it chooses how a tree travels.
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
import { listMethodTree } from "mthds";
|
|
144
|
+
|
|
145
|
+
const tree = await listMethodTree("methods/receipts", { limits: await client.methodTreeLimits() });
|
|
146
|
+
const method = await client.createMethod({ name: "Receipts", tree });
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Reading a tree
|
|
150
|
+
|
|
151
|
+
`getMethod(methodId)` answers a `MethodDetail`: the method, with `files`, the draft's tree listed in tree path order. `getMethodVersion(methodId, version)` answers a `MethodVersion`: the version's summary, with the same `files`. Each file is a `SavedMethodFile`:
|
|
152
|
+
|
|
153
|
+
| Field | Meaning |
|
|
154
|
+
|---|---|
|
|
155
|
+
| `path` | Its tree path. |
|
|
156
|
+
| `sha256` | The SHA-256 of its bytes, 64 lower-case hex digits. |
|
|
157
|
+
| `size` | Its size in bytes. |
|
|
158
|
+
| `url` | A link to its bytes, signed by storage, which any HTTP client reads with no credential. |
|
|
159
|
+
| `expires_at` | When the link stops working, about fifteen minutes after the read. |
|
|
160
|
+
|
|
161
|
+
An empty draft lists no file and has a `draft_digest` of `null`. The listing pages are lighter: `listMethods` rows and `listMethodVersions` summaries carry the digest and no files. The writes, `createMethod`, `writeDraft` and `renameMethod`, answer `MethodData`, the method without its files.
|
|
162
|
+
|
|
163
|
+
**A link is a bearer credential.** Anyone holding it reads the file until it expires, so it belongs in no log, message or stored record; this SDK puts it in none of its errors, not even when the answer that carried it cannot be read. A read that fails because a link expired succeeds once the method is read again.
|
|
164
|
+
|
|
165
|
+
A method whose draft names a tree the platform holds no record of answers `getMethod` with a `409` whose verdict is `runtime`, and a version stored before methods were trees and never rewritten as one names no tree to serve, so `getMethodVersion` answers it with `409` `conflict`, whose verdict is `runtime` too. An answer whose `files` is not a listing, each entry with its string `path`, `url` and `expires_at`, a `sha256` of 64 lower-case hex digits and a non-negative integer `size`, is an `ApiResponseError` naming the entry at fault. Such an answer, like any other the SDK cannot read from these two routes or from `POST /v1/blobs/missing`, reaches its error redacted: `responseBody` has every `url` member and every link's query replaced by `[redacted]`, and the error keeps no `cause`, since a parse failure can quote the body.
|
|
166
|
+
|
|
167
|
+
## Writing a tree to disk
|
|
168
|
+
|
|
169
|
+
`writeMethodTree(files, { destination })` writes a listing to disk, each file at its tree path under `destination`. It runs in Node only, and loads Node's file system modules when it is called:
|
|
170
|
+
|
|
171
|
+
```ts
|
|
172
|
+
const detail = await client.getMethod("mt_123");
|
|
173
|
+
await client.writeMethodTree(detail.files, { destination: "receipts" });
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Each file is downloaded through its link, at most eight at a time, with no credential and no header of this client, following no redirect, under the client's request time limit (`requestTimeoutMs`), which covers the whole download, the body included: a body that stalls once its headers arrived fails with the timeout's code, `ABORT_TIMEOUT`. Each file's size and SHA-256 are checked against the listing, and no more than its listed size and one byte is read, so what lands on disk is the tree the digest names. The files are written into a fresh hidden directory beside `destination`, which takes the name `destination` only once every file is written, so a failed or interrupted write leaves nothing under that name and removes what it wrote.
|
|
177
|
+
|
|
178
|
+
`destination` must not exist, or must be an empty directory: one holding anything is refused, rather than written into or replaced. A listing holding a path that is not a tree path, the same path twice, or two paths that collide once case-folded or where one names a directory of the other, is refused too. Each refusal is a `RequestArgumentError` thrown before any download. An empty listing writes an empty directory.
|
|
179
|
+
|
|
180
|
+
## When a transfer fails
|
|
181
|
+
|
|
182
|
+
`MethodFileTransferError` is thrown when a file could not be moved between this machine and storage. `operation` says which way (`upload` for a request or a save that sends a tree by hash, `download` for `writeMethodTree`), `code` why, `sha256` which bytes, `path` the tree path when one is known, and `status` storage's HTTP status when it answered. Neither its message nor any member carries the link, nor storage's error body, which can echo the signed request, and it keeps no `cause`: the failure `fetch` throws for a request that got no answer can quote the URL, so only its network code (`ECONNREFUSED`, `ABORT_TIMEOUT`) reaches the message.
|
|
183
|
+
|
|
184
|
+
| `code` | When | Retryable |
|
|
185
|
+
|---|---|---|
|
|
186
|
+
| `unreachable` | Storage gave no answer: the connection failed or the time limit ran out. | yes |
|
|
187
|
+
| `storage_error` | Storage timed out or throttled the request (`408`, `429`) or failed (a `5xx` other than `501`). | yes |
|
|
188
|
+
| `refused` | Storage refused the request with any other status, an expired link among them. Read the method again for fresh links. | no |
|
|
189
|
+
| `mismatch` | A download's bytes are not the file the listing names: another size, or another SHA-256. | yes |
|
|
190
|
+
|
|
191
|
+
Its `errorDomain` is always `runtime`. An upload to a target that asks for a create-only write (`If-None-Match`) and answers `412` is not a failure: the bytes are already stored.
|
|
192
|
+
|
|
193
|
+
## What changed from the source fields
|
|
194
|
+
|
|
195
|
+
A run and a validation used to take a method as inline `.mthds` texts (`mthds_contents`, with `mthds_sources` naming them on `validate`), a zipped bundle (`bundle_b64`), or a `files` map of texts, with the files a step reads sent beside it as `resources`. They take its tree now, as every other call does, and `validateFiles`, `MthdsFile`, `ValidateFilesOptions`, `BundleResources` and the SDK's own copy of the runner's path rule for `resources` are gone, with no alias kept. A field the client no longer names still passes through `extra` to a runner that reads it.
|
|
196
|
+
|
|
197
|
+
A method used to hold its source as two strings, `mthds` (a bundle, or the JSON text of a file array) and `python` (the JSON text of the custom PipeFunc files), and a version carried `source_digest`. Both fields are gone from every read and every write, with no alias kept: a method is a tree, a version's digest is `tree_digest`, and the publish state is derived from the two digests, the draft unchanged when `draft_digest` is not `null` and equals `latest_published.tree_digest`. `methodSourceToContents`, `getMethodClosure` and `EmptyMethodSourceError` went with them.
|
|
198
|
+
|
|
199
|
+
A run's `source_digest` is something else, and stays: the SHA-256 of the sources a run sent as text, 64 hex digits with no prefix. `RunDetail` also carries the run's `tree_digest` and `crate_fingerprint`, each `null` for a run that recorded none.
|
package/docs/run-results.md
CHANGED
|
@@ -9,7 +9,10 @@ Two paths produce it. Against the hosted API the SDK starts a durable run and po
|
|
|
9
9
|
```ts
|
|
10
10
|
import { resultsFromExecute, summarizeUsage } from "@pipelex/sdk";
|
|
11
11
|
|
|
12
|
-
const executeResult = await client.execute({
|
|
12
|
+
const executeResult = await client.execute({
|
|
13
|
+
pipe_code: "my_domain.my_pipe",
|
|
14
|
+
contents: { "my_domain.mthds": source },
|
|
15
|
+
});
|
|
13
16
|
const results = resultsFromExecute(executeResult);
|
|
14
17
|
console.log(results.main_stuff, summarizeUsage(results).total_cost_usd);
|
|
15
18
|
```
|
|
@@ -72,7 +75,7 @@ const results = await client.startAndWaitForResult(
|
|
|
72
75
|
|
|
73
76
|
It is never called on the blocking path, a bare runner's `POST /v1/execute` or the fallback to it, which has no run id to give before it answers. The acknowledgement is handed over whole, a `method_ref` run's `method_provenance` and a `method_id` run's `method_version` (the version the run runs, a number or `"draft"`) included. The callback runs synchronously and its return value is ignored; an exception it throws propagates out of `startAndWaitForResult` before anything is polled, and the run it was told about keeps going.
|
|
74
77
|
|
|
75
|
-
**An abort before the run exists creates none.** The `signal` is read before the run is started as well as during the wait: a caller that aborts while the `GET /v1/version` handshake is in flight gets its abort, and neither `POST /v1/start` nor the blocking `POST /v1/execute` is sent; nor is the execute when the abort lands while a runner that looked hosted refuses the start. Once the start or the execute is sent, the abort stops only the wait, and a started run goes on, by its id.
|
|
78
|
+
**An abort before the run exists creates none.** The `signal` is read before the run is started as well as during the wait: a caller that aborts while the `GET /v1/version` handshake is in flight gets its abort, and neither `POST /v1/start` nor the blocking `POST /v1/execute` is sent; nor is the execute when the abort lands while a runner that looked hosted refuses the start. The same holds while the method's tree is prepared: the signal is handed to the directory's listing, each file's read, the blob route and each upload of a missing blob, so an abort stops the one under way and no run is started ([method-trees.md](./method-trees.md#how-a-tree-travels)). The tree is prepared once, before the start, so a fallback to the blocking execute sends the files the start sent, reading and uploading none of them again. Once the start or the execute is sent, the abort stops only the wait, and a started run goes on, by its id.
|
|
76
79
|
|
|
77
80
|
**Knowing whether a run may exist.** `onStarting`, the other callback `StartAndWaitForResultOptions` takes, is called right before each request that may create a run is sent: `POST /v1/start`, and the blocking `POST /v1/execute` of a bare runner or of the fallback to it, so it is called twice when a runner that looked hosted refuses the start. Until it is called no run exists, and once the `signal` has aborted it is not called and no such request is sent, so a caller that stops waiting before then can say that no run was started; from then until `onStarted`, a run may exist that the API has not yet named. It runs synchronously and its return value is ignored; an exception it throws propagates before the request is sent. The `pipelex-sdk` command words its interrupt message by it ([`cli.md`](cli.md)).
|
|
78
81
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pipelex/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.48.0",
|
|
4
4
|
"description": "TypeScript SDK for the Pipelex hosted API — execute methods, manage runs, and call the product surface.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
"vitest": "^4.0.18"
|
|
75
75
|
},
|
|
76
76
|
"dependencies": {
|
|
77
|
-
"mthds": "^0.
|
|
77
|
+
"mthds": "^0.33.0",
|
|
78
78
|
"smol-toml": "^1.6.0",
|
|
79
79
|
"undici": "^7.29.1"
|
|
80
80
|
}
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The files a bundle's steps read, sent with the bundle as the request's `resources`.
|
|
3
|
-
*
|
|
4
|
-
* A bundle sent as content has no directory, so a file one of its steps reads from beside the
|
|
5
|
-
* bundle, such as a `PipeDocGen` step's `template_file`, has to travel with it. The runner takes
|
|
6
|
-
* such files as `resources`: a JSON object mapping each file's path, exactly as the step names it,
|
|
7
|
-
* relative to the bundle and written with forward slashes, to the file's bytes in standard padded
|
|
8
|
-
* base64. It takes them beside `mthds_contents` (or a method bundle) on `/v1/validate`,
|
|
9
|
-
* `/v1/execute` and `/v1/start`, and beside inline `files[]` on `/v1/pipe-io`, `/v1/resolve` and
|
|
10
|
-
* `/v1/codegen`. A caller hands this SDK the bytes, keyed by that path, and the SDK encodes them.
|
|
11
|
-
*
|
|
12
|
-
* The SDK refuses before any request what the runner refuses for certain about the keys, with the
|
|
13
|
-
* runner's own rule (`resource_path_refusal` in pipelex's `bundle_resource.py`): an empty path, a
|
|
14
|
-
* backslash, a colon, a NUL character, an absolute path, a `..` segment, a path naming no file,
|
|
15
|
-
* and two keys naming one file once `.` segments and repeated slashes collapse. Each is a
|
|
16
|
-
* `RequestArgumentError` naming the key, where the runner answers a `422` `InvalidResourcePath`.
|
|
17
|
-
* The sizes are left to the server, since a deployment configures them: 32 files, 5 MiB each and
|
|
18
|
-
* 10 MiB in all by default, a `413` `PayloadTooLarge` beyond, and 512 KiB in all for the `.html`
|
|
19
|
-
* template files, a `413` `BundleResourcesTooLargeError`. So is every rule that needs the parsed
|
|
20
|
-
* bundle: a file no step names is a `422` `BundleResourceNotNamedError`.
|
|
21
|
-
*/
|
|
22
|
-
/** The bytes of one file a bundle's steps read: the byte forms `uploadFile` takes. */
|
|
23
|
-
export type BundleResourceBytes = Uint8Array | ArrayBuffer | Blob;
|
|
24
|
-
/**
|
|
25
|
-
* The files a bundle's steps read, such as a `PipeDocGen` step's template file, keyed by the path
|
|
26
|
-
* the step names (`templates/invoice.docx`), each as its bytes. `./templates/invoice.docx` names
|
|
27
|
-
* the same file, so only one of the two spellings may be given.
|
|
28
|
-
*/
|
|
29
|
-
export type BundleResources = Readonly<Record<string, BundleResourceBytes>>;
|
|
30
|
-
/**
|
|
31
|
-
* Whether `resources` is a plain object, the one shape a map of paths to bytes takes: its
|
|
32
|
-
* prototype is `Object.prototype` or `null`. A `Map`, an array or a class instance is not one,
|
|
33
|
-
* and the encoder refuses it rather than reading it as a map with no file in it.
|
|
34
|
-
*/
|
|
35
|
-
export declare function isResourceMap(resources: unknown): resources is Record<string, unknown>;
|
|
36
|
-
/**
|
|
37
|
-
* Whether `resources` carries at least one file. An absent, `null` or empty map carries none, so
|
|
38
|
-
* it is not sent, as an empty bundle encoding is not. A value that is not a plain object carries
|
|
39
|
-
* none either; the encoder refuses it.
|
|
40
|
-
*/
|
|
41
|
-
export declare function hasBundleResources(resources: unknown): boolean;
|
|
42
|
-
/**
|
|
43
|
-
* Why `path` cannot name a file inside a bundle, or `undefined` when it can — the runner's rule,
|
|
44
|
-
* held by the key of a file sent with a bundle and by the path a step names.
|
|
45
|
-
*/
|
|
46
|
-
export declare function bundleResourcePathRefusal(path: string): string | undefined;
|
|
47
|
-
/**
|
|
48
|
-
* The one spelling of a bundle-relative path the runner keys a file by: `.` segments and repeated
|
|
49
|
-
* slashes collapse, so `./templates//invoice.docx` is `templates/invoice.docx`. Only meaningful
|
|
50
|
-
* for a path {@link bundleResourcePathRefusal} accepts.
|
|
51
|
-
*/
|
|
52
|
-
export declare function canonicalBundleResourcePath(path: string): string;
|
|
53
|
-
/**
|
|
54
|
-
* Encode `resources` for the wire: each key as given, each value as its bytes in standard padded
|
|
55
|
-
* base64. Returns `undefined` when there is nothing to send, so the key stays off the wire.
|
|
56
|
-
*
|
|
57
|
-
* Every key is checked before any value is read, so a refused path costs no read of a `Blob`.
|
|
58
|
-
*
|
|
59
|
-
* @throws {RequestArgumentError} `resources` is not a map of paths to bytes, a key cannot name a
|
|
60
|
-
* file inside the bundle, two keys name one file, or a value is not bytes.
|
|
61
|
-
*/
|
|
62
|
-
export declare function encodeBundleResources(resources: BundleResources | null | undefined): Promise<Record<string, string> | undefined>;
|