Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 52 additions & 16 deletions architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,16 @@ or invoking Git. The supplied base must equal the recorded base. Git is not
consulted to compare the current value of a moving branch with the pinned
commit.

A host that has already created the run's storage record installs
`useRetainedWorkflow(run)` instead, with the exact frozen value. Nothing is left
for the execution to decide: it records that value through the same
`workflow_run` durable operation, allocates no identifier and resolves no base,
and every journal state β€” live, truncated and completed β€” requires the recorded
run to agree with the supplied one in run ID, base and pinned commit. A journal
that disagrees in any of them is not this run's journal, and the refusal names
the fields rather than their values. The programmatic `useWorkflow({ base })`
installation is unchanged.

`getWorkflowRun()` returns the frozen `WorkflowRun` for the current document
execution. Every call in one live execution returns the same object. It throws
outside a document execution associated with `useWorkflow()`, and it exposes no
Expand Down Expand Up @@ -432,9 +442,9 @@ one operation does not enlist unrelated durable operations in the same scope.
The Deno adapter establishes the canonical module's journal provenance for each
WorkflowRun journal and retains that exact witness. The generic pre-persistence
guard is policy-neutral and returns an unproven wrapper; the trusted
secret-filter wrapping site preserves provenance explicitly, so a filtered
journal β€” including one wrapped more than once β€” still carries the witness its
source carried. Before it opens a transaction, the adapter requires both the
secret-filter and execution-owned admission wrapping sites preserve provenance
explicitly, so a filtered journal β€” including one wrapped more than once β€” still
carries the witness its source carried. Before it opens a transaction, the adapter requires both the
exact proof executor and the journal provenance from the consumed invocation to
belong to the selected WorkflowRun. An in-memory stream, another run's journal,
a copied property, an ordinary guard, a custom look-alike, or a wrapper another
Expand Down Expand Up @@ -462,13 +472,14 @@ rather than invoking an untracked native Git side effect.

Successful effect coordination finishes the mutation scope, including child
cleanup, before capturing the resulting root. The Deno provider installs that
ordering for its adapter-private Workspace proof operation: the mutation
ordering for its Workspace effect operation: the mutation
savepoint, root publication, filtered routed Yield, and caller-owned transaction
commit form one boundary. The proof filesystem uses the pinned synchronous DOFS
commit form one boundary. The Workspace filesystem uses the pinned synchronous DOFS
entry points for its string and byte-array surface, so cancellation leaves no
eager promise or stream pull able to reach the connection after transaction
authority ends. Public filesystem components and workflow lifecycle commands do
not yet select that operation.
authority ends. The transaction-bound Files provider selects that operation for
every document filesystem read, write and search; workflow lifecycle commands do
not select it yet.

An external provider cannot join that transaction. Prompt, Git push and pull
request effects derive a stable identity from the run and expansion, ask the
Expand Down Expand Up @@ -541,11 +552,11 @@ after SQLite has restored the prior frontier.

Retained roots, manifests and blobs remain indefinitely. Cloudflare garbage
collection is not in the production closure and is never invoked. The provider
exposes no public Workspace mutation effect, history selection or fork
operation at this layer. Its adapter-private coordinator combines one mutation,
immutable-root publication and one filtered journal result atomically for the
provider-level proof; declarative `<File>` and workflow start/resume do not
reach it yet.
exposes no public history selection or fork operation at this layer. Its
coordinator combines one mutation, immutable-root publication and one filtered
journal result atomically, and the transaction-bound Files provider is what
routes a document's `<File>` and `<Glob>` to it. Workflow start and resume do
not reach it yet.

The coordinator treats only errors produced through its private filesystem
adapter's documented path and mutation refusals as journalable operation
Expand Down Expand Up @@ -935,6 +946,25 @@ than implied:

Neither claim covers a native command a document runs.

The workflow provider's operations are durable effects. A read, a write and a
search each carry a description derived from the current expansion, the
operation and the resolved logical path, so one authored element is the same
effect across replays and a document edited to name another file is a different
one. `checkFilePath` is not among them: it is lexical admission, it performs no
effect, and it appends nothing.

A write, the immutable root that results from it, and the filtered journal
result share the one caller-owned transaction. An ordinary refusal rolls its
mutation savepoint back before that result is published, so the retained
outcome describes a Workspace that is exactly what it was, and the reason that
crosses the boundary is selected from the shared vocabulary rather than derived
from anything the filesystem said. Replay restores those recorded outcomes
without performing the mutation, opening a transaction or consulting the current
frontier, which is why a create/delete/create history replays in order. A
temporary directory is refused outright: the provider has no host directory to
hand out, and falling through to the caller's would be the uncontained
filesystem the boundary exists to prevent.

Failure data crosses the boundary as a plain frozen object under a stable tag,
carrying a reason from a fixed vocabulary and the phase it came from. No
message, errno code, resolved path, temporary name, or symlink target crosses.
Expand Down Expand Up @@ -1125,8 +1155,13 @@ It qualifies only while every one of these holds:
- transfer happens only at a trusted wrapping site β€” one installed before any
code the journal's content could influence, delegating to the exact stream it
was handed. A document execution's journal passes through two: the secret
filter and the execution-owned target-admission wrapper. Each transfers only
what its source already had, so an unproven journal stays unproven; and
filter and the execution-owned admission wrapper. That second wrapper holds a
resumed run to its recorded root selection and, in the same read, applies
whatever an installation required of the history through `admitJournal()` β€”
workflow-run identity among them. Requirements are contributed to it rather
than wrapped around it, so no wrapping site is added and none of them is
reachable by middleware. Each transfers only what its source already had, so
an unproven journal stays unproven; and
- it retains no execution, lifecycle, journal content or provider state.

The exception exists because the exact-object, anti-forgery and loaded-copy
Expand Down Expand Up @@ -1188,6 +1223,7 @@ Status is measured against main.
| targeted `xmd run` | reads a file argument as a document reference and executes the one exact target its selector resolved to, replacing the selector before execution rereads the file | built on the #412 stack |
| targeted workflow definition | the V1 workflow definition optionally carries the exact canonical document target, which takes part in definition identity and in compatible reuse | built on the #412 stack; the workflow CLI does not supply one yet |
| `useWorkflow()` / `getWorkflowRun()` | associates one document execution with a workflow run | built on main |
| `useRetainedWorkflow()` | associates one document execution with a run storage already created, requiring exact journal agreement | built on the #366 stack |
| `Git.revParse()` | verifies and resolves one Git revision expression contextually | built on main |
| workflow run storage | creates or compatibly finds one run by public run ID, retains its identity, state, document executions and filtered journal, and validates immutable Workspace roots through one provider-owned connection entry | built on the #365 stack; public workflow execution is unbuilt |
| caller-owned storage transaction | publishes several changes, including journal events, in one transaction nothing else enlists in | built on main |
Expand All @@ -1197,7 +1233,7 @@ Status is measured against main.
| `API.Service` / `startService()` | creates an authenticated, supervised loopback service attachment through a provider-neutral operation | built on main |
| `API.Files` | routes every document filesystem operation to the installed provider, with no host default and structural failure data | built on the #227 stack |
| host Files provider / `useHostFiles()` | resolves document paths in the caller's filesystem, containing them while the host namespace is stable; installed by all four CLI entrypoints | built on the #227 stack |
| transaction-bound Files provider | resolves document paths in the run-owned logical Workspace inside the caller-owned transaction | unbuilt; the adapter is #227's second layer, and workflow effect coordination and CLI reachability remain unbuilt |
| transaction-bound Files provider | resolves document paths in the run-owned logical Workspace inside the caller-owned transaction | built on the #366 stack; CLI reachability remains unbuilt |
| `service=<binding>` | publishes the attachment's endpoint into the live binding overlay for its invocation | built on main |
| `ephemeral eval` | reconstructs live middleware and bindings without a journal entry | built on main |
| `useWorkflowServiceDenial()` | provides and tests a non-delegating workflow service denial provider; #366 will install it in future start and resume scopes | built on main; no workflow CLI execution branch exists yet |
Expand All @@ -1206,7 +1242,7 @@ Status is measured against main.
| Repository / Worktree / transactional Git effects | compose named checkouts and publish local mutations with their journal result | defined in `specs/workflow-workspace-spec.md`, unbuilt |
| workflow inspection and history fork | reads status/history without advancing a run and creates a new run from a checkpoint | defined in `specs/workflow-workspace-spec.md`, unbuilt |
| read-only workflow Agent / generated XMD | lets an Agent inspect a derived view and propose constrained executable changes | defined in `specs/workflow-workspace-spec.md`, unbuilt |
| Deno-local DOFS provider | owns one authoritative SQLite/DOFS connection per run path, captures arbitrary canonical retained roots, privately restores them, and atomically coordinates an adapter-private mutation proof with its filtered Yield | built on the #365 stack; public mutation and workflow lifecycle reachability are unbuilt |
| Deno-local DOFS provider | owns one authoritative SQLite/DOFS connection per run path, captures arbitrary canonical retained roots, privately restores them, and atomically coordinates one Workspace mutation with its filtered Yield | built on the #365 stack; public document filesystem effects route to it on the #366 stack, and workflow lifecycle reachability is unbuilt |
| scoped Worker Shell | executes `just-bash` through the Workspace adapter inside a Deno Worker | containment and effect-transaction POCs complete (#351, #357); production integration unbuilt |
| `<Retry max timeout>` | retry a region until it completes | defined, unbuilt |
| suspension effect | suspend durably | defined, unbuilt |
Expand Down
20 changes: 20 additions & 0 deletions packages/runtime/files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -443,6 +443,26 @@ function phaseOf(value: unknown): FilesPhase | undefined {
return PHASES.find((phase) => phase === value);
}

/**
* The vocabularies, for a provider that reads a failure back out of storage.
*
* A transaction-bound provider retains what it refused rather than a serialized
* error, so restoring one means turning stored text back into the vocabulary.
* Parsing it here is what keeps one list of reasons and phases: a provider that
* declared its own copy would be a second list to keep in agreement with this.
*/
export function parseFilesReason(value: unknown): FilesReason | undefined {
return reasonOf(value);
}

export function parseFilesPhase(value: unknown): FilesPhase | undefined {
return phaseOf(value);
}

export function parseFileWritePhase(value: unknown): FileWritePhase | undefined {
return writePhaseOf(value)?.[0];
}

function invariantCategory(value: unknown): FilesInvariantCategory | undefined {
return INVARIANT_CATEGORIES.find((category) => category === value);
}
Expand Down
3 changes: 3 additions & 0 deletions packages/runtime/mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,10 @@ export {
fileWriteSuccess,
filesFailure,
isFilesFatal,
parseFilesPhase,
parseFilesReason,
parseFileWriteFailure,
parseFileWritePhase,
parseFileWriteSuccess,
parseFilesFailure,
parseFilesFatal,
Expand Down
3 changes: 3 additions & 0 deletions packages/workflow/deno.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,6 @@ export { useWorkflowRunStorage } from "./src/deno/provider.ts";
export type { WorkflowRunStorageOptions } from "./src/deno/provider.ts";
export { hashRunId, workflowRunPath } from "./src/deno/path.ts";
export { APPLICATION_ID, SCHEMA_VERSION } from "./src/deno/schema.ts";
export { withWorkflowWorkspace } from "./src/deno/workspace/host.ts";
export { WORKSPACE_FILE } from "./src/deno/workspace/files.ts";
export { WORKSPACE_ROOT } from "./src/deno/workspace/logical-path.ts";
7 changes: 4 additions & 3 deletions packages/workflow/mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,11 @@
* data rather than from whoever happened to be holding the journal.
*
* ```ts
* import { useWorkflow } from "@executablemd/workflow";
* import { workflowInstallation } from "@executablemd/workflow";
* import { executeInstalled } from "@executablemd/core/host";
* import { execute } from "@executablemd/core";
*
* yield* useWorkflow({ base: "main" });
* yield* executeInstalled(options, [workflowInstallation({ base: "main" })]);
* const execution = yield* execute({ path: "./workflow.md", stream });
* ```
*
Expand All @@ -26,7 +27,7 @@

export { Git, GitRevisionError, revParse } from "./src/git.ts";
export type { GitApi } from "./src/git.ts";
export { getWorkflowRun, useWorkflow } from "./src/run.ts";
export { getWorkflowRun, retainedWorkflowInstallation, workflowInstallation } from "./src/run.ts";
export type { WorkflowRun } from "./src/run.ts";
export { useWorkflowServiceDenial, WorkflowServiceDeniedError } from "./src/service-denial.ts";

Expand Down
20 changes: 18 additions & 2 deletions packages/workflow/src/deno/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ import { readTransaction } from "./reading.ts";
import { initializeSchema, isUninitialized, translateSqliteError, verifySchema } from "./schema.ts";
import { SavepointObservation } from "./savepoints.ts";
import { usePrivateWorkspace } from "./workspace/private.ts";
import type { PrivateWorkspaceOptions } from "./workspace/private.ts";
import { useWorkspaceEffects } from "./workspace/effect.ts";

const INSERT_RUN = `INSERT INTO workflow_run
Expand Down Expand Up @@ -107,14 +108,29 @@ export const WorkflowRunRecognition = createContext<WorkflowRunRecognitionProbe>
* long as the scope that installed the provider and nothing accumulates
* between runs.
*/
export function* useWorkflowRunStorage(options: WorkflowRunStorageOptions): Operation<void> {
export function useWorkflowRunStorage(options: WorkflowRunStorageOptions): Operation<void> {
return installWorkflowRunStorage(options, {});
}

/**
* The same installation, with what only this adapter's own suites supply.
*
* Kept apart from the published entrypoint on purpose: `internal` carries a
* decorator for the authoritative Workspace filesystem, which is provider
* authority rather than host arrangement. It is captured in the provider's
* closure here and never handed to a scope, a context or a descendant.
*/
export function* installWorkflowRunStorage(
options: WorkflowRunStorageOptions,
internal: PrivateWorkspaceOptions,
): Operation<void> {
const root = authorizedRoot(options.root);
const connections = createWorkflowRunConnections(yield* SavepointObservation.get());
yield* ensure(() => {
connections.close();
});
yield* useJournalRouting(connections);
yield* usePrivateWorkspace(connections);
yield* usePrivateWorkspace(connections, internal);
yield* useWorkspaceEffects(connections);

yield* WorkflowRunStorage.around(
Expand Down
2 changes: 1 addition & 1 deletion packages/workflow/src/deno/workspace/effect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ export function withWorkspaceEffects<T>(
});
}

export function createWorkspaceProofEffect<T extends Json>(
export function createWorkspaceEffect<T extends Json>(
database: WorkflowRunDatabase,
description: EffectDescription,
mutate: DenoWorkspaceMutation<T>,
Expand Down
13 changes: 13 additions & 0 deletions packages/workflow/src/deno/workspace/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,16 @@ export function throwWorkspaceFilesystemFailure(error: unknown): never {
export function isJournalableWorkspaceFailure(error: unknown): error is Error {
return error instanceof JournalableWorkspaceFailure;
}

/**
* The documented filesystem condition this failure is, or `undefined` for one
* that is not documented.
*
* The code is the only part of a DOFS failure anything above this module reads.
* Its message, its cause and the paths either of them names stay here, so a
* consumer selecting a `FilesReason` from this receives a condition rather than
* platform text.
*/
export function journalableWorkspaceCode(error: unknown): string | undefined {
return error instanceof JournalableWorkspaceFailure ? error.code : undefined;
}
Loading
Loading