@pipelex/sdk 0.47.0 → 0.48.1

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.
Files changed (70) hide show
  1. package/README.md +9 -7
  2. package/dist/artifacts.d.ts +4 -3
  3. package/dist/artifacts.js +20 -14
  4. package/dist/artifacts.js.map +1 -1
  5. package/dist/cli/script.d.ts +1 -1
  6. package/dist/cli/source.d.ts +8 -7
  7. package/dist/cli/source.js +11 -5
  8. package/dist/cli/source.js.map +1 -1
  9. package/dist/client.d.ts +326 -213
  10. package/dist/client.js +829 -517
  11. package/dist/client.js.map +1 -1
  12. package/dist/codegen-check.d.ts +2 -3
  13. package/dist/codegen-check.js +2 -3
  14. package/dist/codegen-check.js.map +1 -1
  15. package/dist/errors.d.ts +5 -2
  16. package/dist/errors.js +5 -2
  17. package/dist/errors.js.map +1 -1
  18. package/dist/hooks/bundle-gather.d.ts +47 -40
  19. package/dist/hooks/bundle-gather.js +121 -219
  20. package/dist/hooks/bundle-gather.js.map +1 -1
  21. package/dist/hooks/check-core.d.ts +70 -6
  22. package/dist/hooks/check-core.js +188 -29
  23. package/dist/hooks/check-core.js.map +1 -1
  24. package/dist/hooks/claude-mthds-check.d.ts +19 -5
  25. package/dist/hooks/claude-mthds-check.js +20 -53
  26. package/dist/hooks/claude-mthds-check.js.map +1 -1
  27. package/dist/hooks/secret-guard.d.ts +38 -0
  28. package/dist/hooks/secret-guard.js +134 -0
  29. package/dist/hooks/secret-guard.js.map +1 -0
  30. package/dist/hooks/validate-stage.d.ts +29 -0
  31. package/dist/hooks/validate-stage.js +66 -0
  32. package/dist/hooks/validate-stage.js.map +1 -0
  33. package/dist/index.d.ts +3 -4
  34. package/dist/index.js +4 -3
  35. package/dist/index.js.map +1 -1
  36. package/dist/method-trees.d.ts +220 -74
  37. package/dist/method-trees.js +250 -37
  38. package/dist/method-trees.js.map +1 -1
  39. package/dist/models.d.ts +162 -59
  40. package/dist/node-host/node.d.ts +33 -0
  41. package/dist/node-host/node.js +27 -0
  42. package/dist/node-host/node.js.map +1 -0
  43. package/dist/node-host/unavailable.d.ts +8 -0
  44. package/dist/node-host/unavailable.js +8 -0
  45. package/dist/node-host/unavailable.js.map +1 -0
  46. package/dist/node-host.d.ts +53 -0
  47. package/dist/node-host.js +72 -0
  48. package/dist/node-host.js.map +1 -0
  49. package/dist/prepare-inputs.d.ts +46 -27
  50. package/dist/prepare-inputs.js +123 -42
  51. package/dist/prepare-inputs.js.map +1 -1
  52. package/dist/product-models.d.ts +14 -16
  53. package/dist/upload.d.ts +4 -9
  54. package/dist/upload.js +13 -16
  55. package/dist/upload.js.map +1 -1
  56. package/dist/version.d.ts +1 -1
  57. package/dist/version.js +1 -1
  58. package/docs/architecture.md +44 -21
  59. package/docs/cli.md +1 -1
  60. package/docs/crate-routes.md +43 -21
  61. package/docs/errors.md +5 -5
  62. package/docs/hook-bundle.md +27 -1
  63. package/docs/input-preparation.md +13 -13
  64. package/docs/method-trees.md +120 -22
  65. package/docs/run-results.md +5 -2
  66. package/package.json +8 -2
  67. package/dist/bundle-resources.d.ts +0 -62
  68. package/dist/bundle-resources.js +0 -165
  69. package/dist/bundle-resources.js.map +0 -1
  70. package/docs/bundle-resources.md +0 -107
@@ -1 +1 @@
1
- {"version":3,"file":"upload.js","sourceRoot":"","sources":["../src/upload.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"upload.js","sourceRoot":"","sources":["../src/upload.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EACL,uBAAuB,EACvB,kBAAkB,EAClB,yBAAyB,EACzB,oBAAoB,EACpB,gCAAgC,GACjC,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AACpE,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC5D,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAyC3C,MAAM,oBAAoB,GAAG,0BAA0B,CAAC;AACxD,MAAM,gBAAgB,GAAG,YAAY,CAAC;AAEtC,MAAM,cAAc,GAA2B;IAC7C,GAAG,EAAE,WAAW;IAChB,GAAG,EAAE,YAAY;IACjB,IAAI,EAAE,YAAY;IAClB,GAAG,EAAE,WAAW;IAChB,IAAI,EAAE,YAAY;IAClB,GAAG,EAAE,eAAe;IACpB,GAAG,EAAE,WAAW;IAChB,GAAG,EAAE,YAAY;IACjB,IAAI,EAAE,YAAY;IAClB,IAAI,EAAE,YAAY;IAClB,GAAG,EAAE,iBAAiB;IACtB,GAAG,EAAE,YAAY;IACjB,GAAG,EAAE,UAAU;IACf,IAAI,EAAE,kBAAkB;CACzB,CAAC;AAEF,4FAA4F;AAC5F,SAAS,QAAQ,CAAC,IAAY;IAC5B,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IAClC,OAAO,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,IAAI,IAAI,CAAC;AACzC,CAAC;AAED,qFAAqF;AACrF,MAAM,UAAU,gBAAgB,CAAC,QAAgB;IAC/C,MAAM,GAAG,GAAG,QAAQ,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;IACtC,IAAI,GAAG,GAAG,CAAC,IAAI,GAAG,KAAK,QAAQ,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,oBAAoB,CAAC;IACxE,MAAM,GAAG,GAAG,QAAQ,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC;IAClD,OAAO,cAAc,CAAC,GAAG,CAAC,IAAI,oBAAoB,CAAC;AACrD,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,IAAY;IAC9C,MAAM,MAAM,GAAG,MAAM,YAAY,EAAE,CAAC;IACpC,IAAI,MAAM,CAAC,IAAI,KAAK,IAAI,EAAE,CAAC;QACzB,MAAM,IAAI,uBAAuB,CAC/B,MAAM,CAAC,MAAM,KAAK,IAAI;YACpB,CAAC,CAAC,+BAA+B,IAAI,+CAA+C;gBAChF,+FAA+F;YACnG,CAAC,CAAC,+BAA+B,IAAI,MAAM,MAAM,CAAC,MAAM,EAAE,EAC5D,IAAI,EACJ,YAAY,CAAC,MAAM,CAAC,CACrB,CAAC;IACJ,CAAC;IACD,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC;IACzB,IAAI,CAAC;QACH,OAAO,MAAM,IAAI,CAAC,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;IACtC,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,IAAI,uBAAuB,CAC/B,+BAA+B,IAAI,MAAO,KAA2B,CAAC,IAAI,IAAI,YAAY,IAAI,EAC9F,IAAI,EACJ,EAAE,KAAK,EAAE,CACV,CAAC;IACJ,CAAC;AACH,CAAC;AAED,mFAAmF;AACnF,MAAM,UAAU,aAAa,CAAC,KAAiB;IAC7C,IAAI,OAAO,MAAM,KAAK,WAAW,EAAE,CAAC;QAClC,OAAO,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;IAC/C,CAAC;IACD,IAAI,MAAM,GAAG,EAAE,CAAC;IAChB,MAAM,KAAK,GAAG,MAAM,CAAC,CAAC,kDAAkD;IACxE,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,KAAK,CAAC,MAAM,EAAE,KAAK,IAAI,KAAK,EAAE,CAAC;QACzD,MAAM,IAAI,MAAM,CAAC,YAAY,CAAC,GAAG,KAAK,CAAC,QAAQ,CAAC,KAAK,EAAE,KAAK,GAAG,KAAK,CAAC,CAAC,CAAC;IACzE,CAAC;IACD,OAAO,IAAI,CAAC,MAAM,CAAC,CAAC;AACtB,CAAC;AASD,6EAA6E;AAC7E,KAAK,UAAU,YAAY,CACzB,KAAsB,EACtB,OAA0B;IAE1B,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC9B,MAAM,KAAK,GAAG,MAAM,aAAa,CAAC,KAAK,CAAC,CAAC;QACzC,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,QAAQ,CAAC,KAAK,CAAC,CAAC;QACrD,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,WAAW,EAAE,OAAO,CAAC,WAAW,IAAI,gBAAgB,CAAC,QAAQ,CAAC,EAAE,CAAC;IAC7F,CAAC;IACD,IAAI,KAAK,YAAY,UAAU,EAAE,CAAC;QAChC,OAAO,oBAAoB,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;IAC9C,CAAC;IACD,IAAI,KAAK,YAAY,WAAW,EAAE,CAAC;QACjC,OAAO,oBAAoB,CAAC,IAAI,UAAU,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC,CAAC;IAC9D,CAAC;IACD,0DAA0D;IAC1D,MAAM,KAAK,GAAG,IAAI,UAAU,CAAC,MAAM,KAAK,CAAC,WAAW,EAAE,CAAC,CAAC;IACxD,MAAM,IAAI,GAAG,OAAO,CAAC,QAAQ,IAAK,KAAc,CAAC,IAAI,CAAC;IACtD,MAAM,QAAQ,GAAG,IAAI,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,gBAAgB,CAAC;IACnE,MAAM,WAAW,GAAG,OAAO,CAAC,WAAW,IAAI,CAAC,KAAK,CAAC,IAAI,IAAI,gBAAgB,CAAC,QAAQ,CAAC,CAAC,CAAC;IACtF,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,WAAW,EAAE,CAAC;AAC1C,CAAC;AAED,SAAS,oBAAoB,CAAC,KAAiB,EAAE,OAA0B;IACzE,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,gBAAgB,CAAC;IACtD,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,WAAW,EAAE,OAAO,CAAC,WAAW,IAAI,gBAAgB,CAAC,QAAQ,CAAC,EAAE,CAAC;AAC7F,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU,CAC9B,MAA2B,EAC3B,KAAsB,EACtB,UAA6B,EAAE;IAE/B,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,WAAW,EAAE,GAAG,MAAM,YAAY,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;IAC5E,+EAA+E;IAC/E,cAAc,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;IAC/B,MAAM,IAAI,GAAG,aAAa,CAAC,KAAK,CAAC,CAAC;IAClC,IAAI,QAA2C,CAAC;IAChD,IAAI,CAAC;QACH,QAAQ,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,YAAY,EAAE,WAAW,EAAE,CAAC,CAAC;IAChF,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,cAAc,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC;IACxC,CAAC;IACD,kFAAkF;IAClF,MAAM,MAAM,GAAY,QAAQ,CAAC,QAAQ,CAAC;IAC1C,OAAO;QACL,GAAG,EAAE,QAAQ,CAAC,GAAG;QACjB,QAAQ,EAAE,OAAO,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,QAAQ;QACxD,WAAW;QACX,IAAI,EAAE,KAAK,CAAC,MAAM;KACnB,CAAC;AACJ,CAAC;AAED,sFAAsF;AACtF,SAAS,cAAc,CAAC,KAAc,EAAE,QAAgB;IACtD,IAAI,KAAK,YAAY,gBAAgB,EAAE,CAAC;QACtC,6FAA6F;QAC7F,cAAc;QACd,IAAI,KAAK,CAAC,MAAM,IAAI,GAAG,IAAI,KAAK,CAAC,MAAM,GAAG,GAAG,EAAE,CAAC;YAC9C,OAAO,IAAI,oBAAoB,CAC7B,cAAc,QAAQ,mBAAmB,KAAK,CAAC,MAAM,kCAAkC;gBACrF,4DAA4D,EAC9D,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,IAAI,EAAE,YAAY,EAAE,QAAQ,EAAE,CACrE,CAAC;QACJ,CAAC;QACD,QAAQ,KAAK,CAAC,MAAM,EAAE,CAAC;YACrB,KAAK,GAAG;gBACN,OAAO,IAAI,kBAAkB,CAC3B,wBAAwB,QAAQ,MAAM,KAAK,CAAC,aAAa,IAAI,sCAAsC,GAAG,EACtG,QAAQ,EACR,KAAK,CAAC,MAAM,EACZ,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,WAAW,EAAE,CACpC,CAAC;YACJ,KAAK,GAAG,CAAC;YACT,KAAK,GAAG;gBACN,OAAO,IAAI,yBAAyB,CAClC,cAAc,QAAQ,yBAAyB,KAAK,CAAC,MAAM,0CAA0C,EACrG,KAAK,CAAC,MAAM,EACZ,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,CAC3B,CAAC;YACJ,KAAK,GAAG;gBACN,OAAO,IAAI,gCAAgC,CACzC,wFAAwF;oBACtF,wCAAwC,EAC1C,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,CAC3B,CAAC;YACJ;gBACE,OAAO,IAAI,oBAAoB,CAC7B,cAAc,QAAQ,aAAa,KAAK,CAAC,MAAM,MAAM,KAAK,CAAC,aAAa,IAAI,KAAK,CAAC,UAAU,GAAG,EAC/F;oBACE,KAAK,EAAE,KAAK;oBACZ,MAAM,EAAE,KAAK,CAAC,MAAM;oBACpB,IAAI,EAAE,KAAK,CAAC,MAAM,IAAI,GAAG,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,YAAY;oBACzD,QAAQ;iBACT,CACF,CAAC;QACN,CAAC;IACH,CAAC;IACD,IAAI,KAAK,YAAY,mBAAmB,EAAE,CAAC;QACzC,OAAO,IAAI,oBAAoB,CAC7B,cAAc,QAAQ,sCAAsC,KAAK,CAAC,IAAI,IAAI,aAAa,IAAI;QAC3F,mFAAmF;QACnF,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,KAAK,eAAe,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,aAAa,EAAE,QAAQ,EAAE,CAC7F,CAAC;IACJ,CAAC;IACD,iFAAiF;IACjF,mFAAmF;IACnF,kFAAkF;IAClF,8EAA8E;IAC9E,6DAA6D;IAC7D,MAAM,MAAM,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IACtE,OAAO,IAAI,oBAAoB,CAAC,cAAc,QAAQ,0BAA0B,MAAM,GAAG,EAAE;QACzF,KAAK,EAAE,KAAK;QACZ,IAAI,EAAE,YAAY;QAClB,QAAQ;KACT,CAAC,CAAC;AACL,CAAC"}
package/dist/version.d.ts CHANGED
@@ -3,4 +3,4 @@
3
3
  * `User-Agent` without importing the public barrel. Kept in sync with
4
4
  * `package.json` — enforced by `tests/index.test.ts`.
5
5
  */
6
- export declare const SDK_VERSION = "0.47.0";
6
+ export declare const SDK_VERSION = "0.48.1";
package/dist/version.js CHANGED
@@ -3,5 +3,5 @@
3
3
  * `User-Agent` without importing the public barrel. Kept in sync with
4
4
  * `package.json` — enforced by `tests/index.test.ts`.
5
5
  */
6
- export const SDK_VERSION = "0.47.0";
6
+ export const SDK_VERSION = "0.48.1";
7
7
  //# sourceMappingURL=version.js.map
@@ -38,7 +38,28 @@ The package also declares one `bin`, `pipelex-sdk`, which is `dist/cli.js` with
38
38
 
39
39
  Condition order in `exports` matters: `types` is listed first (TS/Node resolution convention — the type-checker looks at the first matching condition), then `import`, then `default` as the CJS fallback.
40
40
 
41
- The map has a second entry, **`@pipelex/sdk/upload`** (`dist/upload-grant.js`), the browser-safe one. The main entry is Node-first — `upload.ts` names `node:fs/promises` and `artifacts.ts` names `undici`, so a browser-targeting bundler must mark them external — because the client holds an API key and belongs on a server. The subpath carries only what a page needs to store a file with an upload grant: `uploadWithGrant`, the grant types and the three error classes it raises. Its runtime import graph is `errors.ts` (with the fallback table it reads, `error-verdicts.ts`), `timers.ts` and `mthds/protocol`, nothing else, and the unit suite bundles it for the browser with esbuild, nothing marked external, so a Node-only module becoming reachable fails a test rather than a consumer's build. Both entries load the same `dist/errors.js` and `dist/upload-grant.js`, so the function and the error classes are identical whichever entry a consumer imports them through. Whether the main entry should become browser-safe as a whole is a separate question, because it touches how path strings are read.
41
+ The map has a second entry, **`@pipelex/sdk/upload`** (`dist/upload-grant.js`), the one a page that stores a file imports. The client holds an API key and belongs on a server, so a page imports the main entry for its error classes, its types and its pure helpers, never to construct a client, and the subpath carries only what a page needs to store a file with an upload grant: `uploadWithGrant`, the grant types and the three error classes it raises. Its runtime import graph is `errors.ts` (with the fallback table it reads, `error-verdicts.ts`), `timers.ts` and `mthds/protocol`, nothing else. Both entries load the same `dist/errors.js` and `dist/upload-grant.js`, so the function and the error classes are identical whichever entry a consumer imports them through.
42
+
43
+ **Both entries bundle for the browser with nothing marked external.** A Next.js client component importing an error class from `@pipelex/sdk` puts the barrel's whole graph in the browser bundle, the modules it loads by dynamic import included, since a bundler bundles a dynamic import with a literal specifier like a static one; in 0.48.0 that graph reached `node:fs` through the `mthds` package's root entry, which Turbopack's `next build` tolerated and its `next dev` did not, panicking and answering `500` for the page. `tests/browser-bundle.test.ts` is the gate the templates' builds are not: it builds the package with `tsc` into a scratch directory laid out as the published package, bundles every entry of the `exports` map for the browser with esbuild, and fails on any Node built-in, `undici` or the `mthds` root entry it reaches, naming each import and the module that made it.
44
+
45
+ ### The Node host
46
+
47
+ What only Node has, `node:fs/promises`, `node:path`, the `mthds` root entry's tree lister and `undici`'s `Agent`, sits behind one module, the Node host, which the rest of the SDK reaches through `loadNodeHost()` in `src/node-host.ts`. It imports `#node-host`, the package's own subpath import, and the `imports` map of `package.json` resolves it by condition:
48
+
49
+ - under `node`, to `dist/node-host/node.js` (`src/node-host/node.ts`), which loads the two built-ins with it, and the `mthds` root entry and `undici` on their first use, by a dynamic import;
50
+ - under every other condition set, a browser's or an edge runtime's, to `dist/node-host/unavailable.js` (`src/node-host/unavailable.ts`), which imports nothing and says the host is absent.
51
+
52
+ Node, a Next.js server bundle and the hook bundle resolve the host; a browser bundle resolves its absence, so the barrel's graph there holds no Node built-in. `loadNodeHost()` never throws: it answers the host, or its absence with the reason a refusal gives, in three cases.
53
+
54
+ - **Outside Node**, wherever `process.versions.node` is absent, it loads nothing and gives no reason, and each operation makes the refusal it always made there.
55
+ - **Inside Node, without the `node` condition**, as a bundle built for esbuild's `neutral` platform resolves the package, `#node-host` resolves to the absent variant, and the reason names the missing condition and how to bundle with it.
56
+ - **Inside Node, where the import itself fails**, as in a toolchain that does not resolve the `imports` map of `package.json` (Jest before 29.4, whose error is `ERR_PACKAGE_IMPORT_NOT_DEFINED` or "Cannot find module '#node-host'") or in a package installed in part, the reason says the host could not be loaded, names that toolchain and quotes the import's own error, which the refusal carries as its `cause`. It never claims the `node` condition was missing.
57
+
58
+ Each operation that needs Node refuses with its own class and verdict whichever the case, and the reason after its own subject: reading a path string is an `InvalidLocalSourceError`, `downloadArtifacts` an `ArtifactOperationError` (`config`), a `directory` a `RequestArgumentError` (`input`) and `writeMethodTree` a `RequestArgumentError` (`config`). A call given a `directory` asks for the host before its version handshake, so a failed or refused handshake never hides that refusal. An artifact fetch is never refused: without the host, or where `undici` cannot be loaded, it runs on the platform's default dispatcher. `tests/node-host.test.ts`, `tests/node-host-unresolved.test.ts` and `tests/node-host-import-fails.test.ts` hold each case, and `tests/browser-bundle.test.ts` runs the import failure under Node itself, against a built copy of the package whose `package.json` has no `imports` map.
59
+
60
+ The package's own conditions were chosen over marking each dynamic import for bundlers to leave alone (`/* webpackIgnore: true */`, `/* turbopackIgnore: true */`): only webpack and Turbopack read those marks, so esbuild, Vite and Rollup would still bundle Node into a page, and an import left alone is resolved at run time from wherever the bundle landed, which finds the package's own `mthds` or `undici` only when the package manager hoisted it beside the application, and a strict layout such as pnpm's does not. Conditions are read by every bundler and resolve from the package itself.
61
+
62
+ The `imports` map points into `dist/`, which is what ships, so the tools that run the source map `#node-host` to the source of its Node target: `tsconfig.json`'s `paths` for the type checker, which `tests/browser-bundle.test.ts` holds to the map, and `vitest.config.ts` and `scripts/build-hook.mjs`, which derive it from the map. `dependency-cruiser` keeps every other module of the barrel's graph off a Node built-in, `undici` and the `mthds` root entry, type-only imports apart, and nothing imports a variant by its path.
42
63
 
43
64
  ## Wire conventions
44
65
 
@@ -57,18 +78,18 @@ The ruling is about the artifacts, not about one envelope: `RunResults` carries
57
78
 
58
79
  ### Run sources
59
80
 
60
- A run request carries its method inline exactly one of three ways: `mthds_contents` text, a `files` bundle map, or a `bundle_b64` zip. Those three are *encodings* of a bundle the caller ships, and they are mutually exclusive. Two selectors sit beside them on separate axes: the Pipelex-API `method_ref` (an address the RUNNER resolves by git fetch — a complete run source that pairs with nothing; see [Pipelex API run extensions](#pipelex-api-run-extensions-method_ref) below) and the hosted `method_id` (a catalog id the PLATFORM resolves — legal on its own, and legal alongside one inline encoding, where the inline source runs and the id rides along as run-history linkage; see [Hosted run extensions](#hosted-run-extensions-method_id) below). The rules — and crucially, _where they live_ — are:
81
+ A run request carries its method as its **tree**, the MTHDS protocol's one method source, given in exactly one of four forms: `files` (the protocol's map from tree path to file entry, posted as given), `tree` (the protocol's listed `MethodTreeSource`), and the client's own `directory` (a directory on disk, listed by the `mthds` package's lister) and `contents` (each file's bytes or text by tree path). The last three are sent inline or by hash by the `mthds` package's rule ([method-trees.md](./method-trees.md#how-a-tree-travels)), so whatever the form, the wire carries one `files` map. Two selectors sit beside the tree on separate axes: the Pipelex-API `method_ref` (an address the RUNNER resolves by git fetch — a complete run source that pairs with nothing; see [Pipelex API run extensions](#pipelex-api-run-extensions-method_ref) below) and the hosted `method_id` (a catalog id the PLATFORM resolves — legal on its own, and legal alongside the tree, where the tree runs and the id rides along as run-history linkage; see [Hosted run extensions](#hosted-run-extensions-method_id) below). The rules — and crucially, _where they live_ — are:
61
82
 
62
- - **Exclusivity is enforced from the standard, not restated here.** `assertExclusiveRunSources` and `hasBundlePayload` are imported from `mthds/protocol`; they are pure predicates over `RunRequest`, so this client and the MTHDS runners cannot drift on which combinations they reject. A second local copy would be free to disagree about what is legal. The `method_ref` pairings (`assertMethodRefPairsWithNothing` in `client.ts`) and the linkage form's bare `method_id` (`assertLinkageMethodIdIsBare`) are the local additions, because the protocol knows neither field — the wording of the `method_ref` errors mirrors the server's own validator.
63
- - **Exclusivity keys off presence, the run-source precondition keys off runnability.** Supplying `files: {}` _and_ `bundle_b64` is rejected — the caller expressed two encodings — while an empty encoding on its own carries no method and so is neither shipped on the wire nor counted as "something to run".
64
- - **`files` / `bundle_b64` / `bundleMain` / `method_ref` / `method_id` are reserved keys on `extra`.** `extra` merges last into the body, so smuggling a run source through it would overwrite the validated fields and bypass the exclusivity check; `buildExtensions` rejects any reserved key — the named run sources, the two method selectors, _and_ the client-only `bundleMain` hint. `buildExtensions` also strips prototype-pollution keys (`__proto__` / `constructor` / `prototype`), which `JSON.parse`-sourced `extra` can carry, so the client never emits a pollution gadget on the wire.
65
- - **`bundleMain` is client-only and never serialized.** It is an entrypoint hint for a _local_ runner; the request builders name wire fields explicitly, and it is reserved on `extra`, so it cannot leak onto the wire by either path.
83
+ - **The tree's own rule is the standard's, not restated here.** `assertOneMethodSource` is imported from `mthds/protocol` and applied to `files` and `tree` on every run and on `validate`, so this client and the MTHDS runners cannot drift on which combinations they reject: both at once, or either holding no file, is refused. A second local copy would be free to disagree about what is legal. The client adds the refusals of its own two forms (two forms at once, `contents` holding no file), the `method_ref` pairings (`assertMethodRefPairsWithNothing` in `client.ts`) and the linkage form's bare `method_id` (`assertLinkageMethodIdIsBare`), because the protocol knows none of them — the wording of the `method_ref` errors mirrors the server's own validator.
84
+ - **An empty `directory` and a `null` member are absent; an empty map is not.** `directory: ""` names no directory and counts as no form, while `files: {}` or `contents: {}` is refused, as the standard refuses a tree holding no file, rather than read as "nothing to run".
85
+ - **`files` / `tree` / `directory` / `contents` / `bundleMain` / `method_ref` / `method_id` are reserved keys on `extra`.** `extra` merges last into the body, so smuggling a run source through it would overwrite the validated fields and bypass the exclusivity check; `buildExtensions` rejects any reserved key — the protocol's own fields, the client's tree forms, the two method selectors, _and_ the client-only `bundleMain` hint. A field this client no longer names, such as a runner's legacy `mthds_contents`, is not reserved, and passes through `extra` to a runner that still reads it. `buildExtensions` also strips prototype-pollution keys (`__proto__` / `constructor` / `prototype`), which `JSON.parse`-sourced `extra` can carry, so the client never emits a pollution gadget on the wire.
86
+ - **`bundleMain`, `directory` and `contents` are client-only and never serialized as such.** `bundleMain` is an entrypoint hint for a _local_ runner, and the two tree forms become the wire's `files`; the request builders name wire fields explicitly, and each is reserved on `extra`, so none can leak onto the wire by either path.
66
87
 
67
88
  ### Pipelex API run extensions (`method_ref`)
68
89
 
69
90
  `method_ref` is the layer-2 sibling of the hosted `method_id`: a globally resolvable address (`github.com/<owner>/<repo>[/<selector>][@<tag>]`) that any pipelex-api deployment resolves itself — git fetch at the tag, package located by manifest identity — with no catalog anywhere. It lives in its own `PipelexApiRunExtensions` interface rather than in `PipelexHostedRunExtensions`, because the two selectors belong to different layers: an address is meaningful against a bare runner; a catalog id is not. Served by pipelex-api >= 0.21.0; on `api.pipelex.com` availability follows the platform deploy that forwards it.
70
91
 
71
- - **A complete run source, so it pairs with nothing.** The fetched package carries its `.mthds` and its entry pipe. The client rejects `method_ref` beside inline `mthds_contents`, beside a bundle encoding, and beside `method_id` — mirroring the server's 422s, with no linkage exception (an address run has its own provenance and needs no linkage id). `pipe_code` beside it is legal: it overrides the manifest's `main_pipe`.
92
+ - **A complete run source, so it pairs with nothing.** The fetched package carries its `.mthds` and its entry pipe. The client rejects `method_ref` beside the method's tree in any form and beside `method_id`, before any directory is listed or any blob uploaded, — mirroring the server's 422s, with no linkage exception (an address run has its own provenance and needs no linkage id). `pipe_code` beside it is legal: it overrides the manifest's `main_pipe`.
72
93
  - **Provenance comes back typed.** A `method_ref` run's start ack (`PipelexRunResultStart`) and execute result (`PipelexExecuteResult`) carry `method_provenance` — `{address, tag, commit_sha}`, the SHA being what keeps the run explainable when a tag moves.
73
94
  - **It rides the blocking fallback too**, like `method_id` — a bare runner reached through `startAndWaitForResult`'s degrade runs the same fetched package.
74
95
  - **An empty string is treated as absent**, same as `method_id`.
@@ -83,21 +104,20 @@ A run request carries its method inline exactly one of three ways: `mthds_conten
83
104
  - **Alone it is a run source; alongside an inline source it is linkage.** With no inline source the platform resolves and runs the version the id names. With one, the inline source is what RUNS (precedence) and the id is recorded as run-history linkage on the Run row — the index key `GET /v1/runs?method_id=` queries. Rejecting the combination is not an option: it once orphaned every unsaved-bundle run from its method. The client therefore counts `method_id` towards its "something to run" precondition, and never treats it as an exclusivity violation.
84
105
  - **It rides the blocking fallback too.** `startAndWaitForResult` degrades to `POST /v1/execute` against a runner with no run store, and the selector goes with it — so a bare `pipelex-api` answers a `422` that names the key (its Rule 4 diagnosis: "a hosted-only selector must be sent to the hosted API") instead of the client silently dropping it and running something else.
85
106
  - **An empty string is treated as absent.** `method_id: ""` selects no method and links nothing, so it is not sent and does not satisfy the precondition.
86
- - **It may name a version.** A bare `mt_…` runs the method's latest published version, `mt_…@<n>` the fixed version `n` and `mt_…@draft` its draft; the suffix is part of the string and passes through untouched, on the run routes and the tooling routes alike, since the platform parses it at its edge. On the linkage form, beside an inline source, the id must be bare: the inline source is what runs, so a suffix there would claim a version that did not run. The platform refuses it with a `422`, and `execute` and `start` refuse it first with a `RequestArgumentError`, before anything is sent, counting an inline source only when it reaches the wire: an empty `mthds_contents`, `files: {}` or `bundle_b64: ""` is no source, so a suffixed id beside one runs the version it names. The start ack and the execute response carry `method_version`, the version a `method_id` run runs, so a caller learns which version a bare id ran from the answer that started it. `parseMethodSelector` splits a selector into its bare id and version locally, for a caller that reads the method by its bare id.
107
+ - **It may name a version.** A bare `mt_…` runs the method's latest published version, `mt_…@<n>` the fixed version `n` and `mt_…@draft` its draft; the suffix is part of the string and passes through untouched, on the run routes and the tooling routes alike, since the platform parses it at its edge. On the linkage form, beside an inline source, the id must be bare: the inline source is what runs, so a suffix there would claim a version that did not run. The platform refuses it with a `422`, and `execute` and `start` refuse it first with a `RequestArgumentError`, before anything is sent, counting the tree in any of its forms; an empty `directory` is no tree, so a suffixed id beside one runs the version it names. The start ack and the execute response carry `method_version`, the version a `method_id` run runs, so a caller learns which version a bare id ran from the answer that started it. `parseMethodSelector` splits a selector into its bare id and version locally, for a caller that reads the method by its bare id.
87
108
 
88
109
  ## Module layout
89
110
 
90
111
  Flat `src/` (mirrors `mthds-js`'s `runners/api` flatness — the SDK has one client, so no runner/registry abstraction):
91
112
 
92
113
  - `client.ts` — `PipelexApiClient`: the `request()` pipeline (auth, base URL, timeouts/abort, problem-details parsing) and every route. It implements the standard's `MTHDSProtocol<DictPipeOutput>` for every route but `validate`, so the protocol-execution methods stay shaped like the standard: `validate` answers Pipelex's own verdict, `PipelexValidationResult`, because the hosted plane still sends the verdict that predates the protocol's problem items, where the standard's `ValidationResult` lists them.
93
- - `models.ts` — Dict-serialized concretes (`DictStuff` / `DictWorkingMemory` / `DictPipeOutput` / `DictRunResultExecute`), the `/v1/validate` surface (`PipelexValidationResult`, whose two arms are declared whole rather than as narrowings of the standard's `ValidationResult`, `ValidationErrorItem`, and the `SuggestedFix` / `FixOp` repair vocabulary its items carry — the hosted plane's shapes, which predate the standard's problem items, their operation and safety vocabularies pinned to the standard's by a type-level test, see [errors.md](./errors.md#a-validation-item--validationerroritem)), the tools surface (`Diagnostic` / `LintResponse` / `FormatResponse`), and the crate-route request/response models — the shared `MthdsFileItem` / `CrateRequestBase` / `CrateInvalidReport` vocabulary plus the `/v1/resolve`, `/v1/codegen` and `/v1/pipe-io` shapes built on it, `GeneratedArtifact` among them.
114
+ - `models.ts` — Dict-serialized concretes (`DictStuff` / `DictWorkingMemory` / `DictPipeOutput` / `DictRunResultExecute`), the `/v1/validate` surface (`PipelexValidationResult`, whose two arms are declared whole rather than as narrowings of the standard's `ValidationResult`, `ValidationErrorItem`, and the `SuggestedFix` / `FixOp` repair vocabulary its items carry — the hosted plane's shapes, which predate the standard's problem items, their operation and safety vocabularies pinned to the standard's by a type-level test, see [errors.md](./errors.md#a-validation-item--validationerroritem)), the tools surface (`Diagnostic` / `LintResponse` / `FormatResponse`), and the crate-route request/response models — the shared `CrateRequestBase` / `CrateInvalidReport` vocabulary plus the `/v1/resolve`, `/v1/codegen` and `/v1/pipe-io` shapes built on it, `GeneratedArtifact` among them.
94
115
  - `product-models.ts` — the snake_case wire shapes for the hosted-product management routes (profile, methods catalog, organizations, billing, API keys, onboarding, storage, runs list/detail/update). Each interface models only the fields the product actually consumes — no speculative mirrors.
95
116
  - `error-models.ts` — the error-report wire models: `RunErrorReport`, the runner's `ErrorReport` typed with every field it carries (with `UserAction`, `ProviderErrorMetadata` and `MigrationErrorBlock`), which is the one type for a failed run's report on `RunPublic.error`, `PipelineRun.error`, `RunHistoryItem.error`, the failed arm of `RunResultState` and `RunFailedError.error`; and `ProblemDetails` / `FieldError`, the typed members of a problem document that `ApiResponseError` exposes. Every field is optional and every interface keeps an index signature, because the runner owns the shape and adds fields without asking. It also holds the one function that checks a stored report, the internal `readRunErrorReport`, which every read handing a report back applies: each named field is kept only when it has its declared type, and members the SDK does not name are relayed as sent. It imports the `ValidationErrorItem` type and nothing else. See [errors.md](./errors.md#a-failed-run--runerrorreport).
96
- - `bundle-resources.ts` — the files a bundle's steps read, sent as the request's `resources`: the `BundleResources` and `BundleResourceBytes` types the barrel exports, the runner's path rule (`bundleResourcePathRefusal`) and canonical spelling, and `encodeBundleResources`, which checks every key before it reads any value and turns the bytes into standard base64 through `upload.ts`'s `bytesToBase64`. Internal apart from its types; the hook's bundle gatherer reuses its path rule. See [bundle-resources.md](./bundle-resources.md).
97
- - `method-trees.ts` — a saved method's tree on the client side: `MethodTreeSource`, the shape a save reads a tree from, which the `mthds` package's `MethodTree` satisfies; the choice between sending a tree inline and by hash (`INLINE_TREE_MAX_BYTES`), the wire types of `POST /v1/blobs/missing` and the upload of a blob the platform lacks; `redactSignedLinks`, which every error built from an answer holding signed links reads its body through; the tree of an in-memory map; the tree limits a version handshake announces; and the Node-only write of a saved listing to disk, whose Node modules it loads when called. Its tree-sending half mirrors the `mthds` package's own helpers, which the `mthds/protocol` subpath does not export yet, so it holds the same rules with Web Crypto's SHA-256 and a Base64 writer of its own. See [method-trees.md](./method-trees.md).
98
- - `codegen-check.ts` — `runCodegenCheck`, the **pure** offline drift check over a committed codegen tree: the stamp grammar, the `codegen.lock` parser, the hash, and the drift taxonomy, ported from pipelex's `codegen/check.py` so a verdict computed here equals the one `pipelex codegen check` computes over the same bytes (the drift `detail` sentences included, verbatim). It touches neither the client nor `mthds/protocol` — its only runtime imports are `smol-toml` and the plain-object guard of the pure `error-models.ts` — and it hashes through WebCrypto rather than `node:crypto` so it adds no Node builtin to the barrel's import graph (`upload.ts` still names `node:fs/promises`, so a browser-targeting bundler must mark `node:*` external). It raises `CodegenLockError`, which extends `Error` rather than the protocol base and declares the error verdict itself (`input`, not retryable). No filesystem, no fetch, no API key — the caller walks its own tree and hands in the text. See [crate-routes.md](./crate-routes.md#the-offline-check--runcodegencheck).
117
+ - `method-trees.ts` — a saved method's tree on the client side: `MethodTreeSource`, the shape a save reads a tree from, which the `mthds` package's `MethodTree` satisfies; the choice between sending a tree inline and by hash (`INLINE_TREE_MAX_BYTES`), the wire types of `POST /v1/blobs/missing` and the upload of a blob the platform lacks; `redactSignedLinks`, which every error built from an answer holding signed links reads its body through; the tree of an in-memory map; the tree limits a version handshake announces; the listing of a `directory`, by the `mthds` package's lister, which it gets from the Node host only when a directory is given; and the Node-only write of a saved listing to disk, with the file system it gets from the Node host when called. Outside Node, both are refused with `RequestArgumentError`. Its tree-sending half mirrors the `mthds` package's own helpers, which the `mthds/protocol` subpath does not export yet, so it holds the same rules with Web Crypto's SHA-256 and a Base64 writer of its own. See [method-trees.md](./method-trees.md).
118
+ - `codegen-check.ts` — `runCodegenCheck`, the **pure** offline drift check over a committed codegen tree: the stamp grammar, the `codegen.lock` parser, the hash, and the drift taxonomy, ported from pipelex's `codegen/check.py` so a verdict computed here equals the one `pipelex codegen check` computes over the same bytes (the drift `detail` sentences included, verbatim). It touches neither the client nor `mthds/protocol` — its only runtime imports are `smol-toml` and the plain-object guard of the pure `error-models.ts` — and it hashes through WebCrypto rather than `node:crypto` so it adds no Node builtin to the barrel's import graph. It raises `CodegenLockError`, which extends `Error` rather than the protocol base and declares the error verdict itself (`input`, not retryable). No filesystem, no fetch, no API key — the caller walks its own tree and hands in the text. See [crate-routes.md](./crate-routes.md#the-offline-check--runcodegencheck).
99
119
  - `upload-grant.ts` — `uploadWithGrant`, the sender of an upload grant (`requestUploadGrant`, `POST /v1/upload/grant`): the file as the raw body of a `PUT` to the grant's URL with its signed headers unchanged, redirects refused, the whole exchange bounded by a time limit scaled by the file's size unless the caller passes `timeoutMs`, only the first 16 KiB of storage's error body read, and storage's refusals mapped onto the input-preparation errors (`RejectedAssetError` with a `code` for a `4xx` such as the `412` of a used grant or the `403` of a signature mismatch, `UploadTransportError` with a `code` for the time limit running out, a `5xx`, storage's `RequestTimeout`, a `409` conflict, a redirect or no response). It is the browser-safe `@pipelex/sdk/upload` entry, so it imports the error classes, the timer bound in `timers.ts` and the wire types only, never the client. See [input-preparation.md](./input-preparation.md#uploading-with-a-grant--requestuploadgrant--uploadwithgrant).
100
- - `artifacts.ts` — the artifact stack, the download twin of `prepareInputs`, in layers: `locateArtifacts` (a pure, exported walk for `pipelex-storage://` references, each with every `$`-rooted path it sits at) and `collectArtifacts` (the same walk's bare references), `resolveArtifacts` (the bulk resolve route over a whole list, chunked at its bound), `fetchArtifact` (one reference as a bounded `Response`) and `downloadArtifacts` (Node-only, a run's produced files saved under a directory as a produced verdict), plus `artifactFilename`, the rule that names a saved file after the field it fills. The three network operations are client methods over a small structural seam (`ArtifactCapableClient`: the raw `resolveStorageUrls` call and `getRunResult`); the module's static import graph stays free of Node builtins, because `node:fs/promises`, `node:path` and `undici` are loaded by dynamic import behind `isNodeRuntime()` — a browser-targeting bundler still has to mark `node:*` and `undici` external, as it already must for `upload.ts`. `undici` is a runtime dependency for one reason: the fetch runs on an `Agent` carrying `headersTimeout` and `bodyTimeout`, the per-stall bounds an `AbortSignal` budget does not give, and Node exposes its bundled undici's `fetch` but not its `Agent`. See [artifact-download.md](./artifact-download.md).
120
+ - `artifacts.ts` — the artifact stack, the download twin of `prepareInputs`, in layers: `locateArtifacts` (a pure, exported walk for `pipelex-storage://` references, each with every `$`-rooted path it sits at) and `collectArtifacts` (the same walk's bare references), `resolveArtifacts` (the bulk resolve route over a whole list, chunked at its bound), `fetchArtifact` (one reference as a bounded `Response`) and `downloadArtifacts` (Node-only, a run's produced files saved under a directory as a produced verdict), plus `artifactFilename`, the rule that names a saved file after the field it fills. The three network operations are client methods over a small structural seam (`ArtifactCapableClient`: the raw `resolveStorageUrls` call and `getRunResult`); the module's graph, static and dynamic, stays free of Node builtins, because `node:fs/promises`, `node:path` and `undici` come from the Node host. `undici` is a runtime dependency for one reason: the fetch runs on an `Agent` carrying `headersTimeout` and `bodyTimeout`, the per-stall bounds an `AbortSignal` budget does not give, and Node exposes its bundled undici's `fetch` but not its `Agent`. See [artifact-download.md](./artifact-download.md).
101
121
  - `runs.ts` — the run-lifecycle types (`RunStatus`, `RunRead`, `RunResults`, `RunResultState`) and the single poll loop (`pollUntilResult`). `RunResults.main_stuff` (the resolved main output content) is always present for a completed run: on the hosted path it is the `main_stuff.json` S3 artifact; on the bare-runner blocking path the SDK resolves it from the returned working memory via the response's `main_stuff_name`, so both paths deliver the same shape. Consumers read `main_stuff` directly. `working_memory` — every named stuff of the run, `{ root, aliases }`, typed `DictWorkingMemory` from `models.ts` — reads the same on both paths: the hosted path relays the `working_memory.json` artifact, and the blocking path lifts it off `pipe_output`, which still rides whole (blocking path only). `graph_spec` — the executed graph — also reaches the client on both paths: the hosted path relays the `graphspec.json` artifact, and the blocking path lifts it off `pipe_output` beside `graph_assembly_error`, the graph's twin of `usage_assembly_error`. That error field is declared ahead of the wire on the hosted side, where the platform's results body relays no such key and the field is therefore absent, reading `undefined` rather than `null`. A completed run that cannot deliver a main stuff throws `MissingMainStuffError` when the read asked for it. `runs.ts` also declares `RUN_RESULT_ARTIFACTS` / `RunResultArtifact`, the artifacts a results read can be narrowed to with `getRunResult`'s `artifacts` option; with a selection an unselected artifact is absent while a selected unwritten one is `null`. `runs.ts` also owns `TokensUsageRecord` — one client-facing usage record per inference call, carried by `RunResults.tokens_usages` on both paths. That is a mirror of a wire contract this SDK does not own (inference accounting is a Pipelex runtime extension rather than part of the MTHDS standard): every field is optional and the interface carries an index signature so pre-contract artifacts (relayed verbatim, never migrated) still type-check. See [`run-usage.md`](./run-usage.md) for the field reference, the cost/null semantics, and the old-artifact rules, and [`run-results.md`](./run-results.md) for the field-by-field account of `RunResults` — the durable handle, the output and how to read it, the run's working memory, the executed graph and how to render or keep it, and the produced files with their short-lived links.
102
122
  - `usage.ts` — `summarizeUsage`, the **pure** fold of a run's usage pair (`tokens_usages` / `usage_assembly_error`) into one `UsageSummary`: a `state` (`records`, `no_inference` or `unavailable`), the null-aware total cost with its `cost_partial` flag, the `input` and `output` token totals, the call count, the relayed assembly error, and a `by_pipe` rollup sorted by cost with the unattributed calls grouped under a `null` `pipe_code`. It applies the rules [`run-usage.md`](./run-usage.md#summarizing-a-run--summarizeusage) states, so that every consumer reads one computation rather than re-deriving those rules by hand. It imports types from `runs.ts` and nothing else: no client, no fetch, no `mthds/protocol`.
103
123
  - `execute-result.ts` — `PipelexExecuteResult`, a `DictRunResultExecute` subtype that adds a resolved `.main_stuff` accessor (dug out of `pipe_output`'s working memory via the response's `main_stuff_name`, throwing `MissingMainStuffError` if unlocatable). `execute()` returns it, giving blocking and durable results the **same output accessor** — `result.main_stuff` — so callers never branch on which path ran. The neutral `mthds` wire model leaves `main_stuff_name` in its extension index signature; this Pipelex-branded subtype declares it as a typed field (Pipelex owns that concept). `resultsFromExecute` (the public lift from that result onto `RunResults`, in the same module) reads that accessor too, keeping the resolution single-sourced; the `executeBlocking` fallback is its only caller inside the SDK, and a consumer driving `execute()` directly calls it by name.
@@ -107,20 +127,21 @@ Flat `src/` (mirrors `mthds-js`'s `runners/api` flatness — the SDK has one cli
107
127
  - `user-agent.ts` — client identification per the spec `conformance/specs/client-identification.md`, in the `conformance` repository beside the specs' tests: the exported `AppInfo` option, its RFC 9110 token validation (a `TypeError` on a bad field, a `RangeError` past the 512-character ceiling), its rendering, the runtime detection (Node, Bun, Deno, or a browser, where no header is set) and `buildUserAgent`, which the client calls once at construction. The barrel re-exports `buildUserAgent`, `validateAppInfo`, `MAX_USER_AGENT_LENGTH` and the `RuntimeInfo` type, so a consumer's raw fetches send the same value. The client's private `requestHeaders` is the one place API request headers are built, so every route carries the value; the artifact fetch of a presigned link never goes through it. See [client-identification.md](./client-identification.md).
108
128
  - `exact-json.ts` — `parseExactJson` and `stringifyExactJson`, JSON whose integers stay exact, and `ExactJsonUnsupportedError`. The reader, which the client applies to every answer carrying a person's data (a method example, a run's record, a run's results, the blocking execute's answer), is one `JSON.parse` call whose reviver turns a number past `Number.MAX_SAFE_INTEGER` spelled as an integer into a `bigint` of its source text (`context.source`), so it reads everything else exactly as `JSON.parse` does, with no second parser to keep in step; a text holding no run of 16 digits is parsed with no reviver at all. On a runtime whose reviver gets no source text, it refuses a whole number past the safe range with `ExactJsonUnsupportedError`, which the client reports as an answer it cannot read, rather than rounding it. The writer, which `requestRaw` applies to every request body before the request is made, is one `JSON.stringify` call whose replacer turns each `bigint` into `JSON.rawJSON` text, so every getter and `toJSON` in the body runs once, and a `bigint` an inherited `toJSON` such as the `BigInt.prototype.toJSON` polyfill already turned into a string is looked up in its holder and written as its digits; a runtime without `JSON.rawJSON` refuses the `bigint`, as a `RequestArgumentError` from the client, rather than rounding it. The barrel exports all three, for a caller showing such data as JSON text; the error declares its verdict itself, `config` and not retryable, as `CodegenLockError` does. It imports nothing but the `ErrorDomain` type. See [exact-integers.md](./exact-integers.md).
109
129
  - `timers.ts` — `MAX_TIMER_DELAY_MS`, the longest delay `setTimeout` honours, and `isTimerDelay`, which every caller-supplied `timeoutMs` is checked against before it reaches a timer: a longer delay overflows and fires at once. It imports nothing, so the browser-safe upload entry uses it too.
130
+ - `node-host.ts` and `node-host/` — the Node host ([above](#the-node-host)): `loadNodeHost()` and `isNodeRuntime()` in `node-host.ts`, the Node variant `node-host/node.ts`, the only module of the barrel's graph that imports a Node built-in, `undici` or the `mthds` root entry, and `node-host/unavailable.ts`, the variant every other runtime resolves.
110
131
  - `version.ts` — `SDK_VERSION`, in its own module so the client stamps it on the `User-Agent` without importing the barrel, which re-exports it.
111
132
  - `cli.ts` and `cli/` — the `pipelex-sdk` command, `run` and `script` ([`cli.md`](cli.md)): `cli.ts` is the executable `package.json` declares as the package's one `bin`, handing the process to `cli/main.ts`'s `runCommand`, and `cli/` holds the flags, the bundle reader, the inputs, the environment, the two subcommands and what they print. It sits at the TOP of the graph, the other way round from every other module: nothing imports it, the barrel does not export it, and it reaches the SDK only through `index.ts`, which `dependency-cruiser` enforces in both directions.
112
133
  - `index.ts` — the public barrel: `export * from "mthds/protocol"` (single import source for the standard surface) plus the client, models, run types, the standalone pure helpers (`runCodegenCheck`, `summarizeUsage`, `locateArtifacts`, `collectArtifacts`, `artifactFilename`, `parseExactJson`, `stringifyExactJson`), and errors.
113
134
 
114
135
  ## Client surface (current)
115
136
 
116
- - **Protocol execution:** `execute` (returns a `PipelexExecuteResult` with a resolved `.main_stuff`), `start`, `validate` (returns `PipelexValidationResult`), `validateFiles`, `models`, `version`.
137
+ - **Protocol execution:** `execute` (returns a `PipelexExecuteResult` with a resolved `.main_stuff`), `start`, `validate` (returns `PipelexValidationResult`), `models`, `version`.
117
138
  - **Two independent opt-in lists on `validate`.** `render` asks the server to render a *view format* and adds a `rendered_<format>` field; `views` asks for a *structured view* and adds a same-named top-level field — today `input_form` and `output_form`, the per-pipe form descriptors. A caller that wants the forms without the dry-run verdict reads `pipeIo` instead, which derives them with no dry run. Each resolves its own tokens against its own supported set and unknown tokens are lenient-ignored on both, never a `422`. The client always asks for `render: ["markdown"]` but sends `views` only when a caller names a token, because an opt-in view exists precisely so the default response stays byte-identical for the consumers that discard it. `pipelex-api` gates `input_form` on the token as of 0.18.0; a 0.17.0 runner resolved no token and emitted the field on every valid verdict, which is why `PipelexValidationReport.input_form` is typed optional rather than required — the one typing that reads a response from either runner correctly.
118
- - **Method-bundle transport (pipelex-api extension):** `execute` / `start` / the blocking fallback accept the whole method — the `.mthds` plus its `funcs/*.py`, `structures/*.py`, `requirements.txt` — instead of only the inline `mthds_contents` text, so custom PipeFunc Python travels with the method. Two equivalent, mutually exclusive encodings: `files` (a `{ relativePath: text }` map) and `bundle_b64` (the same bundle as a base64 zip). A bundle is self-contained, so it satisfies the "something to run" precondition on its own — neither `pipe_code` nor `mthds_contents` is required beside it. See [run-sources](#run-sources) above.
139
+ - **The method's tree:** `execute` / `start` / `startAndWaitForResult` (both paths) and `validate` take the whole method as its tree — every `.mthds` bundle, its `funcs/*.py`, `structures/*.py` and `requirements.txt`, and every file a step reads, such as a `PipeDocGen` step's template — in one of four forms, `directory`, `contents`, `tree` or `files`, sent inline or by hash. `startAndWaitForResult` prepares the tree once, under its `signal`, before it starts the run, so its fallback to the blocking execute sends the same files without reading or uploading them again. A tree is self-contained, so it satisfies the "something to run" precondition on its own — `pipe_code` is not required beside it. See [run-sources](#run-sources) above and [method-trees.md](./method-trees.md).
119
140
  - **Method selectors on the run methods:** `execute` / `start` / `startAndWaitForResult` take the Pipelex API's `method_ref` (an address the runner resolves; provenance comes back typed as `method_provenance`) and the platform's `method_id` as **typed named options** (`PipelexRunOptions` / `PipelexStartOptions`), never `extra` entries. See [Pipelex API run extensions](#pipelex-api-run-extensions-method_ref) and [hosted run extensions](#hosted-run-extensions-method_id) above.
120
141
  - **An idempotency key on a run start (hosted):** `start` and `startAndWaitForResult` take `idempotencyKey` (`PipelexStartRequestOptions`, intersected into `PipelexStartOptions`), sent as the `Idempotency-Key` header of `POST /v1/start` and never in the body; with no key there is no header. The hosted API binds the key to the run, answers a repeat of the same start with that run, and refuses the key with other options; a bare runner ignores the header. The client refuses a key that is not 1 to 255 visible ASCII characters with a `RequestArgumentError` before any request, the blocking `execute` fallback of `startAndWaitForResult` sends none, and the key leaves the error verdict as it is: a start that may have reached the server stays `runMayHaveStarted`, and the caller who sent a key recovers the run by sending the same start again under it. [errors.md](./errors.md#a-starts-idempotency-key--idempotencykey) holds the contract.
121
- - **Method selectors on `validate`:** the first argument is inline contents (`string[]`) or a `ValidateMethodSelector` — `{ method_ref }` (runner-resolved by address) or `{ method_id }` (hosted, platform-resolved) — under the strict tooling XOR: exactly one selector, no linkage exception, `mthds_sources` legal only beside inline contents. A selector-resolution failure is a non-2xx `ApiResponseError`, never an `is_valid: false` verdict.
122
- - **The files a bundle's steps read (`resources`, pipelex-api extension):** `validate` / `validateFiles` (in their last options argument), `execute` / `start` / `startAndWaitForResult` (both paths), the crate routes and `prepareInputs` (beside inline `files`) take `resources`, the files a `PipeDocGen` step's `template_file` names, keyed by the path the step names, each as its bytes (`Uint8Array`, `ArrayBuffer` or `Blob`), and send them as the wire's base64 map. `bundle-resources.ts` encodes them and refuses, before any request, a key the runner's path rule refuses and two keys for one file; the run routes and `validate` also mirror the runner's pairing refusals (beside a `method_ref`, or with no source on a run), while the crate routes leave theirs to the server, as they do the selector XOR. `resources` is reserved on `extra`, so it cannot bypass those checks. The valid verdict's `warnings` carry the template check's `template_check_warning` items, whose `template_file` and `template_location` `ValidationErrorItem` declares ahead of `mthds`, a difference the type-level pin states. See [bundle-resources.md](./bundle-resources.md).
123
- - **Crate routes (`/v1/resolve`, `/v1/codegen`, `/v1/pipe-io`):** `resolve` emits the **normalized library crate** (the MTHDS standard's Library Crate Format, fingerprint riding inside the payload); `codegen` projects that crate through two explicit axes — `kind` (`types`) × `target` (`ts-zod` / `python-pydantic` / `python-structures`) — into stamped artifacts plus their `codegen.lock`. All three share one closure envelope (`files: [{content, source?}]` XOR a `method_ref`, the address form server-resolved and the registry form a `501`) and return a **discriminated 200 verdict** (`is_valid`, with `CrateInvalidReport` as the shared invalid arm) rather than a bare payload, the same discipline as `validate`. They also take the hosted `method_id` (`PipelexHostedToolingExtensions`) as a third closure form under the strict three-way XOR — a pass-through the platform resolves, meaningless against a bare runner. Writing the artifacts and lock verbatim reproduces a local `pipelex codegen types` run byte-for-byte, so the offline `pipelex codegen check` passes — the SDK stays transport-only and does not write files. Verifying such a tree afterwards is `runCodegenCheck`, a standalone pure helper rather than a client method, because it needs no server at all. `pipeIo` derives a method's three I/O artifacts — the standard's `PipeIOContracts`, `InputForm` and `OutputForm`, imported from `mthds/protocol` — for the pipe the route selects (the request's qualified `pipe_ref`, else the method's own entry pipe) or for every pipe under `all_pipes`, with the runnability facts beside them and the closure's files echoed on `include_files`, and it runs no dry run; a selection it refuses is a `422` typed `EntryPipeNotFoundError` or `EntryPipeAmbiguousError`. See [crate-routes.md](./crate-routes.md).
142
+ - **Method selectors on `validate`:** the first argument is a `PipelexValidateSource` — the method's tree in one of its four forms, `{ method_ref }` (runner-resolved by address) or `{ method_id }` (hosted, platform-resolved) — under the strict tooling XOR: exactly one, no linkage exception, refused with a `RequestArgumentError` before any request. Its options (`ValidateOptions`: `render`, `views`, `timeoutMs`, `signal`) ride the third argument. A selector-resolution failure is a non-2xx `ApiResponseError`, never an `is_valid: false` verdict.
143
+ - **The files a step reads ride the tree.** A `PipeDocGen` step's `template_file` is a file of the method's tree like any other, at the tree path the step names; the separate `resources` field is gone from every route. The valid verdict's `warnings` carry the template check's `template_check_warning` items, whose `template_file` and `template_location` `ValidationErrorItem` declares ahead of `mthds`, a difference the type-level pin states, and its optional `tree_files_read` and `tree_file_readers` name the files of the method's tree each step reads, whatever form the method was sent in ([which step reads which file](./method-trees.md#which-step-reads-which-file)). See [method-trees.md](./method-trees.md#the-files-a-step-reads).
144
+ - **Crate routes (`/v1/resolve`, `/v1/codegen`, `/v1/pipe-io`):** `resolve` emits the **normalized library crate** (the MTHDS standard's Library Crate Format, fingerprint riding inside the payload); `codegen` projects that crate through two explicit axes — `kind` (`types`) × `target` (`ts-zod` / `python-pydantic` / `python-structures`) — into stamped artifacts plus their `codegen.lock`. All three share one closure envelope (the method's tree, in one of the four forms a run takes, XOR a `method_ref`, the address form server-resolved and the registry form a `501`) and return a **discriminated 200 verdict** (`is_valid`, with `CrateInvalidReport` as the shared invalid arm) rather than a bare payload, the same discipline as `validate`. They also take the hosted `method_id` (`PipelexHostedToolingExtensions`) as a third closure form under the strict three-way XOR — a pass-through the platform resolves, meaningless against a bare runner. Writing the artifacts and lock verbatim reproduces a local `pipelex codegen types` run byte-for-byte, so the offline `pipelex codegen check` passes — the SDK stays transport-only and does not write files. Verifying such a tree afterwards is `runCodegenCheck`, a standalone pure helper rather than a client method, because it needs no server at all. `pipeIo` derives a method's three I/O artifacts — the standard's `PipeIOContracts`, `InputForm` and `OutputForm`, imported from `mthds/protocol` — for the pipe the route selects (the request's qualified `pipe_ref`, else the method's own entry pipe) or for every pipe under `all_pipes`, with the runnability facts beside them and the closure's files echoed on `include_files`, and it runs no dry run; a selection it refuses is a `422` typed `EntryPipeNotFoundError` or `EntryPipeAmbiguousError`. See [crate-routes.md](./crate-routes.md).
124
145
  - **Model reference check (`/v1/models/check`):** `checkModelReference(reference, category?)`, a Pipelex API extension served by any `pipelex-api` runner from pipelex 0.78.0 and on the hosted API, whose platform relays it byte-identically. It is a **diagnostic endpoint** like `validate`: a reference that resolves nowhere is a `200` verdict with `resolution: "not_found"` and the names the caller may have meant (`suggestions`, `other_kinds`, `other_categories`), never a thrown error. Only a request that cannot produce a verdict throws `ApiResponseError`, a `422` typed by its `errorType`: `InvalidModelReference` for a reference that is blank, a sigil or a namespace alone, or past the runner's length limit, `InvalidModelCategory` for an unknown `type`, `ValidationError` for a missing or repeated parameter. The SDK validates none of it, so the runner's rule is the only one. `ModelReferenceVerdict` is a union of one arm per reference kind, discriminated on the wire's own `kind` (`PresetReferenceVerdict`, `AliasReferenceVerdict`, `WaterfallReferenceVerdict`, `HandleReferenceVerdict`): a `matches` entry carries no kind of its own, but every entry of one verdict shares the verdict's, so narrowing on `kind` types `matches` as `PresetMatch[]`, `AliasMatch[]`, `WaterfallMatch[]` or `HandleMatch[]` without a field the wire does not carry. `ModelCheckCategory` is the protocol's model categories plus `doc_gen`, the family of `PipeDocGen`, which the protocol and so `models` leave out but a method's `model` field can name. `resolution` is typed open, `ModelReferenceResolution | (string & {})`, under the spec's reader rule that a client reading a value it does not know treats the reference as unresolved, and so is every category a verdict names, `ModelCheckCategory | (string & {})` as `mthds` types `ModelInfo.type`, since a runner of a later version may add one; the `category` a request names stays closed. A match's `resolves_to` is `null` when the reference resolves by name but a run through it would reach no model the runner can call, which a consumer shows as a warning rather than as an unknown name.
125
146
  - **Tools (`/v1/lint`, `/v1/format`):** `lint`, `format` — single-file static diagnostics and canonical formatting, served by any pipelex-api runner (no inference, no bundle load). Both are **diagnostic endpoints** like `validate`: a malformed `.mthds` file is a produced verdict on a **200** carrying `diagnostics[]`, never a thrown error — `format` additionally echoes the content back unchanged with `changed: false`. Only a no-verdict condition is non-2xx: a malformed body, **malformed formatter `options`** (e.g. a non-numeric `column_width` ⇒ 422), auth, or a server fault, all mapped to the typed `ApiResponseError`. `lint` is the cheap static check (syntax / schema / semantic); it does not replace `validate`, which loads the bundle, resolves across files, and dry-runs the pipes.
126
147
  - **Durable run lifecycle (hosted extension — NOT protocol):** `getRunStatus`, `getRunProgress` (`GET /v1/runs/{id}/progress`, the run's progress snapshot, see [run-results.md](./run-results.md#following-a-run-while-it-runs--onprogress-and-getrunprogress)), `getRunResult` (with an optional `artifacts` selection sent as `?artifacts=`, see [run-results.md](./run-results.md#reading-only-some-artifacts--artifacts)), `waitForResult` (whose `onProgress` reads the progress route on each poll that finds the run running and delivers each fresher snapshot by `isFresherRunProgress`, a failed progress read never failing the wait), `startAndWaitForResult` (handshakes `/v1/version`, takes the durable start+poll path on a hosted deployment, and self-heals to the blocking `execute` against a bare runner). The handshake's outcome is cached for the client's lifetime, under the rule the Python SDK follows too: an answer that is not a usable version assumes hosted and lets the start surface the real error, while no answer at all throws its `ApiUnreachableError` at once, caches nothing and sends no start. An answer is anything the server sent back: an error status, a body that is not JSON, not UTF-8 or no version, and a body that arrived but could not be decoded, such as a broken gzip stream, which fetch fails with the decompressor's code (`Z_DATA_ERROR` and the other zlib codes, brotli's `ERR__ERROR_…`, zstd's `ZSTD_error_…`). A run that ended without completing answers the results read with a `409` whose `run_status` and `error` members carry its status and its stored error report; `getRunResult` turns it into the `failed` arm (`status`, `message` from `detail`, `error`) and the waiting calls on the durable path into a `RunFailedError` carrying the same (the blocking fallback's failure is the runner's `ApiResponseError`), the status read from the member, or from the `detail` sentence on a platform that does not send it yet. The status read serves the same report as `RunRead.error`. See [errors.md](./errors.md#a-failed-run--runerrorreport). The status read also carries `degraded` and the `degradation` that explains it, whose `reason` tells a status the platform could not refresh (`status_unavailable`) from a run whose workflow task keeps failing until the platform ends it (`workflow_task_failing`), checked field by field so it never fails the poll, and the run's `request_id`, the id to quote to support. See [run-results.md](./run-results.md#a-degraded-status--degraded-and-degradation). Reading what a completed run consumed is `summarizeUsage(results)`, a standalone pure helper rather than a client method, because it folds the usage pair the results already carry and needs no server. See [run-usage.md](./run-usage.md#summarizing-a-run--summarizeusage).
@@ -131,18 +152,20 @@ Flat `src/` (mirrors `mthds-js`'s `runners/api` flatness — the SDK has one cli
131
152
  The hosted catalog/account routes the webapp drives. All ride the same `{base}/v1/*` + `Authorization: Bearer` + org-from-JWT contract as the protocol routes, go through the shared `requestProduct<T>` helper (30s management timeout, empty-body tolerant), and map a non-2xx RFC 9457 `problem+json` to a typed `ApiResponseError`. Every route parses a 2xx body through one private helper, so an answer that is not JSON (a gateway's HTML page, an empty body where one was due) is an `ApiResponseError` carrying its status and raw text, `runtime` and not retryable, rather than a bare `SyntaxError`. A product route whose answer the SDK or its caller reads before anything else (`listMethods`, `listRuns`, `getRunDetail`, `upload`, `requestUploadGrant`, `resolveStorageUrls`, and the methods routes answering one stored method) goes through `requestProductObject` or a helper built on the same read instead, which also refuses an empty body and JSON that is not an object; the two paged routes then refuse, the same way, through one page reader, a page whose `items` is not an array or whose `next_cursor` is neither a string nor `null`, `upload` an answer with no non-empty string `uri`, and `getMethod` and `getMethodVersion` a `files` member that is not a listing of files, each with its string `path`, `sha256`, `url` and `expires_at` and a non-negative integer `size`. Consumers branch on **`type` and `errorDomain`**, as the hosted-envelope spec (`conformance/specs/pipelex-hosted-envelope.md`, in the `conformance` repository) says, never on the HTTP status. The platform's `type` is `https://pipelex.com/errors/<code>`, one-to-one with its native `code` (`conflict`, `not_found`, `pipelex_api_key_limit_reached`, …), the finer surface-specific code, on which a client of the platform alone may branch as well: the two read the same answer, and a refusal the platform relays from the runner carries the runner's `type` and `errorType` and no `code`. The SDK reads the `error_domain` and `retryable` the server sent first, and where the platform sends none on a refusal it renders itself, as its Lambda-backed routes do not yet, `errorDomain` and `retryable` are the SDK's fallback, read from the status; every error carries both, decided. See [errors.md](./errors.md).
132
153
 
133
154
  - **User profile:** `getMe` (`GET /v1/me`).
134
- - **Methods catalog:** `listMethods`, `getMethod`, `createMethod`, `writeDraft`, `renameMethod`, `publishMethod`, `listMethodVersions`, `getMethodVersion`, `deleteMethod`. **A saved method has a draft and immutable published versions.** Its draft is a method tree with its `input_data`, which `writeDraft` (`PUT /v1/methods/{id}/draft`) replaces whole, without validating it; `updated_at` is the draft's token, which moves on every draft write and nothing else, and `draft_digest` the draft's tree digest (`sha256:` and 64 hex digits), `null` for an empty draft. A save takes the tree as `files`, sent as given, or as `tree` or `contents`, sent inline or by hash after uploading the blobs the organization lacks, and a read lists the tree's files, each with a short-lived link to its bytes, which `writeMethodTree` writes to disk ([method-trees.md](./method-trees.md)). A draft write may carry `expected_updated_at`, the token the caller last saw, and is then a compare-and-swap: a draft that moved since is a `409` `method_update_conflict` and nothing is written. `renameMethod` (`PATCH /v1/methods/{id}`) changes the name alone and moves no token. `publishMethod` (`POST /v1/methods/{id}/publish`) takes the required `expected_draft_updated_at` and answers a `MethodPublishResult` discriminated on `outcome` — `published` with the new version, `unchanged` with the existing latest version when the draft's digest equals it, `refused` with a `reason` (`invalid` or `not_runnable`), a `message` and the runner's `validation` verdict — every arm carrying the `method`; the SDK reads the nested method as `getMethod` does, and an answer whose `outcome` is none of these is an answer it cannot read. A publish that produced no verdict is thrown rather than answered: only a draft that differs from the latest version reaches the runner, and a runner that cannot be reached, answers unusably or does not finish within the platform's deadline is a `502` or `503` with nothing written, so a retry is safe, and a retry of a publish that landed while its answer was lost answers `unchanged` with the version it wrote. A method is stored as one item that keeps room for the summary a publish adds, so a draft that saves can always be published: a creation or a draft write is a `413` `payload_too_large` when it would grow the method past that room, below the store's item limit, and a rename is the same `413` for a name so long it would leave the method too large to publish. A version is numbered from 1, never reused, and holds the draft's tree as it stood, its `tree_digest` (`null` for a version stored before methods were trees), its `crate_fingerprint` (`null` when the runner could not resolve the bundle in memory), the runner's version, and when and by whom it was published; `listMethodVersions` pages the summaries newest first (`MethodVersionPage`, `{items, nextCursor}`, a `limit` from 1 to 100, 20 by default, and a `422` outside that range), and `getMethodVersion` reads one with its `files` listed as a method's are, or answers a `409` `conflict`, whose verdict is `runtime`, for a version stored before methods were trees and never rewritten as one. A method read carries `latest_version` and `latest_published`, both `null` until the first publish, so the publish state is derived: never published, draft unchanged (`draft_digest` not `null` and equal to `latest_published.tree_digest`), or draft ahead. The method routes take a bare id and address the method itself, never one of its versions; the platform does not parse a suffix there, so `mt_x@3` would answer a `404` that reads as a method that does not exist, and each route refuses a suffixed id with `RequestArgumentError` before anything is sent, naming `parseMethodSelector` and `getMethodVersion`. The whole-method `updateMethod` is gone, replaced by the draft write and the rename. **`deleteMethod` is asynchronous and its return type says so:** `DELETE /v1/methods/{id}` answers `202` with a `MethodDeletionAccepted` (`method_id`, `deletion_state`, `deletion_job_id`) the moment the platform has claimed the method and terminated its in-flight workflows — the rest of the cascade (runs, events, S3 objects) is enqueued. A resolved promise means "accepted", never "gone"; completion is the row disappearing from `listMethods`, not any field of that body. The claim is a conditional write, so a double-clicked delete is a `409 method_being_deleted` rather than a second cascade over the same runs, and an unknown or foreign-org id is a `404`. The erasure deletes the method's versions with the rest. (This doc previously stated the SDK had no `delete` because the platform had no such route — the route has always existed; the claim was simply false.) `listMethods` returns **one page**, not the whole catalog — a `MethodPage` (`{items, nextCursor}`) served from a narrow index projection, so its cost is the page size rather than the size of the catalog. It used to return every method with its full `.mthds` bundle attached, which hit DynamoDB's 1 MB page cap at a couple hundred methods and came back truncated with no error and no flag, so methods silently vanished from the UI. Rows are therefore `MethodSummary`, **not** `MethodDetail`: no `files`, no `updated_at`, because neither is in the projection and putting the source back is what restored the truncation. Reach for `getMethod(id)` when you need a method's files. `query.q` is applied server-side across the whole catalog, not over the page — filtering one page client-side would be searching 50 rows of 10,000 and calling it a search. `query.published` filters the same way, sent as `published=true` (the methods with a published version) or `published=false` (those never published), and combines with `q`. Each row carries the method's publish state, so a list tells its methods apart without reading each one: `latest_version` and `latest_published` (a `MethodVersionSummary`), both `null` when the method was never published, and `draft_digest`, `null` for an empty draft; all three are optional, absent from a platform that does not serve them. Never published is `latest_version === null`, the draft unchanged is a `draft_digest` that is not `null` and equals `latest_published.tree_digest`, and the draft ahead is two digests that differ. Both filters run after the index read, so **a filtered page can come back empty and still carry a `nextCursor`**: an empty page is not the end of the list, and only a `null` cursor is. `iterateMethods` is the async iterator that follows the cursor for callers who genuinely want everything (`for await (const m of client.iterateMethods())`); it keeps going **past empty pages**, since `{items: [], nextCursor: "…"}` means "nothing matched in the slice I just read, keep going". A server that keeps minting fresh cursors forever stops it at a page ceiling far past any real catalog with a `PagingNotTerminatingError` carrying `pageLimit`, never a silently truncated stream; `iterateRuns` shares the same loop. It gives up only when the server stops advancing its cursor, and **throws** rather than returning a partial answer; like `iterateRuns` it is deliberately not a `listAllMethods(): Promise<MethodSummary[]>`, because an all-at-once helper needs a page cap and a cap means silently returning a truncated list. `MethodSummary.deletion_state` rides the list rows: a method mid-erasure stays listed so the UI can render it as "Deleting…", while `getMethod` refuses it with a 409. Full read responses (`MethodDetail`, from `getMethod`) carry `org_id`, `created_by_user_id`, a server-derived `description` (recomputed from the draft's top-level `description` on every draft write), the draft token and digest, the latest version's summary and the draft's `files` on top of the write contract's `name` / `input_data`; the writes and the rename answer `MethodData`, the same method without `files`.
155
+ - **Methods catalog:** `listMethods`, `getMethod`, `createMethod`, `writeDraft`, `renameMethod`, `publishMethod`, `listMethodVersions`, `getMethodVersion`, `deleteMethod`. **A saved method has a draft and immutable published versions.** Its draft is a method tree with its `input_data`, which `writeDraft` (`PUT /v1/methods/{id}/draft`) replaces whole, without validating it; `updated_at` is the draft's token, which moves on every draft write and nothing else, and `draft_digest` the draft's tree digest (`sha256:` and 64 hex digits), `null` for an empty draft. A save takes the tree as `files`, sent as given, or as `directory`, `tree` or `contents`, sent inline or by hash after uploading the blobs the organization lacks, and a read lists the tree's files, each with a short-lived link to its bytes, which `writeMethodTree` writes to disk ([method-trees.md](./method-trees.md)). A draft write may carry `expected_updated_at`, the token the caller last saw, and is then a compare-and-swap: a draft that moved since is a `409` `method_update_conflict` and nothing is written. `renameMethod` (`PATCH /v1/methods/{id}`) changes the name alone and moves no token. `publishMethod` (`POST /v1/methods/{id}/publish`) takes the required `expected_draft_updated_at` and answers a `MethodPublishResult` discriminated on `outcome` — `published` with the new version, `unchanged` with the existing latest version when the draft's digest equals it, `refused` with a `reason` (`invalid` or `not_runnable`), a `message` and the runner's `validation` verdict — every arm carrying the `method`; the SDK reads the nested method as `getMethod` does, and an answer whose `outcome` is none of these is an answer it cannot read. A publish that produced no verdict is thrown rather than answered: only a draft that differs from the latest version reaches the runner, and a runner that cannot be reached, answers unusably or does not finish within the platform's deadline is a `502` or `503` with nothing written, so a retry is safe, and a retry of a publish that landed while its answer was lost answers `unchanged` with the version it wrote. A method is stored as one item that keeps room for the summary a publish adds, so a draft that saves can always be published: a creation or a draft write is a `413` `payload_too_large` when it would grow the method past that room, below the store's item limit, and a rename is the same `413` for a name so long it would leave the method too large to publish. A version is numbered from 1, never reused, and holds the draft's tree as it stood, its `tree_digest` (`null` for a version stored before methods were trees), its `crate_fingerprint` (`null` when the runner could not resolve the bundle in memory), the runner's version, and when and by whom it was published; `listMethodVersions` pages the summaries newest first (`MethodVersionPage`, `{items, nextCursor}`, a `limit` from 1 to 100, 20 by default, and a `422` outside that range), and `getMethodVersion` reads one with its `files` listed as a method's are, or answers a `409` `conflict`, whose verdict is `runtime`, for a version stored before methods were trees and never rewritten as one. A method read carries `latest_version` and `latest_published`, both `null` until the first publish, so the publish state is derived: never published, draft unchanged (`draft_digest` not `null` and equal to `latest_published.tree_digest`), or draft ahead. The method routes take a bare id and address the method itself, never one of its versions; the platform does not parse a suffix there, so `mt_x@3` would answer a `404` that reads as a method that does not exist, and each route refuses a suffixed id with `RequestArgumentError` before anything is sent, naming `parseMethodSelector` and `getMethodVersion`. The whole-method `updateMethod` is gone, replaced by the draft write and the rename. **`deleteMethod` is asynchronous and its return type says so:** `DELETE /v1/methods/{id}` answers `202` with a `MethodDeletionAccepted` (`method_id`, `deletion_state`, `deletion_job_id`) the moment the platform has claimed the method and terminated its in-flight workflows — the rest of the cascade (runs, events, S3 objects) is enqueued. A resolved promise means "accepted", never "gone"; completion is the row disappearing from `listMethods`, not any field of that body. The claim is a conditional write, so a double-clicked delete is a `409 method_being_deleted` rather than a second cascade over the same runs, and an unknown or foreign-org id is a `404`. The erasure deletes the method's versions with the rest. (This doc previously stated the SDK had no `delete` because the platform had no such route — the route has always existed; the claim was simply false.) `listMethods` returns **one page**, not the whole catalog — a `MethodPage` (`{items, nextCursor}`) served from a narrow index projection, so its cost is the page size rather than the size of the catalog. It used to return every method with its full `.mthds` bundle attached, which hit DynamoDB's 1 MB page cap at a couple hundred methods and came back truncated with no error and no flag, so methods silently vanished from the UI. Rows are therefore `MethodSummary`, **not** `MethodDetail`: no `files`, no `updated_at`, because neither is in the projection and putting the source back is what restored the truncation. Reach for `getMethod(id)` when you need a method's files. `query.q` is applied server-side across the whole catalog, not over the page — filtering one page client-side would be searching 50 rows of 10,000 and calling it a search. `query.published` filters the same way, sent as `published=true` (the methods with a published version) or `published=false` (those never published), and combines with `q`. Each row carries the method's publish state, so a list tells its methods apart without reading each one: `latest_version` and `latest_published` (a `MethodVersionSummary`), both `null` when the method was never published, and `draft_digest`, `null` for an empty draft; all three are optional, absent from a platform that does not serve them. Never published is `latest_version === null`, the draft unchanged is a `draft_digest` that is not `null` and equals `latest_published.tree_digest`, and the draft ahead is two digests that differ. Both filters run after the index read, so **a filtered page can come back empty and still carry a `nextCursor`**: an empty page is not the end of the list, and only a `null` cursor is. `iterateMethods` is the async iterator that follows the cursor for callers who genuinely want everything (`for await (const m of client.iterateMethods())`); it keeps going **past empty pages**, since `{items: [], nextCursor: "…"}` means "nothing matched in the slice I just read, keep going". A server that keeps minting fresh cursors forever stops it at a page ceiling far past any real catalog with a `PagingNotTerminatingError` carrying `pageLimit`, never a silently truncated stream; `iterateRuns` shares the same loop. It gives up only when the server stops advancing its cursor, and **throws** rather than returning a partial answer; like `iterateRuns` it is deliberately not a `listAllMethods(): Promise<MethodSummary[]>`, because an all-at-once helper needs a page cap and a cap means silently returning a truncated list. `MethodSummary.deletion_state` rides the list rows: a method mid-erasure stays listed so the UI can render it as "Deleting…", while `getMethod` refuses it with a 409. Full read responses (`MethodDetail`, from `getMethod`) carry `org_id`, `created_by_user_id`, a server-derived `description` (recomputed from the draft's top-level `description` on every draft write), the draft token and digest, the latest version's summary and the draft's `files` on top of the write contract's `name` / `input_data`; the writes and the rename answer `MethodData`, the same method without `files`.
135
156
  - **Method examples:** `createMethodExample`, `listMethodExamples`, `iterateMethodExamples`, `getMethodExample`, `updateMethodExample`, `recordMethodExampleLatestRun`, `deleteMethodExample`, on `/v1/methods/{id}/examples`. An example is a named set of inputs saved on a method, shared across the organization, with an `input_revision` that rises each time its inputs change and the `latest_run_id` of those inputs, which an inputs edit clears. `recordMethodExampleLatestRun` records a run against the revision whose inputs it used and answers a `MethodExampleLatestRunResult` discriminated on `outcome`, `recorded` or `stale`, each arm carrying the `example`: a run that outlasted an edit is a verdict, never thrown, and an answer off the union is one the SDK cannot read; one helper, `readVerdict`, reads this verdict and `publishMethod`'s, the example checked once, by the reader every example goes through. Every answer carrying an example is read with `parseExactJson` and every request body written with `stringifyExactJson`, so an integer past `Number.MAX_SAFE_INTEGER` in the inputs reads as a `bigint` and goes back digit for digit, to an edit or as a run's inputs; `3.0` still reads as `3`, and inputs holding one sent back unchanged are an edit for the platform, which compares inputs as canonical JSON. Every example the client hands back, the page rows and the verdict's `example` included, is checked by `readMethodExample` against the fields the Python SDK's model requires, with the strict types it holds them to, an example breaking one being an answer the SDK cannot read. The list is paged oldest first by an opaque cursor, and a page may be short while `nextCursor` is set; `iterateMethodExamples` follows it through the loop `iterateMethods` and `iterateRuns` share, and refuses a method id at the call rather than at the first pull. Every method takes a bare catalog id, and the client refuses an example id that is empty, `.` or `..` or holds a lone surrogate, a create without its name or inputs or whose `run_id` is given but empty, an edit naming neither `name` nor `inputs`, and a latest-run write without a run id or a positive integer revision before sending. See [method-examples.md](./method-examples.md).
136
157
  - **Organizations:** `listMemberships`, `createOrganization`, `renameOrganization` (`PATCH`). Each membership carries the caller's `role_in_org`, an `OrgRole` (`admin`, `member` or `runner`) read open: a role a later platform adds arrives as its raw string, so a `switch` over it says what an unknown role does. The role is for display; what it may do is the platform's to decide, and a call refused to the caller's role is an `ApiResponseError`. The org "switch" is a WorkOS-session op (`/api/active-org`), not a `/v1` route — out of SDK scope.
137
158
  - **Billing:** `getSubscription`, `listPlans`, `listInvoices`, `createCheckout`, `changePlan` (409 `conflict` ⇒ no subscription), `getBillingPortal` (409 `conflict` ⇒ no subscription).
138
159
  - **Pipelex API keys (`plx_sk_…`):** `listPipelexApiKeys`, `createPipelexApiKey` (409 `pipelex_api_key_limit_reached`), `revokePipelexApiKey`, `rotatePipelexApiKey`. The plaintext `api_key` is returned once, on create/rotate.
139
160
  - **Onboarding:** `submitOnboarding`.
140
- - **Storage:** `resolveStorageUrl`, `resolveStorageUrls` (`POST /v1/resolve-storage-url/bulk` — the single route applied to a list of at most 100 references, one verdict per reference in request order, a refused reference a value on its item; a deployment without the route answers a `404` `ApiResponseError`), `upload` (JSON base64 — the multipart hop is browser→BFF only), `requestUploadGrant` (`POST /v1/upload/grant` — a presigned, create-only `PUT` for one new object, which the holder of the bytes sends with the standalone `uploadWithGrant`, also exported from the browser-safe `@pipelex/sdk/upload` entry; a declared size over the cap is a `413` `ApiResponseError`). The higher-level `uploadFile` / `prepareInputs` preparation surface is built on top of `upload`; `prepareInputs` additionally composes one `pipeIo` call, which selects the pipe and answers with the input-form descriptor it walks, and takes the method as inline `files`, a `method_ref` or a `method_id` like every other method-taking operation — see [input-preparation.md](./input-preparation.md). The download twin — `locateArtifacts`, `collectArtifacts`, `resolveArtifacts`, `fetchArtifact`, `downloadArtifacts` — is built on top of `resolveStorageUrls` and `getRunResult`, never on the embedded `public_url`, and is documented on [artifact-download.md](./artifact-download.md); downloading is a separate explicit gesture, and no run call takes a `download` option.
161
+ - **Storage:** `resolveStorageUrl`, `resolveStorageUrls` (`POST /v1/resolve-storage-url/bulk` — the single route applied to a list of at most 100 references, one verdict per reference in request order, a refused reference a value on its item; a deployment without the route answers a `404` `ApiResponseError`), `upload` (JSON base64 — the multipart hop is browser→BFF only), `requestUploadGrant` (`POST /v1/upload/grant` — a presigned, create-only `PUT` for one new object, which the holder of the bytes sends with the standalone `uploadWithGrant`, also exported from the browser-safe `@pipelex/sdk/upload` entry; a declared size over the cap is a `413` `ApiResponseError`). The higher-level `uploadFile` / `prepareInputs` preparation surface is built on top of `upload`; `prepareInputs` additionally composes one `pipeIo` call, which selects the pipe and answers with the input-form descriptor it walks, and takes the method as its tree, a `method_ref` or a `method_id` like every other method-taking operation — see [input-preparation.md](./input-preparation.md). The download twin — `locateArtifacts`, `collectArtifacts`, `resolveArtifacts`, `fetchArtifact`, `downloadArtifacts` — is built on top of `resolveStorageUrls` and `getRunResult`, never on the embedded `public_url`, and is documented on [artifact-download.md](./artifact-download.md); downloading is a separate explicit gesture, and no run call takes a `download` option.
141
162
  - **Runs list/detail/update:** `listRuns` (by `method_id`) returns **one page**, not the whole history — a `RunPage` (`{items, nextCursor}`) served from a time-ordered index, so its cost is the page size rather than the size of the history. Its rows are `RunHistoryItem`, **not** `PipelineRun`: the id, `status`, `created_at`, `finished_at`, `pipe_code`, for a failed run `error`, and `method_version` and `source_digest`, which version of the method the run ran (a number, `"draft"`, or `null` for an inline source) and the SHA-256 of the sources it sent as text, 64 hex digits with no prefix and `null` for a run whose `files` held bytes as Base64 or by hash — what a history row shows. `getRunDetail` also carries the run's `tree_digest` and `crate_fingerprint`, each `null` for a run that recorded none. The history lists the runs of every version and of the draft together, by the bare id. `method_version` and `source_digest` are optional on every run record (`RunPublic`, `RunRead`, `PipelineRun`, `RunDetail`, `RunHistoryItem`), since a run recorded before the platform recorded them carries neither. The run's organization, creator, method, workflow id and result location are not on the list; `getRunDetail` returns them. `iterateRuns` is the async iterator that follows the cursor for callers who genuinely want everything (`for await (const run of client.iterateRuns(id))`); it is deliberately not a `listAllRuns(): Promise<RunHistoryItem[]>`, because an all-at-once helper needs a page cap and a cap means silently returning a truncated list. Both accept `createdFrom` / `createdTo`, applied server-side as index key conditions — these are **instants, not days** (ISO-8601 with a UTC offset; a naive timestamp is a 400), because only the caller knows which timezone's day it means. Both also accept `createdBy: "me"`, sent as `created_by=me`, which keeps the runs the caller started (the platform resolves "me" from the authenticated identity and takes no other value), and `status`, a list of `RunStatus` sent as one repeated `status=` parameter per status, deduplicated into the platform's order, which keeps the runs in any of them: the caller's own latest unfinished run is `{ createdBy: "me", status: ["PENDING", "STARTED", "RUNNING"], limit: 1 }`. An empty `status` list would be sent as no parameter at all, which lists every status, so it is a `RequestArgumentError` before any request, as is a name that is no `RunStatus`. These two are filters applied after the index read, so **a filtered page can hold fewer runs than `limit`, or none, and still carry a `nextCursor`**: an empty page is not the end of the history, a `null` cursor is, and `iterateRuns` keeps following the cursor through one. It runs the same loop as `iterateMethods`, so it also stops on a cursor the server failed to advance and throws `PagingNotTerminatingError` past the same page ceiling. `getRunDetail` (`GET /v1/runs/{id}`) is the only call returning `mthds_contents` — what the run actually executed — and `inputs`; it is kept off the status read that pollers hit every few seconds and off the list, where the bundle would be multiplied by the page size. `updateRun` is the admin/manual status patch. The status/results/start lifecycle routes live in the run-lifecycle section above.
142
163
 
143
164
  ## Hook bundle (`dist-hooks/check.mjs`)
144
165
 
145
- `src/hooks/` holds the post-edit `.mthds` hook pipeline (local lint/format via `@pipelex/tools-wasm`, then a `POST /v1/validate` verdict through this SDK), bundled into a single dependency-free ESM file by `npm run build:hook`. Its validate client is built by `src/hooks/validate-client.ts` with `appInfo: { name: "pipelex-mthds-check", version: SDK_VERSION }`, the `hook` surface's registry token, so the platform attributes its calls to the hook rather than to a plain SDK caller. The one specifier left external is `undici`, reached only by the artifact fetch path through a dynamic import the hook never executes, so the bundle does not inline an HTTP client it cannot use. **Distribution is source-checkout vendoring, not npm:** `pipelex-plugins` runs `make vendor-hook`, which builds `dist-hooks/check.mjs` in a sibling checkout of this repo and copies it in as a static hook asset — the published `@pipelex/sdk` tarball deliberately does not ship the bundle (it inlines the ~4 MB WASM engine, and no npm consumer of the SDK's library API uses it). `dist-hooks/` is gitignored; the consumer repo's own checks gate a stale vendored copy. `@pipelex/tools-wasm` is pinned to an exact version in `package.json`, because the engine carries the MTHDS schema the hook lints against: a newer schema reaches the hook only by a deliberate bump followed by a rebuild and a re-vendor, and the bundle's provenance line (`@pipelex/sdk <version> (<commit>) + @pipelex/tools-wasm <version> (npm)`) names the engine it embeds. Its bundle gatherer (`src/hooks/bundle-gather.ts`) parses each `.mthds` file it sends with `smol-toml`, which the bundle inlines, to find the template files its `PipeDocGen` steps name, and sends them as the request's `resources`. Which file the hook checks on each platform, what it sends with it, and how it finds the file a Codex patch run through the shell wrote, is in [`hook-bundle.md`](./hook-bundle.md).
166
+ `src/hooks/` holds the post-edit `.mthds` hook pipeline (local lint/format via `@pipelex/tools-wasm`, then a `POST /v1/validate` verdict through this SDK), bundled into a single dependency-free ESM file by `npm run build:hook`. Its validate client is built by `src/hooks/validate-client.ts` with `appInfo: { name: "pipelex-mthds-check", version: SDK_VERSION }`, the `hook` surface's registry token, so the platform attributes its calls to the hook rather than to a plain SDK caller. The one specifier left external is `undici`, reached only by the artifact fetch path through the Node host's dynamic import, which the hook never executes, so the bundle does not inline an HTTP client it cannot use; the hook runs in Node, so the build resolves `#node-host` to the source of the Node host. **Distribution is source-checkout vendoring, not npm:** `pipelex-plugins` runs `make vendor-hook`, which builds `dist-hooks/check.mjs` in a sibling checkout of this repo and copies it in as a static hook asset — the published `@pipelex/sdk` tarball deliberately does not ship the bundle (it inlines the ~4 MB WASM engine, and no npm consumer of the SDK's library API uses it). `dist-hooks/` is gitignored; the consumer repo's own checks gate a stale vendored copy. `@pipelex/tools-wasm` is pinned to an exact version in `package.json`, because the engine carries the MTHDS schema the hook lints against: a newer schema reaches the hook only by a deliberate bump followed by a rebuild and a re-vendor, and the bundle's provenance line (`@pipelex/sdk <version> (<commit>) + @pipelex/tools-wasm <version> (npm)`) names the engine it embeds. Its tree gatherer (`src/hooks/bundle-gather.ts`) lists the edited file's directory with the `mthds` package's lister, which the bundle inlines, under the hook's own caps, and the validation sends that tree, the template files its `PipeDocGen` steps name included. The networked stage is `src/hooks/validate-stage.ts`: it holds the whole stage, the version handshake, the blob route, the uploads and the validation, to one deadline (`VALIDATE_TIMEOUT_MS`, ten seconds) whose signal it hands to `validate`, so the request or upload under way stops when it runs out and the stage reads as `unavailable`. Which file the hook checks on each platform, what it sends with it, and how it finds the file a Codex patch run through the shell wrote, is in [`hook-bundle.md`](./hook-bundle.md).
167
+
168
+ Before the validation sends anything, the gatherer reads every file of the tree once and screens it with `src/hooks/secret-guard.ts`, the workshop's secret guard (pipelex-mcp's `src/capabilities/secret-guard.ts`), ahead of the workshop's copy on its SSH key names and its PEM private key block, because the lister reads no `.gitignore` above the tree's root. A file named like a known secret file, or holding a well-known key format, stays home and is named in the note the verdict carries, placed after the verdict's text and named once for one tree, never quoted, and no note is shown when no verdict comes back; one that is a bundle or the manifest, which every validation reads, keeps the whole tree home, and the stage reads as `refused`. The tree sent serves the bytes screened, never read again ([`hook-bundle.md`](./hook-bundle.md#secrets-stay-home)).
146
169
 
147
170
  ## Testing
148
171
 
package/docs/cli.md CHANGED
@@ -96,7 +96,7 @@ A `--method` path is read by the command's own reader (`src/cli/bundle.ts`), who
96
96
 
97
97
  - **A `.mthds` file** is a one-file bundle, sent under its own file name. A file of any other name is refused.
98
98
  - **A directory** is every `.mthds` file under it, at any depth, each sent under its path relative to the directory, with `/` between segments.
99
- - **The order** is the order of those names compared code point by code point, which is the order of their UTF-8 bytes: `Zeta.mthds` before `main.mthds`, `steps/score.mthds` before `steps2.mthds`, and `~.mthds` (U+FF5E) before `🧾.mthds` (U+1F9FE), where UTF-16 order would put them the other way round. A run sends the contents in this order as `mthds_contents`, and the pipe I/O route gets them as `files`, each with its name as `source`.
99
+ - **The order** is the order of those names compared code point by code point, which is the order of their UTF-8 bytes: `Zeta.mthds` before `main.mthds`, `steps/score.mthds` before `steps2.mthds`, and `~.mthds` (U+FF5E) before `🧾.mthds` (U+1F9FE), where UTF-16 order would put them the other way round. A run sends the contents in this order as `mthds_contents`, a field the SDK no longer names and so passes through the request's `extra`, and the pipe I/O route gets them as the method's tree, the `files` map, each under its name as its tree path, sent inline as given.
100
100
  - **A hidden entry**, a file or a directory whose name starts with `.`, is skipped, so a `.venv/` or a `.git/` inside a bundle is never read.
101
101
  - **Any file not named `.mthds`** is ignored, Python files included: the command sends only the MTHDS sources.
102
102
  - **A symbolic link** is judged by its own name. One named `.mthds` that leads to a file is read through, under the link's name; one that leads to a directory is not descended into, which keeps a link cycle from looping; one that leads nowhere is refused. The `--method` path itself may be a link, and is followed.
@@ -12,13 +12,17 @@ Three routes project a **closure** of MTHDS files into the artifacts downstream
12
12
 
13
13
  ## The shared envelope
14
14
 
15
- All three take the same closure selector — inline `files`, a `method_ref`, or a hosted `method_id`, exactly one (the strict tooling XOR):
15
+ All three take the same closure selector — the method's tree, a `method_ref`, or a hosted `method_id`, exactly one (the strict tooling XOR) — and the tree may bring its dependencies:
16
16
 
17
17
  ```ts
18
18
  interface CrateRequestBase {
19
- files?: MthdsFileItem[]; // [{ content, source? }] — `source` is the provenance label
20
- method_ref?: string; // address form server-resolved (pipelex-api >= 0.21.0); registry form reserved → 501
21
- resources?: BundleResources | null; // beside `files`: the files the steps read, keyed by the path a step names
19
+ files?: MethodFiles; // the protocol's map: tree path → text, { base64 } or { sha256, size }
20
+ directory?: string; // or a directory on disk, listed by the mthds package (Node only)
21
+ tree?: MethodTreeSource; // or a listed tree, sent inline or by hash
22
+ contents?: Readonly<Record<string, Uint8Array | string>>; // or each file's bytes or text by tree path
23
+ dependencies?: Record<string, MethodFiles>; // beside the tree: the packages it calls by address
24
+ method_ref?: string; // address form server-resolved; registry form reserved → 501
25
+ read_scope?: string; // Pipelex-API extension; the hosted platform stamps its own
22
26
  }
23
27
 
24
28
  interface PipelexHostedToolingExtensions {
@@ -26,30 +30,45 @@ interface PipelexHostedToolingExtensions {
26
30
  }
27
31
  ```
28
32
 
29
- `source` is why the envelope carries files rather than bare strings. A closure is a set of files, and a diagnostic that cannot say _which_ file it came from is much harder to act on. Pass a filename (or any label) per file and, **when the engine can attribute the diagnostic to a file**, it comes back as `source` on the corresponding `validation_errors[]` item.
33
+ **The tree is the MTHDS protocol's `files` map**, the shape `validate`, the runs and the method catalog take: each key a tree path, each value the file's text, `{ base64 }` for bytes that are not text, or `{ sha256, size }` naming a blob the organization already holds. The runner writes the tree whole and loads it as the method's library directory, so every `.mthds` bundle is named by its tree path and every other file, a `PipeDocGen` template beside its bundle for instance, is one a step may read. The tree is given in exactly one of four forms, as a run, a validation and a save take it ([method-trees.md](./method-trees.md)):
30
34
 
31
- Treat that attribution as best-effort, not a guarantee — which is why `ValidationErrorItem.source` is typed optional and why the `resolve` sample below reads `err.source ?? "?"`. Some arms don't populate it: graph-level `dry_run` and `pipe_factory` items have no single owning file, and a `main_pipe` naming a nonexistent pipe currently reports its provenance in the message prose while leaving the structured field unset. The syntax-error arm, the common case, does thread it.
35
+ - `files`, the map, posted as given with no round trip;
36
+ - `directory`, a directory on disk, listed by the `mthds` package's lister under the standard's membership rule and the deployment's limits, in Node only;
37
+ - `tree`, a listed source such as the `MethodTree` the `mthds` package's `listMethodTree` returns;
38
+ - `contents`, each file's bytes or text keyed by its tree path.
32
39
 
33
- > `MthdsFileItem` is not the same type as `MthdsFile` (the one `validateFiles()` takes). `/v1/validate` spells the provenance label `uri` and carries it in a parallel `mthds_sources` array; this envelope spells it `source` and carries it inline. The two collapse into one only if `/v1/validate` ever migrates onto `files[]` — a protocol-level change owned by the MTHDS standard, not ours to make here.
40
+ A `directory`, `tree` or `contents` travels as a run or a save sends it ([method-trees.md](./method-trees.md#how-a-tree-travels)): the client reads the version handshake first, then sends the tree inline when its map weighs no more than the lower of `INLINE_TREE_MAX_BYTES` (256 KiB) and the announced `max_inline_bytes`, and otherwise, against an API that declares a blob store, by hash after asking `POST /v1/blobs/missing` which blobs the organization lacks and uploading each one once; against an API with no blob store it sends the tree inline whatever its weight, refusing one past `max_inline_bytes`. A blob that fails to upload throws `MethodFileTransferError`, whose message ends "so the request was not sent", and the route is never asked. Two forms at once, a directory the lister refuses, and a `tree` holding no file, are refused with `RequestArgumentError` before any request. A `files` map is never checked client-side: a key or an entry the tree's rule refuses is the server's `422`.
34
41
 
35
- An **address-form** `method_ref` (`github.com/<owner>/<repo>[/<selector>][@<tag>]`) is resolved by the server through the same fetch path as a `method_ref` run: the repository fetched at the tag, the package located by manifest identity, the package's real relative paths feeding the per-file `source` labels. Any non-address reference stays reserved and answers `501`.
42
+ **An empty map is absent**, as in the Python SDK: `files: {}` or `files: null`, `contents: {}`, `directory: ""` and `dependencies: {}` are not sent, so an empty map never reaches the wire as a second selector, and `{ files: {}, method_id: "mt_…" }` posts the `method_id` alone. A value that is not a map, such as a list, is not absent: it is posted as given and refused by the server.
36
43
 
37
- `method_id` is the hosted platform's selector, and it is a **pass-through**: nothing is expanded client-side — the platform resolves the id against the org's catalog and injects the stored source before the runner sees the request. A bare `mt_…` names the method's latest published version, `mt_…@<n>` the fixed version `n` and `mt_…@draft` its draft, and the suffix rides the string untouched. It is meaningless against a bare runner (no catalog), and on `api.pipelex.com` its availability follows the platform deploy that adds the tooling-route transform. An unknown or foreign-org id is a `404` (indistinguishable by design); a bare id of a method never published is a `409` `method_not_published`, whose detail names `mt_…@draft`; a version the method never published is a `404` `method_version_not_found`; a malformed suffix is a `422`; a stored method with no MTHDS source is a `422`.
44
+ When the request carries a `read_scope`, the blob route is asked under it, as `POST /v1/blobs/missing?read_scope=…`, so the missing blobs are uploaded into the store the request's hash entries are read from; each upload target the runner answers is signed for that scope, so the uploads need nothing more. The hosted platform's own blob route reads the caller's organization and ignores the query, as it does the request's `read_scope`.
38
45
 
39
- Supplying **no** selector or **more than one** is a request-shape `422` — the tooling routes are stateless, so there is no linkage exception; a second selector could only be ignored, which is the worst contract of the three. The SDK does not model the XOR in the type system — the union would force the overwhelmingly common `{ files }` call site to pick a branch for no gain, and the server's answer is a typed `ApiResponseError` either way.
46
+ **Stopping a call.** Each route takes a second argument, `CrateRouteOptions`, whose `signal` is read before each step a `directory`, `tree` or `contents` costs — the version handshake, the directory's listing, the blob route, each read and each upload of a missing blob — and before the request itself, and is handed to each of those steps, so an abort stops the handshake, the blob route, a read or an upload already under way. Once it has aborted, nothing further starts and the call throws the abort; the route's own request, once sent, runs to its end, since the routes take no transport options (a static route's budget is the client's own). `prepareInputs` passes its `signal` here, so an abort during the handshake stops it and leaves the blob route, the uploads and `POST /v1/pipe-io` unasked.
40
47
 
41
- A caller facing a bare runner, which has no catalog to resolve an id against, expands a stored method itself: it reads the method's tree with `getMethod` or `getMethodVersion`, writes it to disk with `writeMethodTree`, and sends the files from there ([method-trees.md](./method-trees.md)).
48
+ When the engine can attribute a diagnostic to a file, the `validation_errors[]` item's `source` names it by its tree path. Treat that attribution as best-effort, not a guarantee — which is why `ValidationErrorItem.source` is typed optional and why the `resolve` sample below reads `err.source ?? "?"`. Some arms don't populate it: graph-level `dry_run` and `pipe_factory` items have no single owning file, and a `main_pipe` naming a nonexistent pipe currently reports its provenance in the message prose while leaving the structured field unset.
42
49
 
43
- Beside inline `files`, `resources` carries the files the closure's steps read, such as a `PipeDocGen` step's `template_file`, keyed by the path the step names, each as its bytes. It is the one member the client does not post as given: it encodes the bytes into the wire's base64 map, after refusing a key the runner's path rule refuses. A step whose template file was not sent makes the closure invalid, a `200` verdict saying so; `resources` beside a `method_ref` is the server's `422`. The rules, the limits and the refusals are in [`bundle-resources.md`](./bundle-resources.md).
50
+ `dependencies` carries the method packages the tree calls by address, each a `files` map keyed by its address, posted as given. They are loaded beside the method, never merged into it, so a cross-package call resolves without the server fetching anything. A call into a package nothing shipped makes the closure invalid, a `200` verdict whose `unresolved_package_dependency` item names the package; `dependencies` without the tree is the server's `422`.
44
51
 
45
- A `method_ref` makes the server fetch the repository before it answers, so the client gives a call carrying one a fetch-sized budget (three minutes) instead of the 30-second default. A call carrying `resources` gets the client's request ceiling (`requestTimeoutMs`, twenty minutes by default) that `start` and `validate` give the same files, because uploading them can take longer than the default on its own. On the hosted API the gateway caps every request at about 30 seconds whatever the client allows, so a cold `method_ref` clone can answer a `502`; retrying clears it once the runner has cached the clone.
52
+ An **address-form** `method_ref` (`github.com/<owner>/<repo>[/<selector>][@<tag>]`) is resolved by the server through the same fetch path as a `method_ref` run: the package's whole tree is read from the commit at the tag and loaded as a sent tree is, with the packages it ships under its own `.mthds/methods/` beside it. Any non-address reference stays reserved and answers `501`.
53
+
54
+ `method_id` is the hosted platform's selector, and it is a **pass-through**: nothing is expanded client-side — the platform resolves the id against the org's catalog and injects the stored tree as `files` before the runner sees the request. A bare `mt_…` names the method's latest published version, `mt_…@<n>` the fixed version `n` and `mt_…@draft` its draft, and the suffix rides the string untouched. It is meaningless against a bare runner (no catalog). An unknown or foreign-org id is a `404` (indistinguishable by design); a bare id of a method never published is a `409` `method_not_published`, whose detail names `mt_…@draft`; a version the method never published is a `404` `method_version_not_found`; a malformed suffix is a `422`; a stored method with no MTHDS source is a `422`.
55
+
56
+ `read_scope` is a Pipelex-API extension naming the scope the request reads the method under: a hash entry of the tree resolves in that scope's blob store. A caller of a bare runner may send it; **the hosted platform stamps it from the caller's organization and drops one the caller sent**, so a hosted caller never needs it.
57
+
58
+ Supplying **no** selector or **more than one** is a request-shape `422` — the tooling routes are stateless, so there is no linkage exception; a second selector could only be ignored, which is the worst contract of the three. The SDK does not model the XOR in the type system — the union would force the overwhelmingly common `{ files }` call site to pick a branch for no gain, and the server's answer is a typed `ApiResponseError` either way. Only the four forms of the tree are checked client-side, because the client has to choose how to send it.
59
+
60
+ A caller facing a bare runner, which has no catalog to resolve an id against, expands a stored method itself: it reads the method's tree with `getMethod` or `getMethodVersion`, writes it to disk with `writeMethodTree`, and sends it from there as a `tree` ([method-trees.md](./method-trees.md)).
61
+
62
+ The files the closure's steps read, such as a `PipeDocGen` step's `template_file`, are files of the tree like any other, at the tree path the step names. A step whose template file the tree does not hold makes the closure invalid, a `200` verdict saying so ([method-trees.md](./method-trees.md#the-files-a-step-reads)).
63
+
64
+ A `method_ref` makes the server fetch the repository before it answers, so the client gives a call carrying one a fetch-sized budget (three minutes) instead of the 30-second default. A call whose `files` carry more than `INLINE_TREE_MAX_BYTES` (256 KiB) inline, as a tree sent to an API keeping no blob store does whatever its weight, and as a `files` map given as is may, gets the client's request ceiling (`requestTimeoutMs`) instead, since uploading the body is charged against the budget; a tree sent by hash keeps the default, its files uploaded before the request. On the hosted API the gateway caps every request at about 30 seconds whatever the client allows, so a cold `method_ref` clone can answer a `502`; retrying clears it once the runner has cached the clone.
46
65
 
47
66
  ## `resolve` — the normalized crate
48
67
 
49
68
  Resolution is a first-class language operation alongside validation: the closure is loaded and statically validated, then emitted with fully qualified refs, refinement flattened, natives materialized, and a fingerprint set.
50
69
 
51
70
  ```ts
52
- const result = await client.resolve({ files: [{ content: src, source: "method.mthds" }] });
71
+ const result = await client.resolve({ files: { "method.mthds": src } });
53
72
 
54
73
  if (!result.is_valid) {
55
74
  for (const err of result.validation_errors) console.error(`${err.source ?? "?"}: ${err.message}`);
@@ -62,7 +81,7 @@ console.log(result.crate.fingerprint); // rides INSIDE the crate, not beside it
62
81
  Two things to internalize:
63
82
 
64
83
  - **`fingerprint` and `mthds_version` are crate members**, not siblings of `crate`.
65
- - **Compare `fingerprint` values; never hash the crate yourself.** The fingerprint is a property of the _logical_ crate, not of an encoding: the server hashes `{concepts, pipes, domains}` with provenance `source` stripped, excluding `source_map`, `mthds_version` and `fingerprint` itself. And these are not the bytes `pipelex resolve --format json` prints — the CLI pretty-prints, the route answers compact JSON. Same logical crate, different serialization. Hashing `JSON.stringify(result.crate)` and comparing it against `result.crate.fingerprint` will mismatch on every call, and will look like tampering.
84
+ - **Compare `fingerprint` values; never hash the crate yourself.** The fingerprint is a property of the _logical_ crate, not of an encoding: the server hashes `{concepts, pipes, domains}` with each provenance `source` stripped, excluding `source_map`, `mthds_version` and `fingerprint` itself. And these are not the bytes `pipelex resolve --format json` prints — the CLI pretty-prints, the route answers compact JSON. Same logical crate, different serialization. Hashing `JSON.stringify(result.crate)` and comparing it against `result.crate.fingerprint` will mismatch on every call, and will look like tampering.
66
85
  - **`crate` is typed as opaque transport** (`Record<string, unknown>`). Its schema belongs to the MTHDS standard; restating it here would be a second source of truth, free to drift from the one the server emits.
67
86
 
68
87
  `resolve` runs **no dry-run sweep**. A valid verdict says the library resolves, never that it runs — runnability is `validate`'s vocabulary.
@@ -111,9 +130,9 @@ The request is the shared envelope plus three optional fields, posted verbatim:
111
130
  | --------------- | ------- | --------------------------------------------------------------------------------------------------------- |
112
131
  | `pipe_ref` | none | The qualified `domain.pipe_code` to describe. Omitted, the server's selection chain decides. |
113
132
  | `all_pipes` | `false` | Describe every pipe the closure loads instead of the selected one. |
114
- | `include_files` | `false` | Echo the resolved closure's `.mthds` files as `files`, in the request's own `{ content, source? }` shape. |
133
+ | `include_files` | `false` | Echo the text of the method's own `.mthds` bundles as `files`, a map from tree path to text. |
115
134
 
116
- **Selection.** The request's `pipe_ref`; else a fetched package's manifest `main_pipe`; else the closure's `main_pipe`, when exactly one domain declares one. A `pipe_ref` that names no pipe, and — without `all_pipes` — a chain that finds no entry pipe or several, are refused with a `422` whose `errorType` names the refusal (see [What throws](#what-throws)). The server does not refuse a bare code yet: a bare ref that matches one pipe resolves across domains, and the answer reports the qualified ref. With `all_pipes: true` the route never refuses for want of an entry pipe, so a method that declares none is still describable.
135
+ **Selection.** The request's `pipe_ref`; else the `main_pipe` of the `METHODS.toml` at the tree's root, sent in the tree or fetched with a package; else the closure's `main_pipe`, when exactly one domain declares one. A `pipe_ref` that names no pipe, and — without `all_pipes` — a chain that finds no entry pipe or several, are refused with a `422` whose `errorType` names the refusal (see [What throws](#what-throws)). The server does not refuse a bare code yet: a bare ref that matches one pipe resolves across domains, and the answer reports the qualified ref. With `all_pipes: true` the route never refuses for want of an entry pipe, so a method that declares none is still describable.
117
136
 
118
137
  The valid arm (`PipeIOValidReport`):
119
138
 
@@ -121,7 +140,7 @@ The valid arm (`PipeIOValidReport`):
121
140
  - `pipe_io_contracts`, `input_form`, `output_form` — the three maps, sharing one key set: the resolved `pipe_ref` alone by default, every pipe under `all_pipes`. For a closure `validate` also accepts, each equals `validate`'s same-named field restricted to the same keys, null members such as a contract's `item_count: null` included.
122
141
  - `default_pipe_ref` — the method's own entry pipe, the selection chain without the request's `pipe_ref`, or `null` when that chain finds none or several. A request that omits `pipe_ref` always answers `pipe_ref === default_pipe_ref`. It is **not** `validate`'s field of the same name, which is the run default and names the first of several declaring domains where this route refuses to choose.
123
142
  - `pending_signatures`, `is_runnable` — what they mean on `validate`, read off the loaded library with no dry run, so a method whose dry run would fail is still reported runnable here.
124
- - `files` — present only with `include_files: true`: the request's files for inline `files`, the fetched package's `.mthds` files under their package-relative paths for a `method_ref`, the stored files under their stored names for a hosted `method_id`. Absent, not empty, otherwise.
143
+ - `files` — present only with `include_files: true`: the text of each `.mthds` bundle of the method's own tree, keyed by its tree path — the request's bundles for a sent tree, whatever form or entry carried them, the fetched package's bundles keyed by their path from the package root for a `method_ref`, the stored tree's bundles for a hosted `method_id`. The tree's other files and the bundles of its dependencies are never included. Absent, not empty, otherwise.
125
144
 
126
145
  `is_valid: true` means what it means on `resolve`: the closure parsed, loaded and passed static validation. The invalid arm is the shared `CrateInvalidReport`, and it carries no artifacts, no selection and no files, whatever the request asked for.
127
146
 
@@ -137,19 +156,22 @@ Only a **no-verdict** condition, as the typed `ApiResponseError` — branch on i
137
156
 
138
157
  | Status | Cause |
139
158
  | ------------- | --------------------------------------------------------------------------------------------------------- |
140
- | `422` | Request shape: no closure selector or more than one, an over-limit file, an unknown `kind`/`target`, a `pipe_ref` on `kind: "types"`, a fetched package with no `.mthds` file, a stored method with no MTHDS source, a `method_id` whose version suffix is malformed. |
141
- | `422` / `413` / `400` | On `resources`: `InvalidResourcePath`, `BundleResourceNotNamedError` (a file no step names), `ValidationError` (beside a `method_ref`), `PayloadTooLarge`, `BundleResourcesTooLargeError`, `InvalidBase64` — see [`bundle-resources.md`](./bundle-resources.md#what-the-server-refuses). |
159
+ | `422` | Request shape: no closure selector or more than one, `dependencies` without the tree, an unknown `kind`/`target`, a `pipe_ref` on `kind: "types"`, a fetched package with no `.mthds` file, a stored method with no MTHDS source, a `method_id` whose version suffix is malformed. |
160
+ | `422` | The tree's refusals, as on `validate`: `InvalidMethodFiles` for a key or an entry the tree's rule refuses (a `files` that is not a map among them), `MissingBlobs` for a hash the server does not hold, `MethodTreeTooLarge` past the tree's limits, `InvalidBundle` for a tree holding no `.mthds` bundle. |
142
161
  | `422` | On `pipeIo`, a pipe selection the route refuses: `errorType` `EntryPipeNotFoundError` for a `pipe_ref` that names no pipe or a method with no entry pipe, `EntryPipeAmbiguousError` for a code that matches pipes in several domains or several `main_pipe` declarations. The `serverMessage` names the candidates where there are some. |
143
162
  | `404` | Unknown or foreign-org `method_id` (indistinguishable by design); no package at a `method_ref` address. |
144
163
  | `404` | `code` `method_version_not_found`: a `mt_…@<n>` naming a version the method never published. |
145
164
  | `409` | `code` `method_not_published`: a bare `method_id` of a method never published; `mt_…@draft` reaches its draft. |
146
165
  | `501` | Registry-form `method_ref` — reserved, not implemented (the address form resolves). |
147
166
  | `401` / `403` | Auth, or on a hosted origin a route the gateway does not list yet (`403 {"message":"Forbidden"}`). |
167
+ | `403` | `CustomCodeRequiresSandbox` for a tree holding a `.py` file on a deployment that is not sandbox-hosted, `MethodStructuresRefusedError` for one whose Python declares a structure class on one that is; `ReadScopeForbidden` for a `read_scope` a `jwt` caller may not read under. |
148
168
  | `502` | On a hosted origin, a cold `method_ref` clone that outran the gateway's 30-second cap; a retry clears it. |
149
169
  | `5xx` | Server fault, including on `pipeIo` a pipe whose artifacts cannot be derived. |
150
170
 
151
171
  Note the split: a bad **closure** is a 200 verdict; a bad **request** throws.
152
172
 
173
+ Before any request, a `directory`, `tree` or `contents` can also throw: `RequestArgumentError` for two forms of the tree, a directory the lister refuses, a tree holding no file or one too heavy to send inline to an API with no blob store, and `MethodFileTransferError` for a blob that could not be uploaded, so the request was not sent ([errors.md](./errors.md)).
174
+
153
175
  (The offline check below is not a route and throws its own `CodegenLockError` instead — it never reaches the network.)
154
176
 
155
177
  ## The offline check — `runCodegenCheck`
@@ -177,7 +199,7 @@ if (!report.isCurrent) {
177
199
 
178
200
  A `codegen()` response feeds straight in with no mapping — `GeneratedArtifact` and `CodegenTreeFile` are structurally identical on purpose, so `runCodegenCheck({ lockContent: result.lock, files: result.artifacts })` type-checks and reports `isCurrent`. That is worth doing right after a regeneration, before writing anything to disk.
179
201
 
180
- The function is `async` because it hashes through **WebCrypto** (`crypto.subtle`) rather than `node:crypto`, so the check adds no Node builtin to the barrel's import graph. (That is not the same as the barrel being browser-bundleable today: `upload.ts` still names `node:fs/promises`, so a browser-targeting bundler must mark `node:*` external.) One caveat that never bites a CI script but should not surprise anyone: `crypto.subtle` is secure-context-only, so a browser page must be served over https (or localhost).
202
+ The function is `async` because it hashes through **WebCrypto** (`crypto.subtle`) rather than `node:crypto`, so the check adds no Node builtin to the barrel's import graph, which bundles for the browser with nothing marked external ([architecture.md](./architecture.md#module-format--packaging)). One caveat that never bites a CI script but should not surprise anyone: `crypto.subtle` is secure-context-only, so a browser page must be served over https (or localhost).
181
203
 
182
204
  ### The algorithm
183
205