Skip to content
Merged
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
14 changes: 13 additions & 1 deletion architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ Existing documents and code get aligned to this section retroactively.
| stop reason | why a workflow run or a document execution stopped: a categorical host code, or a reference to an already-filtered journal event |
| run ID | an opaque stable public identifier generated by the host or selected by an authorized caller; it associates the run's durable records and effects, remains unchanged for the life of the run, and has no semantics beyond equality and lifecycle addressing |
| definition base | the Git revision supplied to choose a workflow definition's pinned commit |
| Repository base | the optional Git revision from which one named Workspace Repository initializes its primary checkout |
| Repository base | the optional Git revision from which one named Repository initializes its primary checkout |
| Repository selection | plain structural composition data naming the repository one component invocation acts on: an opaque provider-minted selection identifier, the display name, the credential-free repository identity, and the selected checkout path. It carries no credential, provider handle, lock, database, run ID or authority — the installed provider authenticates every selection against private state before it touches Git or a service, so a copied, replaced or rebuilt one can misname a target and be refused but can never reach one |
| pinned commit | the commit obtained by resolving a base once; it remains the workflow run's starting repository state even as the run creates descendant commits |
| document target | an addressable static heading in a root document's own Markdown flow, named by the canonical path of heading labels that reaches it; selecting one executes the preamble, each ancestor's own content, and that heading's complete subtree |
| Prompt | a person's original request, in ordinary natural language. `xmd plan` takes exactly one |
Expand Down Expand Up @@ -2379,6 +2380,16 @@ hidden inside library objects that accumulate. One exception: metadata an
author declares at module evaluation, about a value the author owns, may live
on that value.

A Repository selection is composition data and is therefore replaceable: a
document may bind one, render one, hand one to a child, and construct one that
looks exactly like it. Nothing a repository provider does is authorized by the
value it was handed. What stays provider-owned, in the provider's own closure,
is everything a selection is *not*: the canonical Git identity each selection
resolves to, the retained rows behind it, and the credentials and locators used
to reach a service. A selection that the provider did not mint, or one whose
name, checkout path or identity was edited after it did, is refused before Git
or a service is touched.

### Definition-owned return state

A value body — a value root, or a Markdown component that declares `returns` —
Expand Down Expand Up @@ -3759,6 +3770,7 @@ Status is measured against main.
| `useWorkflowServiceDenial()` | provides a non-delegating workflow service denial provider, installed inside every start and resume execution scope | built on the #366 stack |
| `xmd workflow start` / `xmd workflow resume` | starts or resumes a workflow run from the CLI, under the Deno entrypoints only | built on the #366 stack; both acquire #367's executor lock before any lifecycle transition |
| implicit workflow Workspace | retains provider-neutral filesystem, repository and attachment state by run ID | document filesystem built on the #366 stack and Repository/Worktree composition on the #293 stack; process capabilities unbuilt (#218) |
| repository composition vocabulary | one array of thirteen ordinary, shadowable registrations — `Repository`, `Worktree`, `Dir`, the four `Git.*` operations, `PullRequest` and its three evidence reads, `IssueTracker` and `Issue` — declared once and consumed by the workflow attachment, so one vocabulary is described and resolved wherever it is installed. Registering it installs no provider, discovers no repository, acquires no lock, spawns no Git and reads no credential; what a name does is the installed provider's. A repository-local Markdown or TypeScript component of the same name is chosen ahead of any of them | built on the #643 stack |
| `<Repository>` / `<Worktree>` / `<Dir>` composition | names a Git repository and its linked checkouts inside the run-owned Workspace, installs each as contextual working directory, and retains creation identity beside the retained Git bytes | built on the #293 stack, Deno provider only |
| transactional Git effects (`Git.Switch` / `Git.Add` / `Git.Commit`) | publish local Git mutations with their journal result; the enclosing Repository and the contextual working directory select which retained checkout one runs in, and neither observation carries authority — the observed record is compared with the retained row and the directory with the checkouts that row holds, so a failure of authority, of retained state or of an unrecognized native condition fails the run instead of publishing a result | built on the #294 stack, Deno provider only |
| `Git.Push` | publishes the selected checkout's exact current named branch and commit to the same branch on the retained Repository's canonical `origin`, reconciled through the shared Git-host state machine rather than through a Workspace transaction: no props and no component result, no force, no upstream mutation and no implicit staging or committing; the durable request and record carry the Repository's filtered identity without its checkout path, and the transport runs in a provider-owned isolated control repository reading the checkout's objects through an object-source attachment whose alternates chain and object tree are proven contained before the first remote observation, aimed at the exact private retained locator. A destination proven absent is published to once and one already naming this exact commit is adopted; one naming a distinct commit that same authenticated source proves is in this commit's ancestry is a performable pre-state, published over by the same exact non-force refspec and retained as the predecessor with the attested relation, while a divergent commit and one the source cannot read are both conflicts and nothing is fetched to decide either; a completed Push is reconstructed from the Workspace root its own journal event was appended against, read without publishing it or moving the run's frontier, so a branch published more than once resumes | built on the #370 stack, Deno provider only |
Expand Down
17 changes: 11 additions & 6 deletions packages/workflow/mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,25 +146,21 @@ export type {
} from "./src/composition/git-records.ts";
export {
destinationRefFor,
filteredRepositoryIdentity,
GIT_PUSH,
gitPushInputsJson,
gitPushNaturalKeyJson,
gitPushObservationsJson,
gitPushPreStateJson,
gitPushRepositoryIdentityJson,
gitPushResultJson,
parseGitPushInputs,
parseGitPushNaturalKey,
parseGitPushObservations,
parseGitPushPreState,
parseGitPushRecord,
parseGitPushRepositoryIdentity,
parseGitPushResult,
PUSH_REMOTE,
pushExpectation,
refspecFor,
sameRepositoryIdentity,
} from "./src/composition/git-push-records.ts";
export type {
GitPushExpectation,
Expand All @@ -173,7 +169,6 @@ export type {
GitPushObservations,
GitPushOutcome,
GitPushPreState,
GitPushRepositoryIdentity,
GitPushRequest,
GitPushResult,
} from "./src/composition/git-push-records.ts";
Expand Down Expand Up @@ -216,7 +211,10 @@ export type {
PullRequestUpdateKey,
} from "./src/composition/pull-request-records.ts";
export { admitPushEvidence } from "./src/composition/push-evidence.ts";
export { useCompositionComponents } from "./src/composition/installation.ts";
export {
COMPOSITION_REGISTRATIONS,
useCompositionComponents,
} from "./src/composition/installation.ts";

export { ISSUE_API, IssueApi, NoIssueProvider } from "./src/issue/api.ts";
export type {
Expand Down Expand Up @@ -409,6 +407,13 @@ export {
} from "./src/suspension/api.ts";
export type { WorkflowSuspensionApi, WorkflowSuspensionRequest } from "./src/suspension/api.ts";
export { SUSPENSION_ANSWER } from "./src/suspension/answer.ts";
export {
filteredRepositoryIdentity,
parseRepositoryIdentity,
repositoryIdentityJson,
sameRepositoryIdentity,
} from "./src/composition/selection.ts";
export type { RepositoryIdentity } from "./src/composition/selection.ts";
export {
WorkflowAnswerDeliveryError,
WorkflowInputDelivery,
Expand Down
117 changes: 75 additions & 42 deletions packages/workflow/src/composition/api.ts
Original file line number Diff line number Diff line change
@@ -1,76 +1,109 @@
/**
* The provider-neutral Repository/Worktree composition Api.
* The profile-neutral Repository/Worktree composition Api.
*
* Repository and Worktree components ask this Api for work; the installed
* provider decides how to do it. This package names no subprocess, no host
* filesystem and no runtime: a workflow host with the authority to touch a Git
* checkout — the Deno host, for now — installs a concrete provider inside its
* `withWorkflowWorkspace()` attachment.
* filesystem and no runtime: whichever host has the authority to touch a Git
* checkout installs a concrete provider, and there are two of them. A workflow
* host installs one inside its `withWorkflowWorkspace()` attachment, where a
* checkout is a retained Workspace root; the Deno and compiled `xmd run`
* entrypoints install another, where a checkout is a directory on the caller's
* own filesystem held open by an advisory lock.
*
* Each component performs two steps against it, and the split is what makes
* replay work. **Creation** is the durable half: it clones, resolves, pins and
* retains, and a completed one restores from the journal without contacting
* anything. **Attachment** is the ephemeral half: it runs every time, live or
* replayed, and its job is to rebuild the live facade and check that the
* retained state the journal selected is still the state that is there. Partial
* replay therefore reattaches without recreating, and a retained checkout that
* has gone missing is discovered where it can still stop the run.
* Every operation answers with a {@link RepositorySelection} — plain structural
* data naming a target, carrying no authority. What a provider does with a
* selection it is handed afterwards is authenticate it against private state,
* so a selection that was copied, replaced or rebuilt can misname a target and
* be refused; it cannot reach one.
*
* The default handler throws. There is no in-memory fallback: a Repository that
* "starts" without a provider would retain nothing while claiming it had.
* "starts" without a provider would retain nothing while claiming it had, and a
* host that installs none must be distinguishable from one whose repository is
* merely not there.
*/

import { type Api, createApi } from "@effectionx/context-api";
import type { Operation } from "effection";
import { RepositoryCompositionProviderError } from "./errors.ts";
import type {
RepositoryCreationRequest,
RepositoryRecord,
WorktreeCreationRequest,
WorktreeRecord,
} from "./records.ts";
import type { RepositorySelection } from "./selection.ts";

/**
* What a `<Repository>` invocation asks the provider to select.
*
* Parsed at the component boundary from the caller's props and expressions. The
* provider receives only the bytes it acts on; the locator is still raw here,
* because admitting it is the provider's job and refusing an unusable one is
* one of the answers it gives.
*/
export interface RepositoryRequest {
readonly name: string;
readonly locator: string;
readonly base: string | undefined;
}

/**
* What a `<Worktree>` invocation asks the provider to select.
*
* The Repository is the selection the enclosing lexical `<Repository>` — or the
* ambient one — already produced, rather than a name from props: a Worktree
* exists inside a Repository, and letting a document write the name would let it
* name a Repository that is not in scope.
*/
export interface WorktreeRequest {
readonly name: string;
readonly branch: string;
readonly base: string | undefined;
}

export interface RepositoryCompositionApi {
/**
* Create or restore the named Repository's creation identity.
* Select the Repository this lexical invocation names, creating it when the
* provider has none.
*
* One durable effect. A live first reach authorizes the locator, resolves the
* base once, pins the commit and retains the checkout; a replayed one returns
* what was retained without reaching a remote.
* One operation rather than a creation and an attachment, because a component
* has one question: which repository am I acting on. A workflow provider
* still performs both halves inside it — one durable effect that clones,
* resolves, pins and retains, then an ephemeral reattachment that proves the
* retained state is still there — and a live provider acquires a lease,
* revalidates a compatible reuse and hands back the same directory.
*/
createRepository(request: RepositoryCreationRequest): Operation<RepositoryRecord>;
selectRepository(request: RepositoryRequest): Operation<RepositorySelection>;

/** Select a named linked checkout of an already-selected Repository. */
selectWorktree(
repository: RepositorySelection,
request: WorktreeRequest,
): Operation<RepositorySelection>;

/**
* Rebuild the live facade for a Repository whose creation identity is settled,
* and verify that the Workspace still holds the state that identity names.
* The Repository the host is already standing in, for an element written
* outside a lexical `<Repository>`.
*
* Ephemeral: it appends nothing and is performed on every execution.
* Three answers, and they are three different situations. A selection means
* this profile has an ambient Repository and this invocation is in one. A
* throw means it has ambient Repositories and this invocation is not in one,
* and the sentence says how to run inside one. `undefined` means the profile
* has no such thing at all — a workflow document names its repositories, and
* the component's own refusal is what says so.
*/
attachRepository(record: RepositoryRecord): Operation<void>;

/** Create or restore the named Worktree's creation identity, as one durable effect. */
createWorktree(request: WorktreeCreationRequest): Operation<WorktreeRecord>;

/** Rebuild and verify a settled Worktree's live facade. */
attachWorktree(record: WorktreeRecord): Operation<void>;
ambientRepository(): Operation<RepositorySelection | undefined>;
}

export const RepositoryComposition: Api<RepositoryCompositionApi> =
createApi<RepositoryCompositionApi>("executablemd.workflow.composition.repository", {
// deno-lint-ignore require-yield
*createRepository(_request: RepositoryCreationRequest): Operation<RepositoryRecord> {
*selectRepository(_request: RepositoryRequest): Operation<RepositorySelection> {
throw new RepositoryCompositionProviderError("<Repository>");
},
// deno-lint-ignore require-yield
*attachRepository(_record: RepositoryRecord): Operation<void> {
throw new RepositoryCompositionProviderError("<Repository>");
},
// deno-lint-ignore require-yield
*createWorktree(_request: WorktreeCreationRequest): Operation<WorktreeRecord> {
*selectWorktree(
_repository: RepositorySelection,
_request: WorktreeRequest,
): Operation<RepositorySelection> {
throw new RepositoryCompositionProviderError("<Worktree>");
},
// deno-lint-ignore require-yield
*attachWorktree(_record: WorktreeRecord): Operation<void> {
throw new RepositoryCompositionProviderError("<Worktree>");
*ambientRepository(): Operation<RepositorySelection | undefined> {
throw new RepositoryCompositionProviderError("an element written outside a <Repository>");
},
});
4 changes: 2 additions & 2 deletions packages/workflow/src/composition/components/GitAdd.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ import type { PropsSchema } from "@executablemd/core";
import type { Operation } from "effection";
import type { Json } from "@executablemd/durable-streams";
import { GitComposition } from "../git-api.ts";
import { currentRepository } from "../context.ts";
import { selectedRepository } from "../context.ts";
import { GitOperationAuthorityError, GitOperationError } from "../errors.ts";
import { wellFormedText } from "../parse.ts";

Expand Down Expand Up @@ -132,7 +132,7 @@ export default function* GitAdd(props: Record<string, Json>): Operation<string>
invalid("renders nothing, so it takes no content. Write it as <Git.Add paths=… />.");
}

const repository = yield* currentRepository();
const repository = yield* selectedRepository();
if (repository === undefined) {
throw new GitOperationAuthorityError(
ADD,
Expand Down
4 changes: 2 additions & 2 deletions packages/workflow/src/composition/components/GitCommit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ import type { PropsSchema, ReturnsSchema } from "@executablemd/core";
import type { Operation } from "effection";
import type { Json } from "@executablemd/durable-streams";
import { GitComposition } from "../git-api.ts";
import { currentRepository } from "../context.ts";
import { selectedRepository } from "../context.ts";
import { GitOperationAuthorityError, GitOperationError } from "../errors.ts";
import { wellFormedText } from "../parse.ts";
import { parseGitCommitMessageSource } from "../git-records.ts";
Expand Down Expand Up @@ -188,7 +188,7 @@ export default function* GitCommit(props: Record<string, Json>): Operation<strin
const body = (yield* hasContent()) ? yield* content() : "";
const composed = composeCommitMessage(message, body);

const repository = yield* currentRepository();
const repository = yield* selectedRepository();
if (repository === undefined) {
throw new GitOperationAuthorityError(
COMMIT,
Expand Down
4 changes: 2 additions & 2 deletions packages/workflow/src/composition/components/GitPush.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ import type { PropsSchema } from "@executablemd/core";
import type { Operation } from "effection";
import type { Json } from "@executablemd/durable-streams";
import { GitComposition } from "../git-api.ts";
import { currentRepository } from "../context.ts";
import { selectedRepository } from "../context.ts";
import { GitOperationAuthorityError, GitOperationError } from "../errors.ts";

/** The component name, as a document writes it and as a refusal names it. */
Expand All @@ -75,7 +75,7 @@ export default function* GitPush(_props: Record<string, Json>): Operation<string
);
}

const repository = yield* currentRepository();
const repository = yield* selectedRepository();
if (repository === undefined) {
throw new GitOperationAuthorityError(
PUSH,
Expand Down
4 changes: 2 additions & 2 deletions packages/workflow/src/composition/components/GitSwitch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ import { hasContent } from "@executablemd/core";
import type { Operation } from "effection";
import type { Json } from "@executablemd/durable-streams";
import { GitComposition } from "../git-api.ts";
import { currentRepository } from "../context.ts";
import { selectedRepository } from "../context.ts";
import { GitOperationAuthorityError, GitOperationError } from "../errors.ts";

/** The component name, as a document writes it and as a refusal names it. */
Expand Down Expand Up @@ -88,7 +88,7 @@ export default function* GitSwitch(props: Record<string, Json>): Operation<strin
);
}

const repository = yield* currentRepository();
const repository = yield* selectedRepository();
if (repository === undefined) {
throw new GitOperationAuthorityError(
SWITCH,
Expand Down
Loading
Loading