@executablemd/runtime 0.7.0 → 0.8.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.
package/esm/test/stubs.js CHANGED
@@ -19,7 +19,25 @@
19
19
  * });
20
20
  * ```
21
21
  */
22
+ import { Stdio } from "@effectionx/process";
22
23
  import { API } from "../apis.js";
24
+ import { SERVICE_HOSTNAME } from "../service.js";
25
+ /** Install a provider-neutral scoped service attachment stub. */
26
+ export function* useStubService(endpoint) {
27
+ if (endpoint.hostname !== SERVICE_HOSTNAME) {
28
+ throw new Error("stub service endpoint must use 127.0.0.1");
29
+ }
30
+ if (!Number.isInteger(endpoint.port) || endpoint.port < 1 || endpoint.port > 65_535) {
31
+ throw new Error("stub service endpoint port must be an integer from 1 through 65535");
32
+ }
33
+ const exact = Object.freeze({ hostname: SERVICE_HOSTNAME, port: endpoint.port });
34
+ yield* API.Service.around({
35
+ // deno-lint-ignore require-yield
36
+ *start() {
37
+ return { endpoint: exact };
38
+ },
39
+ });
40
+ }
23
41
  /**
24
42
  * Install an in-memory filesystem stub.
25
43
  *
@@ -57,15 +75,20 @@ export function* useStubFs(files) {
57
75
  *
58
76
  * Recognizes `bash -c "echo ..."` and returns the echo'd text as stdout.
59
77
  * All other commands return the script text as stdout with exit code 0.
78
+ *
79
+ * It answers like a real child: the text is written to the stdio chain as it
80
+ * "arrives", so whatever encloses the call sees it the way it would see a
81
+ * child's, and it is retained in the outcome only when the caller asked
82
+ * for retention. A stub that always returned strings would let a document
83
+ * render output the contract says was already displayed.
60
84
  */
61
85
  export function* useEchoExec() {
62
86
  yield* API.Process.around({
63
87
  *exec([options], _next) {
64
88
  const script = (options.command[2] ?? "").trim();
65
- if (script.startsWith("echo ")) {
66
- return { exitCode: 0, stdout: script.slice(5) + "\n", stderr: "" };
67
- }
68
- return { exitCode: 0, stdout: script + "\n", stderr: "" };
89
+ const text = script.startsWith("echo ") ? script.slice(5) + "\n" : script + "\n";
90
+ yield* Stdio.operations.stdout(encoder.encode(text));
91
+ return retained(options, { exitCode: 0, stdout: text, stderr: "" });
69
92
  },
70
93
  });
71
94
  }
@@ -76,8 +99,17 @@ export function* useEchoExec() {
76
99
  */
77
100
  export function* useFailingExec(exitCode, stderr = "command failed") {
78
101
  yield* API.Process.around({
79
- *exec(_args, _next) {
80
- return { exitCode, stdout: "", stderr };
102
+ *exec([options], _next) {
103
+ yield* Stdio.operations.stderr(encoder.encode(stderr));
104
+ return retained(options, { exitCode, stdout: "", stderr });
81
105
  },
82
106
  });
83
107
  }
108
+ const encoder = new TextEncoder();
109
+ /** What the caller asked to keep, so a stub cannot retain more than a call would. */
110
+ function retained(options, outcome) {
111
+ if (options.retain === false) {
112
+ return { exitCode: outcome.exitCode, stdout: undefined, stderr: undefined };
113
+ }
114
+ return outcome;
115
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@executablemd/runtime",
3
- "version": "0.7.0",
3
+ "version": "0.8.1",
4
4
  "description": "Runtime host APIs for executable.md documents.",
5
5
  "homepage": "https://executable.md",
6
6
  "repository": {
@@ -20,6 +20,12 @@
20
20
  "default": "./esm/mod.js"
21
21
  }
22
22
  },
23
+ "./files": {
24
+ "import": {
25
+ "types": "./types/files.d.ts",
26
+ "default": "./esm/files.js"
27
+ }
28
+ },
23
29
  "./test": {
24
30
  "import": {
25
31
  "types": "./types/test/mod.d.ts",
@@ -30,11 +36,11 @@
30
36
  "scripts": {},
31
37
  "dependencies": {
32
38
  "@effectionx/context-api": "0.6.0",
33
- "@effectionx/fetch": "0.2.0",
39
+ "@effectionx/fetch": "0.2.1",
34
40
  "@effectionx/fs": "0.3.0",
35
- "@effectionx/node": "0.2.4",
36
41
  "@effectionx/process": "0.8.1",
37
- "effection": "4.1.0-alpha.10"
42
+ "effection": "4.1.0",
43
+ "@effectionx/node": "0.2.4"
38
44
  },
39
45
  "_generatedBy": "dnt@dev"
40
46
  }
package/types/apis.d.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * Runtime Context APIs — platform I/O operations with pluggable middleware.
3
3
  *
4
- * Five domain-specific context APIs built on `@effectionx/context-api`.
4
+ * Five host-backed domain APIs plus the provider-neutral Service Api, built on
5
+ * `@effectionx/context-api`.
5
6
  * Each API provides default Node.js implementations. Use `.around()` to
6
7
  * install middleware (mocking, instrumentation, sandboxing) scoped to the
7
8
  * current Effection scope.
@@ -25,14 +26,19 @@
25
26
  * });
26
27
  * ```
27
28
  *
28
- * ## Why four separate APIs?
29
+ * ## Why separate APIs?
29
30
  *
30
31
  * - **Process** — subprocess lifecycle has its own cancellation semantics
31
32
  * (killing processes on scope teardown). Middleware targets exec only.
32
- * - **Fs** — reading, writing, and inspecting files form a cohesive file-IO
33
- * surface used together for component resolution, replay guards, and the
34
- * `<File>` component. Middleware installed here sees a document's own file
35
- * access on the same terms as the engine's.
33
+ * - **Fs** — the low-level host file surface: reading, writing, and inspecting
34
+ * paths the engine itself resolves, for component lookup, replay guards, and
35
+ * the root document. It is the host adapter's own dependency, not the
36
+ * boundary a document's paths cross.
37
+ * - **Files** — document filesystem access, in whole semantic operations
38
+ * (`files.ts`). `<File>`, `<Glob>`, and `<TempDir>` speak only this Api, so
39
+ * the same document means the same thing whether its paths resolve in the
40
+ * caller's filesystem or in a run-owned logical one. Its terminal handler
41
+ * throws: an uninstalled provider must not silently reach the host.
36
42
  * - **Fetch** — HTTP has distinct timeout/body/abort semantics. Merging
37
43
  * with Fs or Process would blur cancellation boundaries.
38
44
  * - **Env** — the host itself: metadata (env vars, platform) plus the two
@@ -41,6 +47,8 @@
41
47
  * use `.around()` to mock platform/env for deterministic replay; an
42
48
  * entrypoint installs its `command` and `compile` with `{ at: "min" }` so
43
49
  * ordinary middleware can wrap them.
50
+ * - **Service** — scoped service attachment. Its terminal handler requires an
51
+ * explicit host provider and never detects or imports a runtime.
44
52
  *
45
53
  * ## Middleware
46
54
  *
@@ -59,10 +67,13 @@
59
67
  * ## Test stubs
60
68
  *
61
69
  * Common stubs are provided by `@executablemd/runtime/test`:
62
- * `useStubFs(files)`, `useEchoExec()`, `useFailingExec(code, stderr)`.
70
+ * `useStubFs(files)`, `useEchoExec()`, `useFailingExec(code, stderr)`,
71
+ * `useStubService(endpoint)`.
63
72
  */
64
73
  import { type Api } from "@effectionx/context-api";
65
74
  import type { Operation } from "effection";
75
+ import { Files } from "./files.js";
76
+ import { Service } from "./service.js";
66
77
  /**
67
78
  * Result of a `stat` call.
68
79
  *
@@ -95,17 +106,34 @@ export interface RuntimeFetchResponse {
95
106
  /** Read the response body as text. */
96
107
  text(): Operation<string>;
97
108
  }
109
+ /**
110
+ * What a finished child process reports.
111
+ *
112
+ * `stdout` and `stderr` are the text a caller asked to be retained. A caller
113
+ * that asked for none gets `undefined` rather than an empty string: nothing was
114
+ * accumulated, and "no output" and "not retained" are different answers.
115
+ */
116
+ export interface ProcessOutcome {
117
+ exitCode: number;
118
+ stdout: string | undefined;
119
+ stderr: string | undefined;
120
+ }
121
+ export interface ProcessExecOptions {
122
+ command: string[];
123
+ cwd?: string;
124
+ env?: Record<string, string>;
125
+ timeout?: number;
126
+ /**
127
+ * Retain the child's output in the outcome. Default `true`.
128
+ *
129
+ * Retention is the caller's explicit choice, never an inference: a run that
130
+ * keeps no diagnostic record asks for `false` and the text is forwarded to
131
+ * whatever is displaying it without ever being accumulated here.
132
+ */
133
+ retain?: boolean;
134
+ }
98
135
  interface ProcessHandler {
99
- exec(options: {
100
- command: string[];
101
- cwd?: string;
102
- env?: Record<string, string>;
103
- timeout?: number;
104
- }): Operation<{
105
- exitCode: number;
106
- stdout: string;
107
- stderr: string;
108
- }>;
136
+ exec(options: ProcessExecOptions): Operation<ProcessOutcome>;
109
137
  }
110
138
  interface FsHandler {
111
139
  readTextFile(path: string): Operation<string>;
@@ -178,10 +206,35 @@ interface EnvHandler {
178
206
  export declare const API: {
179
207
  Process: Api<ProcessHandler>;
180
208
  Fs: Api<FsHandler>;
209
+ Files: typeof Files;
181
210
  Fetch: Api<FetchHandler>;
182
211
  Env: Api<EnvHandler>;
212
+ Service: typeof Service;
183
213
  };
184
- export declare const exec: typeof API.Process.operations.exec;
214
+ /**
215
+ * Run a child process.
216
+ *
217
+ * A caller that says nothing about retention keeps the output, which is what
218
+ * every caller with something to read wants and what callers have always had.
219
+ * Asking for `retain: false` keeps the exit status alone, and the overloads say
220
+ * so: there is no string to read on that path, and the type refuses to pretend
221
+ * otherwise. Core, which decides retention per block, calls the Api operation
222
+ * directly and handles both.
223
+ */
224
+ export declare function exec(options: ProcessExecOptions & {
225
+ retain: false;
226
+ }): Operation<{
227
+ exitCode: number;
228
+ stdout: undefined;
229
+ stderr: undefined;
230
+ }>;
231
+ export declare function exec(options: ProcessExecOptions & {
232
+ retain?: true;
233
+ }): Operation<{
234
+ exitCode: number;
235
+ stdout: string;
236
+ stderr: string;
237
+ }>;
185
238
  export declare const readTextFile: typeof API.Fs.operations.readTextFile;
186
239
  export declare const stat: typeof API.Fs.operations.stat;
187
240
  export declare const glob: typeof API.Fs.operations.glob;
@@ -196,4 +249,24 @@ export declare const cwd: typeof API.Env.operations.cwd;
196
249
  export declare const platform: typeof API.Env.operations.platform;
197
250
  export declare const command: typeof API.Env.operations.command;
198
251
  export declare const compile: typeof API.Env.operations.compile;
252
+ /**
253
+ * Discard the standard output of subprocesses started in this scope.
254
+ *
255
+ * For a caller whose subprocess output is an *answer* rather than something to
256
+ * show: a command whose stdout is parsed and returned would otherwise also
257
+ * print itself into whatever the process was rendering. `stderr` is left alone,
258
+ * because that is where a failing command explains itself and a diagnostic is
259
+ * worth seeing.
260
+ *
261
+ * It lives here because reaching the process Api's stdio directly is host
262
+ * behavior, and modules held to the runtime-neutral boundary may not import a
263
+ * host process module of their own.
264
+ *
265
+ * Installed at the display boundary, where the host's own writer sits: not
266
+ * showing something and not knowing it are different, and a caller that asked
267
+ * for the answer must still be given it. Anything upstream — this adapter's
268
+ * retention, a document's capture, a run's record — reads the bytes first and
269
+ * only the host is left out.
270
+ */
271
+ export declare function useQuietProcessOutput(): Operation<void>;
199
272
  export {};
package/types/config.d.ts CHANGED
@@ -1,24 +1,42 @@
1
1
  /**
2
2
  * Config Api — shared execution configuration with pluggable middleware.
3
3
  *
4
- * Supplies the contextual timeout in milliseconds. Process, Fetch, and
5
- * Agent operations read it when a call does not provide an explicit
6
- * timeout. Override it for a scope with:
4
+ * Three timeouts, three owners, no defaults:
5
+ *
6
+ * - `timeout` is the deadline for the whole run — preparation and execution
7
+ * together — and only the outer run boundary consumes it.
8
+ * - `timeoutExec` is what each exec block gets, and only exec blocks and the
9
+ * built-in `timeout` modifier consume it.
10
+ * - `timeoutFetch` is what each Fetch gets, and only Fetch consumes it.
11
+ *
12
+ * `undefined` means no timeout, and it is what every field starts as. An
13
+ * operation nobody bounded runs until it finishes or the run's own deadline
14
+ * cancels it; a general "shared timeout" that quietly bounded processes,
15
+ * requests, prompts, and services alike is what this replaces. Override a
16
+ * field for a scope with:
7
17
  *
8
18
  * ```typescript
9
- * yield* Config.around({ timeout: () => 30_000 }, { at: "min" });
19
+ * yield* Config.around({ timeoutExec: () => 30_000 }, { at: "min" });
10
20
  * ```
21
+ *
22
+ * Installing at `min` is what lets a nested override win: a block's own
23
+ * `timeout=` outranks the value the command line established for the run.
24
+ * Omitting a field inherits the enclosing value rather than clearing it.
11
25
  */
12
26
  import { type Api } from "@effectionx/context-api";
13
27
  import type { Operation } from "effection";
14
28
  export interface ConfigApi {
15
- /** Shared timeout in milliseconds. */
16
- timeout: number;
29
+ /** Deadline for the entire run, in milliseconds; undefined for none. */
30
+ timeout: number | undefined;
31
+ /** Default timeout for each exec block, in milliseconds; undefined for none. */
32
+ timeoutExec: number | undefined;
33
+ /** Default timeout for each Fetch, in milliseconds; undefined for none. */
34
+ timeoutFetch: number | undefined;
17
35
  }
18
36
  export declare const Config: Api<ConfigApi>;
19
- /**
20
- * The validated contextual timeout. Always a positive, finite number of
21
- * milliseconds a middleware-supplied value that is not valid fails loudly
22
- * here rather than silently disabling or corrupting timeouts downstream.
23
- */
24
- export declare const timeout: Operation<number>;
37
+ /** The validated run deadline. Read by the run boundary and nothing else. */
38
+ export declare const timeout: Operation<number | undefined>;
39
+ /** The validated default timeout for an exec block. */
40
+ export declare const timeoutExec: Operation<number | undefined>;
41
+ /** The validated default timeout for a Fetch. */
42
+ export declare const timeoutFetch: Operation<number | undefined>;
@@ -0,0 +1,18 @@
1
+ /**
2
+ * The duration grammar, in one place.
3
+ *
4
+ * Every timeout a caller or a document writes is spelled the same way: the
5
+ * three CLI options, and the `timeout=` modifier a block declares. A duration
6
+ * is a positive whole number with a unit — `500ms`, `30s`, `5min`, `20min` —
7
+ * or bare digits, which are milliseconds.
8
+ *
9
+ * Nothing here substitutes a value. An empty, zero, negative, or malformed
10
+ * duration is refused where it was written, because the alternative is a run
11
+ * bounded by a number nobody asked for.
12
+ */
13
+ /** Milliseconds, or `undefined` when `text` is not a duration. */
14
+ export declare function asDuration(text: string): number | undefined;
15
+ /** What a rejected duration says, with `label` naming where it was written. */
16
+ export declare function durationError(label: string, text: string): Error;
17
+ /** Milliseconds. Throws when `text` is not a duration this grammar accepts. */
18
+ export declare function parseDuration(text: string, label: string): number;
@@ -0,0 +1,299 @@
1
+ /**
2
+ * `API.Files` — the document filesystem boundary.
3
+ *
4
+ * A document names files with a path relative to the contextual working
5
+ * directory, and every one of those operations arrives here. What is on the
6
+ * other side is a provider's choice: `xmd run` installs a host adapter that
7
+ * resolves those paths in the caller's own filesystem, and `xmd workflow`
8
+ * installs one that resolves them in a logical filesystem owned by a database
9
+ * transaction. Neither is named in the components that call this Api, which is
10
+ * what lets one document mean the same thing under both.
11
+ *
12
+ * The operations are **semantic**, not primitive. `writeTextFile` is a whole
13
+ * replacement — admission, resolution, target classification, parent creation,
14
+ * and commit — rather than a sequence a caller assembles, because assembling it
15
+ * from outside is what would let a path admitted by one provider be used by
16
+ * another. `API.Fs` remains the low-level host surface a host adapter is built
17
+ * on; it is not this boundary.
18
+ *
19
+ * `checkFilePath` is the one exception, and it is deliberately weak: pure path
20
+ * arithmetic, no filesystem access, and nothing usable comes back — no path, no
21
+ * handle, no authority token. `<File>`'s write form calls it to decide whether
22
+ * its children may expand at all, and the later `writeTextFile` repeats the
23
+ * same admission from the same authored path. A check that was skipped,
24
+ * replaced, or answered by another provider therefore authorizes nothing.
25
+ *
26
+ * ## Two kinds of failure
27
+ *
28
+ * An ordinary filesystem condition — missing, a directory, permission denied,
29
+ * no space — comes back as `Err(FilesError)` carrying frozen structural data.
30
+ * The consumer reads that data and selects a sentence from a fixed vocabulary;
31
+ * no message, errno code, resolved path, temporary path, or symlink target
32
+ * crosses this boundary. Cancellation is neither of these: it is not caught and
33
+ * never becomes a Result.
34
+ *
35
+ * A provider that is absent, that refuses an operation, or that breaks its own
36
+ * contract is not a filesystem condition. Those **throw**, with fixed
37
+ * diagnostics and no cause, and they end the execution rather than becoming
38
+ * something a document renders. A run whose Files provider is missing must not
39
+ * quietly reach the host instead.
40
+ *
41
+ * ## Why the data is structural
42
+ *
43
+ * Both the failures and the write outcome carry a plain frozen object under a
44
+ * stable `type` tag, and every consumer recognizes them by parsing that tag
45
+ * rather than with `instanceof`. Two copies of this package can be loaded at
46
+ * once — a repository component resolving its own runtime beside the engine's —
47
+ * and `instanceof` answers false across them, which would turn a provider
48
+ * failure into an unrecognized throw exactly when it matters most.
49
+ */
50
+ import { type Api } from "@effectionx/context-api";
51
+ import type { Operation, Result } from "effection";
52
+ /** The stable discriminant on ordinary filesystem failure data. */
53
+ export declare const FILES_ERROR = "executablemd.runtime.files-error/v1";
54
+ /** The stable discriminant on infrastructure failure data. */
55
+ export declare const FILES_FATAL = "executablemd.runtime.files-fatal/v1";
56
+ /** The stable discriminant on a successful write's outcome data. */
57
+ export declare const FILES_WRITE_SUCCESS = "executablemd.runtime.files-write-success/v1";
58
+ /**
59
+ * The vocabulary an ordinary failure is reported in.
60
+ *
61
+ * A provider maps whatever its platform produced onto one of these before
62
+ * returning. An unmapped condition becomes `operation-failed`, which is a real
63
+ * answer rather than a placeholder: the consumer has a sentence for it, and the
64
+ * unmapped value itself never crosses the boundary.
65
+ */
66
+ export type FilesReason = "empty-path" | "absolute-path" | "lexical-escape" | "resolved-escape" | "missing" | "directory" | "special-file" | "not-directory" | "permission-denied" | "read-only" | "too-many-symlinks" | "path-too-long" | "no-space" | "quota-exhausted" | "cross-device" | "busy" | "too-many-open-files" | "directory-not-empty" | "invalid-pattern" | "operation-failed";
67
+ /** The operations whose failure carries no commit outcome. */
68
+ export type FilesOperation = "check-file-path" | "read" | "glob" | "temporary-directory";
69
+ /** Where a non-write operation stopped. */
70
+ export type FilesPhase = "lexical" | "resolution" | "target" | "access" | "pattern" | "traversal" | "acquire";
71
+ /** Where a write stopped, which is what decides what may be said about the target. */
72
+ export type FileWritePhase = "lexical" | "resolution" | "target" | "parents" | "temporary" | "commit" | "cleanup" | "transaction";
73
+ /**
74
+ * What is known about the target afterwards.
75
+ *
76
+ * `commit-unknown` is an answer rather than a missing one: a commit that threw
77
+ * may have run or not, and no provider can tell which from where it stands.
78
+ * What still holds in that case is that the target is one complete version.
79
+ */
80
+ export type FileWriteTarget = "unchanged" | "commit-unknown" | "committed" | "rolled-back";
81
+ export interface FilesFailureData {
82
+ readonly type: typeof FILES_ERROR;
83
+ readonly operation: FilesOperation;
84
+ readonly phase: FilesPhase;
85
+ readonly reason: FilesReason;
86
+ }
87
+ export interface FileWriteFailureData {
88
+ readonly type: typeof FILES_ERROR;
89
+ readonly operation: "write";
90
+ readonly phase: FileWritePhase;
91
+ readonly reason?: FilesReason;
92
+ readonly cleanup?: FilesReason;
93
+ readonly target: FileWriteTarget;
94
+ }
95
+ export type FilesErrorData = FilesFailureData | FileWriteFailureData;
96
+ /**
97
+ * The message every ordinary failure carries.
98
+ *
99
+ * Constant on purpose. A message is the part of an Error that gets printed by
100
+ * accident, and there is nothing safe to put in this one: the authored path
101
+ * belongs to the consumer that wrote it, and everything else belongs to the
102
+ * platform.
103
+ */
104
+ export declare const FILES_ERROR_MESSAGE = "Files operation failed";
105
+ /** An ordinary filesystem failure. What it means is in `data`, never in the message. */
106
+ export declare class FilesError extends Error {
107
+ readonly data: FilesErrorData;
108
+ constructor(data: FilesErrorData);
109
+ }
110
+ export interface FileWriteSuccess {
111
+ readonly type: typeof FILES_WRITE_SUCCESS;
112
+ readonly publication: "host-committed" | "transaction-staged";
113
+ }
114
+ export interface FilePathInput {
115
+ readonly cwd: string;
116
+ readonly path: string;
117
+ }
118
+ export interface FileWriteInput extends FilePathInput {
119
+ readonly content: string;
120
+ }
121
+ export interface GlobInput {
122
+ readonly cwd: string;
123
+ readonly include: string[];
124
+ readonly exclude: string[];
125
+ }
126
+ export interface FilesHandler {
127
+ /**
128
+ * Whether this authored path is admissible at all, decided from the path and
129
+ * `cwd` alone. No filesystem access, and nothing usable comes back.
130
+ */
131
+ checkFilePath(input: FilePathInput): Operation<Result<void>>;
132
+ readTextFile(input: FilePathInput): Operation<Result<string>>;
133
+ writeTextFile(input: FileWriteInput): Operation<Result<FileWriteSuccess>>;
134
+ /** Sorted, deduplicated, POSIX-separated paths of the regular files that match. */
135
+ globFiles(input: GlobInput): Operation<Result<string[]>>;
136
+ /**
137
+ * A directory that lives as long as the acquiring scope. A resource, so the
138
+ * caller holds it by acquisition rather than by remembering to remove it.
139
+ */
140
+ temporaryDirectory(): Operation<Result<string>>;
141
+ }
142
+ /** The operations a provider may refuse outright rather than fail at. */
143
+ export type FilesDeniableOperation = "temporary-directory";
144
+ /**
145
+ * Which contract a provider broke.
146
+ *
147
+ * `authority` — the identity authorizing access is stale, foreign, or gone.
148
+ * `savepoint` — a nested transaction could not be rolled back or released.
149
+ * `protocol` — a handler threw, or returned data no consumer can trust.
150
+ * `teardown` — cleanup failed while the scope was already unwinding.
151
+ */
152
+ export type FilesInvariantCategory = "authority" | "savepoint" | "protocol" | "teardown";
153
+ /**
154
+ * Infrastructure failure data.
155
+ *
156
+ * Three kinds, each with fixed fields and nothing derived from the condition
157
+ * that produced it. A category is control data for a consumer deciding what to
158
+ * fence, not text: no diagnostic interpolates it.
159
+ */
160
+ export type FilesFatalData = {
161
+ readonly type: typeof FILES_FATAL;
162
+ readonly kind: "provider-unavailable";
163
+ } | {
164
+ readonly type: typeof FILES_FATAL;
165
+ readonly kind: "operation-denied";
166
+ readonly operation: FilesDeniableOperation;
167
+ } | {
168
+ readonly type: typeof FILES_FATAL;
169
+ readonly kind: "invariant";
170
+ readonly category: FilesInvariantCategory;
171
+ };
172
+ export declare const FILES_PROVIDER_UNAVAILABLE_MESSAGE = "Files provider is not installed";
173
+ export declare const FILES_OPERATION_DENIED_MESSAGE = "Files provider does not support temporary-directory";
174
+ export declare const FILES_INVARIANT_MESSAGE = "Files provider invariant failed";
175
+ /** No Files provider is installed, and there is no host to fall back to. */
176
+ export declare class FilesProviderUnavailableError extends Error {
177
+ readonly data: FilesFatalData;
178
+ constructor();
179
+ }
180
+ /** The installed provider does not implement this operation at all. */
181
+ export declare class FilesOperationDeniedError extends Error {
182
+ readonly data: FilesFatalData;
183
+ constructor(operation: FilesDeniableOperation);
184
+ }
185
+ /** A provider broke its own contract. */
186
+ export declare class FilesInvariantError extends Error {
187
+ readonly data: FilesFatalData;
188
+ constructor(category: FilesInvariantCategory);
189
+ }
190
+ /** An infrastructure failure, recognized structurally rather than by class. */
191
+ export interface FilesFatalFailure extends Error {
192
+ readonly data: FilesFatalData;
193
+ }
194
+ /**
195
+ * The vocabularies, for a provider that reads a failure back out of storage.
196
+ *
197
+ * A transaction-bound provider retains what it refused rather than a serialized
198
+ * error, so restoring one means turning stored text back into the vocabulary.
199
+ * Parsing it here is what keeps one list of reasons and phases: a provider that
200
+ * declared its own copy would be a second list to keep in agreement with this.
201
+ */
202
+ export declare function parseFilesReason(value: unknown): FilesReason | undefined;
203
+ export declare function parseFilesPhase(value: unknown): FilesPhase | undefined;
204
+ export declare function parseFileWritePhase(value: unknown): FileWritePhase | undefined;
205
+ /**
206
+ * The infrastructure failure data this Error carries, if it carries valid data.
207
+ *
208
+ * Every field is checked, the member count with them, and that the object is
209
+ * frozen: extra keys are not the shape this contract describes, and a mutable
210
+ * one is not the shape a constructor here produces. Accepting either would let
211
+ * a provider smuggle a path or a message through under a recognized tag.
212
+ */
213
+ export declare function parseFilesFatal(error: unknown): FilesFatalData | undefined;
214
+ /**
215
+ * Whether this failure satisfies the whole public infrastructure-failure
216
+ * contract, not merely the tag.
217
+ *
218
+ * Recognition decides two different things at once, and the second is why this
219
+ * is stricter than `parseFilesFatal`. A recognized failure is **rethrown by
220
+ * identity** — the object that was thrown is the object a fail-stop records —
221
+ * so recognizing one is a decision to let that exact object travel onward. An
222
+ * Error that carries the right data but also a raw platform message, or a cause
223
+ * chain holding an errno and a path, would then carry all of that past the
224
+ * boundary the reason vocabulary exists to hold.
225
+ *
226
+ * So the whole object has to match what a constructor here produces: the fixed
227
+ * name and diagnostic for its kind, frozen structural data with exactly the
228
+ * fields the kind describes, no cause, and no other enumerable member — string
229
+ * or symbol. Anything else is a candidate that fails the contract, and
230
+ * `invokeFiles` replaces it with a fresh invariant rather than preserving it.
231
+ *
232
+ * Structural throughout, so a failure constructed by a separately loaded copy
233
+ * of this package is recognized on exactly the same terms as one constructed
234
+ * here — `instanceof` answers false across two copies, which is the case this
235
+ * has to survive. That is also why the `name` is checked rather than the class:
236
+ * a second copy's constructor is a different function producing the same name.
237
+ */
238
+ export declare function isFilesFatal(error: unknown): error is FilesFatalFailure;
239
+ /**
240
+ * The infrastructure failure this one is, by identity.
241
+ *
242
+ * The original object comes back rather than a replacement, because a fail-stop
243
+ * that records "the first error" has to record the one that was thrown.
244
+ */
245
+ export declare function asFilesFatal(error: unknown): FilesFatalFailure | undefined;
246
+ /** Build an ordinary non-write failure. */
247
+ export declare function filesFailure(input: {
248
+ operation: FilesOperation;
249
+ phase: FilesPhase;
250
+ reason: FilesReason;
251
+ }): FilesError;
252
+ /**
253
+ * Build a write failure, refusing any combination a consumer could not read.
254
+ *
255
+ * A write's report is the only place a document learns what became of a file it
256
+ * asked to replace, so an invalid combination is a provider bug rather than a
257
+ * value to pass along and interpret later.
258
+ */
259
+ export declare function fileWriteFailure(input: {
260
+ phase: FileWritePhase;
261
+ reason?: FilesReason;
262
+ cleanup?: FilesReason;
263
+ }): FilesError;
264
+ /**
265
+ * The non-write failure data this error carries, if it carries valid data.
266
+ *
267
+ * Malformed data is not fatal here — the consumer already has a sentence for
268
+ * "the operation failed" and nothing about a target is at stake — so this
269
+ * simply declines to recognize it.
270
+ */
271
+ export declare function parseFilesFailure(error: unknown): FilesFailureData | undefined;
272
+ /**
273
+ * The write failure data this error carries, if it carries valid data.
274
+ *
275
+ * Unlike a non-write failure, malformed data here has no safe reading: every
276
+ * sentence a consumer could print makes a claim about whether the file was
277
+ * replaced. A caller treats `undefined` from a write as a protocol invariant
278
+ * rather than inventing a commit state.
279
+ */
280
+ export declare function parseFileWriteFailure(error: unknown): FileWriteFailureData | undefined;
281
+ /** A successful write's outcome. */
282
+ export declare function fileWriteSuccess(publication: FileWriteSuccess["publication"]): FileWriteSuccess;
283
+ /**
284
+ * The write outcome this value is, if it is a valid one.
285
+ *
286
+ * A malformed success is as untrustworthy as a malformed failure: a provider
287
+ * that cannot describe what it did may not have done it, so a caller treats
288
+ * `undefined` here as a protocol invariant too.
289
+ */
290
+ export declare function parseFileWriteSuccess(value: unknown): FileWriteSuccess | undefined;
291
+ /**
292
+ * The document filesystem Api.
293
+ *
294
+ * The terminal handler throws for every operation, including `checkFilePath`.
295
+ * A default that reached the host would make an uninstalled provider
296
+ * indistinguishable from an installed one, and the whole point of the boundary
297
+ * is that a workflow run cannot silently touch the caller's filesystem.
298
+ */
299
+ export declare const Files: Api<FilesHandler>;