diff --git a/.oxfmtrc.json b/.oxfmtrc.json index 5e2da66e6..76809d287 100644 --- a/.oxfmtrc.json +++ b/.oxfmtrc.json @@ -4,9 +4,14 @@ // the exact indentation the rule tests assert on. // `packages/*/npm` is generated dnt output, written by a test while the // battery runs; format-checking it fails on a file nobody wrote by hand. + // The retained-repository history fixtures are journals an older revision + // emitted, held to the SHA-256 their own receipt records: reserializing one + // changes the bytes the evidence is, and the suite that continues them + // verifies those hashes. "ignorePatterns": [ "**/*.md", "scripts/tests/fixtures/**", + "packages/git/tests/fixtures/retained-repository-history/**", "**/npm/**", "packages/workflow/vendor/cloudflare-computer-dofs/**", "packages/acp/vendor/acpx/**", diff --git a/architecture.md b/architecture.md index e0c9e5261..c5a4174ef 100644 --- a/architecture.md +++ b/architecture.md @@ -5181,13 +5181,67 @@ The bundled Git Plugin declares sixteen ordinary shadowable defaults: `Git.Files`, `Git.Changes`, `Git.Diff`, `Git.File`, `PullRequest`, `PullRequest.Reviews`, `PullRequest.Comments`, `PullRequest.Checks`, `IssueTracker` and `Issue`. Dir is core-protected and -remains available without this Plugin. One declaration array serves execution, +remains available without this Plugin. One declaration source serves execution, syntax inspection, Plan validation/generation and executing workflow actions. A Workspace attachment installs providers and durable state, not vocabulary. Declaration alone starts nothing: no provider, repository discovery, lock, Git process or credential read. A repository component may shadow any of these ordinary defaults. +Six of the sixteen reach something no local transaction encloses — `Git.Push`, +`PullRequest`, its three evidence reads and `Issue` — and each has exactly one +implementation, which asks a semantic feature operation for its work. Every +command that carries this vocabulary registers all sixteen, `repl` included: one +registry is what says which names exist, and it is what syntax inspection, Plan +validation and preflight read. An execution that *retains* one of those +operations — a REPL entry, for what it publishes and what it files — declares +that same name again from inside its own scope with its own policy under it, +through the ordinary trusted installation and the same component module, so a +nearer declaration shadows the registered default in the one place it applies. +The component is the same either way — one body, one set of forms, one return +contract — and a repository component of the name still outranks the default. + +#### One invocation binding for the external repository operations + +`Git.Push`, `PullRequest`, its three evidence reads and `Issue` have one +implementation each, in the provider-neutral half of `@executablemd/git`. Each +of those bodies interprets its own props and forms, expands its own content, +normalizes what the document wrote, asks a semantic feature operation — +`GitApi["push"]`, `IssueOperationsApi` or `PullRequestOperationsApi` — and +projects what came back. None of them receives an authenticator, an owner, a +site, native access, a policy object, a record codec or any publication state, +and none of them names a journal record. + +Between the component and the host is one feature-owned handoff. For a host that +retains work named after an invocation it authenticates the actual invocation +canonical core issued — once, in the engine frame the component was entered in, +before the body projects any content, because a projecting ancestor is +deliberately ineligible for one — hands that site to the host's own policy, and +runs the one body against the operations the policy answers with. The site is +private to that policy: no operation answers with it, no public request carries +it, and no Context holds it. + +A host that retains nothing named after an invocation runs the same bodies over +the public feature operations, with no identity authenticated, because there is +nothing for one to name. Only an operation a host retains *above* the public +chain, named after an invocation it authenticates, takes its operation as an +argument, and that is `Git.Push` and `Issue`. A workflow run retains +`PullRequest` and its three evidence reads too — the upsert through the Git-host +reconciliation beneath it, each read as policy on `PullRequestReadExecution` — +but it installs that retention under their feature Api instead of owning a +boundary over it, and no REPL entry owns one either, so those four ask their Api +directly as any other component does. A REPL entry retains the four Git reads +the same way — `GitQuery` middleware at its own implementation slot — which is +what lets those four stay ordinary registrations under a host that retains every +page they answer with. That is what makes an ordinary run, a workflow run, a +provider-absent runtime and a REPL entry four policies rather than four +components. + +The lifetime of what a policy hands over is the ordinary one: an operation runs +in the scope of the invocation that asked for it, and cancellation, teardown and +the joined cleanup of any attempt beneath it are owned by that scope rather than +by a guard around the handoff. + #### Command declaration ownership Command declarations contain everything one command declares, in one order and under one @@ -7114,10 +7168,11 @@ flag is retained, and host paths are kept as the bytes they arrived as. Identity is the entry's own owner and the invocation identity canonical core minted, encoded in the same two record fields a workflow run uses for its run -and expansion. The trusted body takes that invocation once and owns the pair -throughout; neither reaches public Git input, so a replacement registration and -a direct `Git.operations.push(...)` name no durable work of this entry's and -reach the ordinary missing-provider refusal. +and expansion. The common invocation binding authenticates the invocation once +and hands the site it authenticated to this host's policy, which owns the pair +throughout; neither reaches public Git input or the shared component, so a +replacement registration and a direct `Git.operations.push(...)` name no durable +work of this entry's and reach the ordinary missing-provider refusal. Composition is ordinary. A fresh invocation dispatches through an operation-local default under the stable Git Api name, so every public handler a @@ -7193,20 +7248,22 @@ implementation for this exact invocation. The `runId` and `expansionId` field spellings in the record are a compatibility encoding of those two and nothing more. -Neither member can be supplied from outside. The owner reaches the -implementation as a trusted closure the host built for one entry, the identity -authenticator is delivered to that implementation by canonical execution and -exists only as the argument of that call, and neither appears in a prop, a -tracker, a Context, a middleware request or a journal field a document can -write. The entry's `` is an ordinary shadowable registration of the name -the Plugin already declares, so a repository component of that name keeps its -ordinary precedence — and such a replacement obtains no identity of the entry's. -Which of the two external effects an invocation asks for is settled by canonical -form dispatch from the form the scan recorded, because a composable answer about -an element's shape is not something an irreversible effect may be selected by. +Neither member can be supplied from outside. The owner reaches the host's policy +as a trusted closure built for one entry, the identity authenticator is +delivered to the common binding by canonical execution and exists only as the +argument of that call, and neither appears in a prop, a tracker, a Context, a +middleware request or a journal field a document can write. `` here is +the one shared component with this entry's policy under it: the name is the +registered default the command installed, and the entry declares it again in its +own execution because it retains work named after each invocation — a repository +component of that name keeps its ordinary precedence, and such a replacement +obtains no identity of the entry's. Which of the two external +effects an invocation asks for is settled by canonical form dispatch from the +form the scan recorded, because a composable answer about an element's shape is +not something an irreversible effect may be selected by. One durable boundary encloses the operation, and the live attempt inside it is -composed rather than hard-wired: the trusted implementation dispatches +composed rather than hard-wired: the entry's policy dispatches `IssueOperations`, where a document's own middleware answers, transforms, refuses and delegates as it does anywhere else, over a transport-facing default of the entry's own that reaches `IssueApi` and adds no second envelope. That @@ -8479,7 +8536,7 @@ Status is measured against main. | caller-owned storage transaction | publishes several changes, including journal events, in one transaction nothing else enlists in [Invariant](#one-database-at-a-time-and-a-transaction-a-caller-can-hold). | built on main | | REPL entry retained file work | one executing REPL entry's own recording, established by core initialization in the scope the invocation owns and ending with it: it retains the `` reads, `` writes, `` deletions and `` ensures that land inside a checkout the entry is working in — the ambient one it was started in, or a managed one it selected — authored and admitted-generated alike, because both cross the one document filesystem. A completed operation answers from its saved result without live path canonicalization, ambient discovery, a lease, a permission lookup or a filesystem call — so only *new* work needs the environment, a continuation still binds the text its read answered with while the file says something else, and a new entry reading that path sees what is there now. A record's name is the digest of the record version, the operation kind and the complete received request, so two equal requests in one entry are one name at two ordered positions and the durable engine's positional matching tells them apart; equal work in two entries is the same name, and each entry executing against its own stream segment is what keeps those apart. Recording is ordinary ancestor-max `Files` middleware, so a provider beneath that answers directly or transforms and delegates is kept as provider behavior while the received request stays the operation's identity. A shared Git directory admits one live *mutating* entry at a time, held lazily for as long as the execution lasts and refused rather than queued; no read acquires it, so a new read proceeds while another entry holds it. The local Git and issue work an entry retains have rows of their own beside this one; work outside every checkout the entry is working in stays ordinary and unrecorded, and a passive reading initializes nothing [Reference](#what-one-entry-retains-about-the-checkout-it-is-standing-in). | built on the #181 stack | | REPL entry retained selection | one executing REPL entry's own `` and `` selections, recorded beside its file work as small routing records: the canonical managed root, the checkout root, the common Git directory, the credential-free repository identity, the display name and the owner checkout, with a root-level ``'s ambient owner recorded as its own answer. The selector is the ordinary managed one, with its own creation, compatible-reuse, conflict and interrupted-adoption policy; what this adds is that a completed selection answers from its record, calling no selector and reaching no clone, `worktree add`, sidecar, credential or lock — so a continuation works in a checkout somebody has since changed or removed, while only new work meets today's filesystem. Parsed facts are held to the call they stand at with pure slot arithmetic against the root the record itself carries, registered in entry-private state and minted as a fresh selection, so a copied or edited Repository value can misname a target and never reach one. One entry at a time *owns* a managed slot, held until teardown and reacquired lazily by the first new operation inside a restored placement; that is a different hold from the shared Git directory's mutation lease, which only a new `` selector and a new mutation take. A selection is acknowledged before its path is bound, and native work that was never acknowledged runs live again under the ordinary partial-creation policy [Reference](#what-one-entry-retains-about-the-checkout-it-is-standing-in). | built on the #181 stack | -| REPL entry retained issue work | one executing REPL entry's own `` work, retained under the ordinary `issue_effect` records beside its file work and needing no checkout at all: no repository is discovered for it, no managed root is made and no hold is taken. A completed read answers with the snapshot it saw and a completed upsert with the URL it was filed under, neither resolving a provider, reading configuration nor opening a credential. What names it is the entry's own owner — derived by the session from the execution and the entry's admission-order position — with the identity canonical execution minted where it selected this implementation for this invocation; the `runId` and `expansionId` spellings are a compatibility encoding of those two, and a REPL entry installs no synthetic WorkflowRun. Neither member is reachable from a prop, a tracker, a Context, a middleware request or a record field a document can write, and the entry's `` is an ordinary shadowable registration a repository component still outranks. One durable boundary encloses the operation and the live attempt inside it composes `IssueOperations` over a transport-facing default of the entry's own, bound in the attempt's own scope under the attempt's own idempotency key and gone before the envelope publishes; canonical form dispatch selects which of the two effects an invocation asks for. An upsert the service accepted and this end never acknowledged retains nothing and presents the same key next time, which is what the provider adopts under; a read has no key and may be made again. The entry's retained issue records are held to its own owner before any of them are replayed [Reference](#what-one-entry-retains-about-the-issue-it-filed). | built on the #181 stack | +| REPL entry retained issue work | one executing REPL entry's own `` work, retained under the ordinary `issue_effect` records beside its file work and needing no checkout at all: no repository is discovered for it, no managed root is made and no hold is taken. A completed read answers with the snapshot it saw and a completed upsert with the URL it was filed under, neither resolving a provider, reading configuration nor opening a credential. What names it is the entry's own owner — derived by the session from the execution and the entry's admission-order position — with the identity canonical execution minted where it selected this implementation for this invocation; the `runId` and `expansionId` spellings are a compatibility encoding of those two, and a REPL entry installs no synthetic WorkflowRun. Neither member is reachable from a prop, a tracker, a Context, a middleware request or a record field a document can write, and the entry's `` is the one shared component with this entry's policy declared over the command's registered default, which a repository component still outranks. One durable boundary encloses the operation and the live attempt inside it composes `IssueOperations` over a transport-facing default of the entry's own, bound in the attempt's own scope under the attempt's own idempotency key and gone before the envelope publishes; canonical form dispatch selects which of the two effects an invocation asks for. An upsert the service accepted and this end never acknowledged retains nothing and presents the same key next time, which is what the provider adopts under; a read has no key and may be made again. The entry's retained issue records are held to its own owner before any of them are replayed [Reference](#what-one-entry-retains-about-the-issue-it-filed). | built on the #181 stack | | REPL entry retained local Git | one executing REPL entry's own ``, `` and ``, recorded beside its file work and its selections. The performers are the ordinary ones — exactly the pathspecs a document wrote, the ordinary branch rules, the current index and nothing else — installed as ordinary Git dispatch at the implementation slot, so middleware above may answer, refuse, or transform and delegate, and a call answered wholly by a replacement acquires no retention. One durable operation per call and no outer envelope, so a staging child inside a Commit is an earlier journal operation of its own. A completed operation answers from its saved result and runs no `git` at all, writing no tree and reading no identity; a saved commit object id names the commit that was made then and is never re-resolved against today's HEAD. A result is fixed-size evidence — the checkout, the branch, commit, HEAD tree and index tree on both sides, and the operation's own arguments and products — never a file list, a checkout snapshot or Git output, and a commit message appears as a digest and a byte length rather than a second copy of the text. Routing is private and live-only: the received working directory is canonicalized and the ordinary selector chooses the longest covering checkout among this entry's registrations under the whole authenticated Repository identity, and a saved result's checkout is held to those registrations with parsed facts alone. The invoking person's author and committer are read once per entry, lazily, and only by a genuinely live Commit; a host that cannot say becomes the existing Commit-only refusal. A live operation takes the selected placement's managed slot and then the shared Git directory's mutation lease, keeps both until teardown and takes neither on replay, with this entry's own native attempts serialized per shared Git directory. An ordinary refusal is retained under the route that reported it, so a native ignored-path Add says what `git` reported and promises no rollback; contention, admission and infrastructure failures are fatal and unrecorded. Native completion and acknowledgement are separate: an unacknowledged Commit runs live again and may instead refuse empty-index, and a failed append fences later work with no success binding and no compensating Close. A local result confers no publication evidence, and publication is the row below rather than part of these three [Reference](#the-three-local-git-operations-and-what-a-saved-object-id-means). | built on the #181 stack | | REPL entry retained publication | one executing REPL entry's own ``, retained as a request record written before anything is contacted and the shared `git_host_effect` outcome that answers it. The request is resolved from the checkout — the authenticated selection, its common Git directory, the covering checkout for the received working directory, the admitted credential-free origin held to the identity's own fingerprint, and the branch and commit read inside the entry's native turn — and keeps the question asked, the reading the live dispatch received and what routing resolved, with no selection identifier, source object, credential, Git output, HEAD inventory or publication flag. Identity is the entry's owner and core's minted invocation identity, encoded in the run and expansion fields the workflow profile uses; a replacement registration and a direct `Git.operations.push(...)` reach the ordinary missing-provider refusal. Dispatch is ordinary Git composition through an operation-local default: a synthetic answer retains nothing, a second delegation of one invocation is refused, and a continuation consumes the saved request before any dispatch. A publication takes the managed slot and no mutation lease, reads objects through a bare control repository with a read-only alternate rather than through the checkout, reconciles a pending request against the exact commit it named without fetching, and publishes only after every per-attempt resource has closed. A completed publication reaches no provider, credential or remote; a failed request append contacts nothing and a failed outcome append leaves no false receipt [Reference](#what-one-entry-asked-to-publish-and-what-the-git-host-did-about-it). | built on the #181 stack | | live durable-operation coordinator | explicitly coordinates structured live execution with existing Yield publication while leaving replay and callback effects unchanged [Invariant](#live-effect-coordination). | built on the #365 stack | @@ -8509,7 +8566,7 @@ Status is measured against main. | transactional Git effects (`Git.Switch` / `Git.Add` / `Git.Commit`) under a workflow run | 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 admits anything — the observed record is compared with the retained row and the directory with the checkouts that row holds, so a failure of admission, of retained state or of an unrecognized native condition fails the run instead of publishing a result [Invariant](#workspace-local-transactions); [Interface and evidence](specs/workflow-workspace-spec.md#7-git-operations). | built on the #294 stack, Deno provider only | | `Git.Push` under a workflow run | Publishes or adopts the selected named branch through authenticated non-force reconciliation. [Authored contract](specs/workflow-workspace-spec.md#74-push) and [effect mechanics](#replay-and-effect-transactions). | built on the #370 stack, Deno provider only | | `` under a workflow run | Upserts the exact published branch and returns stable evidence only after this run proves matching Push history. [Props, results and refusals](specs/workflow-workspace-spec.md#75-pull-request); [positive-integer number and reconciliation](#issue-effects). | built on the #295 stack, Deno provider only | -| repository composition vocabulary | Declares sixteen ordinary shadowable defaults without installing an operational provider. [Names and profile ownership](#ordinary-repository-vocabulary). | built on the #643 stack | +| repository composition vocabulary | Declares sixteen ordinary shadowable defaults for every command that carries it, without installing an operational provider; an execution that retains one of the six external operations declares that name again with its own policy over the default. [Names and profile ownership](#ordinary-repository-vocabulary). | built on the #643 stack | | `` / `` composition under an ordinary run | Selects an authenticated persistent checkout under an execution-owned exclusive lock [Full contract](#ordinary-repository-ownership). | built on the #643 stack, Deno and compiled only | | local Git operations (`Git.Switch` / `Git.Add` / `Git.Commit`) under an ordinary run | Performs explicit local transitions against the authenticated checkout without rollback or replay [Full contract](#ordinary-repository-ownership). | built on the #643 stack, Deno and compiled only | | `Git.Push` under an ordinary run | Publishes or adopts a non-force branch result and keeps invocation-private evidence [Full contract](#ordinary-repository-ownership). | built on the #643 stack, Deno and compiled only | diff --git a/packages/cli/src/plugin-host.ts b/packages/cli/src/plugin-host.ts index c67fab63a..ce3b5c2c0 100644 --- a/packages/cli/src/plugin-host.ts +++ b/packages/cli/src/plugin-host.ts @@ -25,6 +25,7 @@ import { incompleteStructural } from "@executablemd/core/host"; import type { ExecutionDeclaration, ExecutionInstallation, + IdentityComponent, IncompleteStructural, } from "@executablemd/core/host"; diff --git a/packages/cli/src/repl/session.ts b/packages/cli/src/repl/session.ts index 79c680bce..548b99fbc 100644 --- a/packages/cli/src/repl/session.ts +++ b/packages/cli/src/repl/session.ts @@ -1651,6 +1651,11 @@ function* preflight( return yield* validateDocument({ ...inlineSource(source), includes, + // The command's own declarations. A Plugin's vocabulary is registered in + // the scope this session was opened in, so the names an entry may write + // are already resolvable here — and metadata alone: validation resolves a + // name from a declaration and calls no factory, so nothing initializes + // policy. components: declaredComponents(installations), declarations: declaredMarkdown(installations), }); diff --git a/packages/cli/src/syntax.ts b/packages/cli/src/syntax.ts index 45de9fdc4..f7340b542 100644 --- a/packages/cli/src/syntax.ts +++ b/packages/cli/src/syntax.ts @@ -100,6 +100,9 @@ export function* useCommandSyntax( const inputs = { includes: [...includes], registry: new Map(yield* Component.operations.registry), + // The engine's own invocation-named components. A Plugin's vocabulary is + // in the registry this captured above, so describing it reaches no + // identity: inspection reads metadata and calls no factory. components: agentIdentityComponents(), // `` is part of the run profile, so symbols that left it out would // describe a vocabulary no run has. Described from the packaged bytes: diff --git a/packages/cli/tests/repl-issues.test.ts b/packages/cli/tests/repl-issues.test.ts index ea80d91e0..6ea275e97 100644 --- a/packages/cli/tests/repl-issues.test.ts +++ b/packages/cli/tests/repl-issues.test.ts @@ -100,7 +100,10 @@ function useReplRepositories(root: string): Operation { * What a command contributes to an entry's execution. * * Taken from the Git Plugin's own contribution for the `repl` command rather - * than written out here, because that contribution is the production path. + * than written out here, because that contribution is the production path — and + * called in the scope the session is opened in, because installing it is what + * registers the vocabulary. `` resolves in preflight and in the entry + * through that registry, with no component catalog supplied beside it. */ function* replInstallations(): Operation { const install = gitPlugin.install; @@ -1485,9 +1488,11 @@ describe("how a REPL entry gets its issues", () => { }); expect(absent.entryInstallation).toBe(undefined); - // Selecting reaches nothing, and what it selected declares both halves of - // an entry's external capability: an installation is built per entry, and - // this command has run none. + // Selecting reaches nothing, and what it selected declares exactly the + // operations an entry retains — publication and filing, and neither + // `` nor its reads, which stay the defaults the command's + // scope registered. An installation is built per entry, and this command + // has run none. const built = yield* scoped(function* (): Operation { if (factory === undefined) { throw new Error("the production selector selected no entry capability"); diff --git a/packages/cli/tests/repl-push.test.ts b/packages/cli/tests/repl-push.test.ts index f00a50917..4e6eb1cf5 100644 --- a/packages/cli/tests/repl-push.test.ts +++ b/packages/cli/tests/repl-push.test.ts @@ -133,7 +133,14 @@ function useWorkingDirectory(directory: string): Operation { ); } -/** What the Git Plugin contributes for the `repl` command. */ +/** + * What the Git Plugin contributes for the `repl` command. + * + * Called in the scope the session is opened in, because installing is what + * registers the vocabulary: `` resolves in preflight and in the + * entry through this registry, exactly as it does under the real command, and + * this harness supplies no component catalog of its own. + */ function* replInstallations(): Operation { const install = gitPlugin.install; if (install === undefined) { @@ -424,8 +431,8 @@ describe("how a REPL entry gets its publication capability", () => { } // Building one reaches nothing — no directory, no `git`, no credential and - // no managed root — and what it declares is both of an entry's external - // capabilities. + // no managed root — and what it declares is the two operations this entry + // retains, under names the command's scope has already registered. const built = yield* scoped(function* (): Operation { return yield* factory('["executablemd.repl.issue-owner",1,"execution-cli-6","1"]'); }); diff --git a/packages/git/deno.ts b/packages/git/deno.ts index 91545603f..562e85940 100644 --- a/packages/git/deno.ts +++ b/packages/git/deno.ts @@ -64,10 +64,10 @@ export { replRepositoryInstallation } from "./src/deno/retained-repository/provi /** * One REPL entry's issue capability, as the inert value the CLI selects. * - * The installation alone. What it builds — the entry's own `` - * implementation, the identity behind it, the admission over its history and - * the transport-facing default inside a live attempt — stays inside it: a - * package that could reach one of those could file an issue under an entry it - * does not own. + * The installation alone. What it builds — the common `` declaration, + * the policy behind it, the admission over its history and the + * transport-facing default inside a live attempt — stays inside it: a package + * that could reach one of those could file an issue under an entry it does not + * own. */ export { replIssueInstallation } from "./src/deno/retained-repository/issues.ts"; diff --git a/packages/git/mod.ts b/packages/git/mod.ts index 88c3390ac..d709ab2b4 100644 --- a/packages/git/mod.ts +++ b/packages/git/mod.ts @@ -90,6 +90,14 @@ export { gitDirectorySelection } from "./src/composition/definitions.ts"; * one this package ships. */ export { declaresFor as gitPluginDeclaresFor } from "./src/plugin.ts"; +/** + * Whether this command's executions install their own policy under the six + * operations no local transaction encloses, rather than registering them. + * + * Named for the Plugin it classifies, for the same reason as the predicate + * above: a host assembling a command needs this answer and the vocabulary's own + * gating to agree. + */ export { GitObjectError, GitRepositoryError, GitRevisionError } from "./src/git.ts"; export type { GitObjectFormat } from "./src/git.ts"; export { diff --git a/packages/git/src/composition/components/GitPush.ts b/packages/git/src/composition/components/GitPush.ts index f07ce248b..c491d5da8 100644 --- a/packages/git/src/composition/components/GitPush.ts +++ b/packages/git/src/composition/components/GitPush.ts @@ -2,31 +2,31 @@ * Publish the contextual checkout's current branch to its Repository origin * (specs/workflow-workspace-spec.md §7.4). * - * `` takes no props or content, renders nothing and binds nothing. * The enclosing Repository and cwd select the checkout, including a linked - * worktree reached through Dir. Remote, branch, commit, refspec and force are not - * authored inputs; reconciliation evidence stays in the journal. - * - * The remote cannot join the local transaction. Shared Git-host reconciliation - * observes first, adopts this exact commit, publishes a proven absence or advances - * a proven ancestor once, and refuses unproven compatibility. It never force-pushes - * or changes upstream tracking. - * - * Git.Switch's failure policy applies. Detached HEAD is a fixed GitOperationError - * before remote contact; missing Repository is an admission failure that printing - * cannot recover. + * worktree reached through Dir. Remote, branch, commit, refspec and force are + * not authored inputs, and the remote cannot join the local transaction: what + * reconciliation proves before it publishes belongs to the host policy beneath + * this body and stays in the journal. */ import { cwd } from "@executablemd/runtime"; -import { hasContent } from "@executablemd/core"; -import type { FirstPartyDocumentation, PropsSchema } from "@executablemd/core"; +import { documented, formDispatcher } from "@executablemd/core"; +import type { + ComponentRegistration, + FirstPartyDocumentation, + FunctionComponent, + PropsSchema, +} from "@executablemd/core"; +import type { IdentityClaimant, IdentityComponent } from "@executablemd/core/host"; import type { Operation } from "effection"; import type { Json } from "@executablemd/durable-streams"; -import { Git } from "../git-api.ts"; +import type { GitApi } from "../git-api.ts"; +import { COMPOSITION_ORIGIN } from "../definitions.ts"; import { selectedRepository } from "../context.ts"; import { GitOperationAdmissionError, GitOperationError } from "../errors.ts"; +import { boundComponent } from "../invocation-binding.ts"; +import type { BindOperations, ComponentBody } from "../invocation-binding.ts"; -/** The component name, as a document writes it and as a refusal names it. */ export const PUSH = ""; export const props: PropsSchema = { @@ -35,13 +35,6 @@ export const props: PropsSchema = { additionalProperties: false, }; -/** - * What a reader is told about this component, in one place. - * - * Declared here rather than beside a registration because there are two: the - * bundled vocabulary's, and the one a REPL entry installs with its own identity - * behind it. Inspection describes one `Git.Push`, so both read this. - */ export const documentation: FirstPartyDocumentation = { description: `Publish the current branch. \`\` pushes its current commit to the same branch ` + @@ -50,15 +43,10 @@ export const documentation: FirstPartyDocumentation = { context: null, }; -export default function* GitPush(_props: Record): Operation { - if (yield* hasContent()) { - throw new GitOperationError( - PUSH, - "invalid-invocation", - `${PUSH} renders nothing, so it takes no content. Write it as .`, - ); - } - +export const body: ComponentBody = function* GitPush( + _props: Record, + push: GitApi["push"], +): Operation { const repository = yield* selectedRepository(); if (repository === undefined) { throw new GitOperationAdmissionError( @@ -67,10 +55,44 @@ export default function* GitPush(_props: Record): Operation + new GitOperationError( + PUSH, + "invalid-invocation", + `${PUSH} renders nothing, so it takes no content. Write it as .`, + ), }); - return ""; +} + +/** An ordinary registered default, over whichever provider the host installed. */ +export function pushRegistration(push: GitApi["push"]): ComponentRegistration { + return { ...METADATA, fn: dispatched((props) => body(props, push)) }; +} + +/** A declaration, for an execution that owns this operation's policy. */ +export function pushDeclaration(bind: BindOperations): IdentityComponent { + return { + ...METADATA, + factory: (claim: IdentityClaimant) => dispatched(boundComponent(claim, bind, body)), + }; } diff --git a/packages/git/src/composition/components/Issue.ts b/packages/git/src/composition/components/Issue.ts index 24566055f..c5eb26e25 100644 --- a/packages/git/src/composition/components/Issue.ts +++ b/packages/git/src/composition/components/Issue.ts @@ -18,31 +18,40 @@ * refusals; provider conflicts, ambiguity, availability and ceiling failures keep * the provider's own vocabulary. * - * The three parts below the component — which form this is, what a read asks for - * and what an upsert asks for — are shared with the trusted implementation a - * REPL entry installs, which asks the same questions of the same props and then - * retains the answer under its own identity. Two copies of these checks would - * be two vocabularies for one component. + * The host beneath this component decides how the operation it asks for is + * performed, retained and replayed. */ -import { content, hasContent } from "@executablemd/core"; -import type { FirstPartyDocumentation, PropsSchema, ReturnsSchema } from "@executablemd/core"; +import { content, documented, formDispatcher } from "@executablemd/core"; +import type { + ComponentRegistration, + FirstPartyDocumentation, + FunctionComponent, + PropsSchema, + ReturnsSchema, +} from "@executablemd/core"; +import type { IdentityClaimant, IdentityComponent } from "@executablemd/core/host"; import type { Operation } from "effection"; import type { Json } from "@executablemd/durable-streams"; +import { COMPOSITION_ORIGIN } from "../definitions.ts"; +import { boundComponent } from "../invocation-binding.ts"; +import type { BindOperations, ComponentBody } from "../invocation-binding.ts"; import { currentIssueTracker } from "../../issue/context.ts"; import { canonicalIssueTarget, issueProviderName, resolveIssueDestination, } from "../../issue/tracker.ts"; -import { IssueOperations } from "../../issue/operations.ts"; -import type { IssueReadInvocation, IssueUpsertInvocation } from "../../issue/operations.ts"; +import type { + IssueOperationsApi, + IssueReadInvocation, + IssueUpsertInvocation, +} from "../../issue/operations.ts"; import type { IssueDetails, IssueInput } from "../../issue/api.ts"; import { normalizedTags } from "../../issue/records.ts"; import { IssueContentError, IssueProtocolError, IssueTrackerError } from "../../issue/errors.ts"; -/** The component name, as a document writes it and as a refusal names it. */ -export const ISSUE_ELEMENT = ""; +const ISSUE_ELEMENT = ""; export const props: PropsSchema = { type: "object", @@ -110,22 +119,14 @@ export const returns: ReturnsSchema = { ], }; -/** What the shape of this invocation asks for. */ -export type IssueMode = "read" | "upsert"; +type IssueMode = "read" | "upsert"; /** - * Which of the two this invocation is, or the refusal that says why neither. - * - * Decided from the props and the shape alone, before a tracker is read and - * before the boundary is reached. Every branch names what to write instead, - * because each of them is a document that meant one of the two forms. - * - * `paired` is how the element was written. The ordinary component reads it - * through `Component.hasContent()`; the trusted implementation is entered by - * canonical form dispatch and states the form the scan recorded, which is the - * same fact established somewhere nothing composable can answer for it. + * Decided from the props and from `paired` — the form the scan recorded, which + * the dispatched body states here — before a tracker is read or the boundary is + * reached. */ -export function issueMode(props: Record, paired: boolean): IssueMode { +function issueMode(props: Record, paired: boolean): IssueMode { const url = typeof props.url === "string" && props.url !== ""; const title = typeof props.title === "string" && props.title !== ""; @@ -173,13 +174,9 @@ export function issueMode(props: Record, paired: boolean): IssueMo ); } -/** - * What this read asks for: a canonical issue URL, and a discriminator or none. - * - * No tracker is consulted. The URL is the identity, so a tracker written - * around a read has nothing to add to it and changes nothing about it. - */ -export function issueReadRequest(props: Record): IssueReadInvocation { +/** No tracker is consulted: the URL is the identity, and one written around a + * read has nothing to add to it. */ +function issueReadRequest(props: Record): IssueReadInvocation { const url = canonicalIssueTarget(props.url); if (url === undefined) { throw new IssueTrackerError( @@ -192,16 +189,10 @@ export function issueReadRequest(props: Record): IssueReadInvocati } /** - * What this upsert asks for: the rendered description, the destination the - * enclosing tracker names, and the normalized issue. - * - * The description before the destination, so a body that failed to render stops - * this element before a tracker is read. Rendering it is this element's own - * content projection and belongs outside whatever durable boundary the caller - * puts around the request: a child that retains work of its own finishes in its - * own order, before this request exists. + * The description renders before the destination is read, so a body that failed + * to render stops this element before a tracker is reached. */ -export function* issueUpsertRequest(props: Record): Operation { +function* issueUpsertRequest(props: Record): Operation { const description = yield* content(); if (description === "") { throw new IssueContentError( @@ -228,14 +219,6 @@ export function* issueUpsertRequest(props: Record): Operation`, wherever it is registered. - * - * One value, because two registrations describe one component: the bundled - * default every document command declares, and the trusted implementation a - * REPL entry installs over it. A reader asking either of them what `` - * is gets this. - */ export const documentation: FirstPartyDocumentation = { description: `Read or file an issue. \`\` reads one. ` + @@ -245,8 +228,7 @@ export const documentation: FirstPartyDocumentation = { context: "The issue's description, for the form that files one.", }; -/** What a read binds, as a document reads it. */ -export function issueReadBinding(details: IssueDetails): Json { +function issueReadBinding(details: IssueDetails): Json { return { url: details.url, title: details.title, @@ -256,25 +238,65 @@ export function issueReadBinding(details: IssueDetails): Json { }; } -export default function* Issue(props: Record): Operation { - // The shape first, and locally. Everything after this depends on which of the - // two questions is being asked, and a document that asked neither must not - // have one of them chosen for it. - const asked = issueMode(props, yield* hasContent()); - return asked === "read" ? yield* read(props) : yield* upsert(props); +const readBody: ComponentBody = function* Issue( + props: Record, + operations: IssueOperationsApi, +): Operation { + issueMode(props, false); + const details = yield* operations.read(issueReadRequest(props)); + return issueReadBinding(details); +}; + +const upsertBody: ComponentBody = function* Issue( + props: Record, + operations: IssueOperationsApi, +): Operation { + issueMode(props, true); + // Rendered here, so a child that retains work of its own finishes at its own + // journal position before this upsert is offered to the host beneath. + const request = yield* issueUpsertRequest(props); + const reference = yield* operations.upsert(request); + return { url: reference.url }; +}; + +const METADATA = { + name: "Issue", + origin: COMPOSITION_ORIGIN, + props, + returns, + forms: ["self-closing", "paired"] as const, + ...documented(documentation), +}; + +/** + * Which of the two external effects this element asks for comes from the scan, + * not from a contextual `hasContent()` answer: an irreversible effect is not + * something an installed handler may select. + */ +function dispatched(read: FunctionComponent, upsert: FunctionComponent): FunctionComponent { + return formDispatcher({ forms: "both", "self-closing": read, paired: upsert }); } -function* read(props: Record): Operation { - const details = yield* IssueOperations.operations.read(issueReadRequest(props)); - return issueReadBinding(details); +/** An ordinary registered default, over whichever provider the host installed. */ +export function issueRegistration(operations: IssueOperationsApi): ComponentRegistration { + return { + ...METADATA, + fn: dispatched( + (props) => readBody(props, operations), + (props) => upsertBody(props, operations), + ), + }; } -function* upsert(props: Record): Operation { - const reference = yield* IssueOperations.operations.upsert(yield* issueUpsertRequest(props)); - return { url: reference.url }; +/** A declaration, for an execution that owns this operation's policy. */ +export function issueDeclaration(bind: BindOperations): IdentityComponent { + return { + ...METADATA, + factory: (claim: IdentityClaimant) => + dispatched(boundComponent(claim, bind, readBody), boundComponent(claim, bind, upsertBody)), + }; } -/** The discriminator this prop names, or `undefined` when it names none. */ function named(value: Json | undefined): string | undefined { if (value === undefined) { return undefined; diff --git a/packages/git/src/composition/components/PullRequest.ts b/packages/git/src/composition/components/PullRequest.ts index 5d0110b29..e5ad6110a 100644 --- a/packages/git/src/composition/components/PullRequest.ts +++ b/packages/git/src/composition/components/PullRequest.ts @@ -14,72 +14,38 @@ * * ``` * - * `title` is required, `number` defaults to none, `base` defaults to the - * Repository's recorded initial branch, `draft` defaults to false, and the - * rendered content is the body. The head is the branch the selected checkout is - * on — decided the way §7.1 decides every Git operation's place, by the - * enclosing `` and the contextual working directory — so there is no - * `head`, `repository`, `remote` or `provider` prop to say it a second way. + * The head is the branch the selected checkout is on, decided the way §7.1 + * decides every Git operation's place, so there is no `head`, `repository`, + * `remote` or `provider` prop to say it a second way. Whether the document + * named a number is the document's own word rather than a search: without one + * it asks for a pull request from this head to this base, with one it asks for + * that pull request, and a number belonging elsewhere is a conflict rather than + * a rewrite onto unrelated state. * - * ## An upsert over one explicit identity - * - * Without a number the document is asking for *a pull request from this head to - * this base*: one is created, or the compatible one an interrupted earlier - * attempt already created is adopted. With a number it is asking for *that pull - * request*: its title, body, draft state and base are brought to what this - * invocation says, and if they already say it, nothing is performed and the - * no-op is recorded. - * - * Which of the two it is, is the document's own word rather than a search. A - * number that names a pull request belonging to another Repository, opened from - * another head, or no longer open is a conflict — never a rewrite onto - * unrelated state. The head is never rewritten either: an update moves the four - * fields it names and nothing else. - * - * ## It publishes nothing - * - * A pull request is a statement about a branch that already exists on the Git - * host, and this component never makes one exist. `` is written - * explicitly, and the run must already hold that Push's own successful result - * for this exact Repository, head branch, destination ref and commit — before a - * creation and before an update alike. Direct remote observation is not a - * substitute: a branch this run finds at the host may have been put there by - * anything, and opening or repointing a pull request for it would be publishing - * somebody else's work under this run's name. - * - * ## What it produces - * - * Stable evidence of what this effect settled on, bound through `as`: the - * filtered Repository identity, the provider's own stable identity for the pull - * request, its number, its URL, its open state, and the head and base commits - * of the snapshot the reconciliation finished at. Reviews, comments, checks, - * labels, merge state and later edits are separate reads rather than freshness - * smuggled into the result — a replayed run hands back exactly what the run - * recorded, whatever the pull request looks like now. - * - * ## Failure - * - * Content expands first and completely, so a body that failed to render stops - * everything this component owns before a Repository is even observed. After - * that, a missing Repository context, a checkout whose HEAD names no branch and - * missing, conflicting or unreadable Push evidence are all decided locally, - * before the Git host exists in the story, and each fails the run rather than - * printing: a later sibling must not run as though a pull request had been - * opened. What the Git host itself answers — a conflict, an ambiguity, an - * unavailability — travels under §10.2's shared vocabulary. + * It publishes nothing. A pull request is a statement about a branch that + * already exists at the Git host, and the run must already hold ``'s + * own successful result for this exact Repository, head, destination ref and + * commit. Direct remote observation is not a substitute: a branch found at the + * host may have been put there by anything, and opening a pull request for it + * would be publishing somebody else's work under this run's name. */ - import { cwd } from "@executablemd/runtime"; -import { content, hasContent } from "@executablemd/core"; -import type { PropsSchema, ReturnsSchema } from "@executablemd/core"; +import { content, documented, hasContent } from "@executablemd/core"; +import type { + ComponentRegistration, + FirstPartyDocumentation, + FunctionComponent, + PropsSchema, + ReturnsSchema, +} from "@executablemd/core"; import type { Operation } from "effection"; import type { Json } from "@executablemd/durable-streams"; import { PullRequestOperations } from "../pull-request-operations.ts"; +import { COMPOSITION_ORIGIN } from "../definitions.ts"; import { selectedRepository } from "../context.ts"; import { PullRequestAdmissionError } from "../errors.ts"; import { pullRequestResultJson } from "../pull-request-records.ts"; -/** The component name, as a document writes it and as a refusal names it. */ export const PULL_REQUEST_ELEMENT = ""; export const props: PropsSchema = { @@ -137,12 +103,21 @@ export const returns: ReturnsSchema = { additionalProperties: false, }; -export default function* PullRequest(props: Record): Operation { - // Before anything of this element's own exists — before a Repository is - // observed, before a checkout is selected, before retained Push evidence is - // read and long before a Git host is contacted. The body is what the pull - // request would say, and a body that never finished rendering must not become - // one that was published. +export const documentation: FirstPartyDocumentation = { + description: + `Open or update a pull request. \`…\` uses its ` + + `content as the body after \`\`.`, + as: "Optional. The pull request's repository, number, url, state and head and base commits.", + context: "The pull request's body.", +}; + +const body: FunctionComponent = function* PullRequest( + props: Record, +): Operation { + // Before a Repository is observed, a checkout selected or retained Push + // evidence read: a body that never finished rendering must not become one + // that was published, and a child that retains work of its own finishes at + // its own journal position first. const body = (yield* hasContent()) ? yield* content() : ""; const repository = yield* selectedRepository(); @@ -156,13 +131,11 @@ export default function* PullRequest(props: Record): Operation): Operation): Operation { - // The props schema refuses a missing or empty `url` before this function is - // entered, and therefore before any provider is asked anything. // Canonicalized before any provider sees it, so the component, the durable // request and every adapter read one answer. const url = canonicalPullRequestUrl(props.url); @@ -201,10 +197,15 @@ function read(kind: PullRequestReadKind, element: string) { }; } -function declare(kind: PullRequestReadKind, element: string): FormDeclaration { +/** + * The self-closing form comes from the scan: neither a replaceable + * `hasContent()` answer nor a caller-made invocation object can turn a paired + * element into an evidence read. + */ +function declare(fn: FunctionComponent, element: string): FormDeclaration { return { forms: "self-closing", - fn: read(kind, element), + fn, refuse: (_props: Record, written: InvocationForm | undefined) => new PullRequestReadError( "unexpected-content", @@ -218,6 +219,71 @@ function declare(kind: PullRequestReadKind, element: string): FormDeclaration { }; } -export const reviewsForm: FormDeclaration = declare("reviews", REVIEWS_ELEMENT); -export const commentsForm: FormDeclaration = declare("comments", COMMENTS_ELEMENT); -export const checksForm: FormDeclaration = declare("checks", CHECKS_ELEMENT); +const DOCUMENTATION: Readonly> = Object.freeze( + { + reviews: { + description: + `Read pull request reviews. \`\` ` + + `reads reviews from the specified pull request.`, + as: "Required. Each review's author, state, body, submission time, commit and url.", + context: null, + }, + comments: { + description: + `Read pull request comments. \`\` ` + + `reads both conversation and review comments.`, + as: + "Required. Each comment's kind, author, body, timestamps and url, and a review " + + "comment's file, hunk and line.", + context: null, + }, + checks: { + description: + `Read pull request checks. \`\` ` + + `reads check runs and commit statuses for its head.`, + as: "Required. Each check's kind, name, head commit and outcome.", + context: null, + }, + }, +); + +const NAMES: Readonly> = Object.freeze({ + reviews: "PullRequest.Reviews", + comments: "PullRequest.Comments", + checks: "PullRequest.Checks", +}); + +const ELEMENTS: Readonly> = Object.freeze({ + reviews: REVIEWS_ELEMENT, + comments: COMMENTS_ELEMENT, + checks: CHECKS_ELEMENT, +}); + +const RETURNS: Readonly> = Object.freeze({ + reviews: reviewsReturns, + comments: commentsReturns, + checks: checksReturns, +}); + +function metadata(kind: PullRequestReadKind) { + return { + name: NAMES[kind], + origin: COMPOSITION_ORIGIN, + props, + returns: RETURNS[kind], + forms: ["self-closing"] as const, + ...documented(DOCUMENTATION[kind]), + }; +} + +/** An ordinary registered default, over whichever provider the host installed. */ +export function pullRequestReadRegistration(kind: PullRequestReadKind): ComponentRegistration { + const element = ELEMENTS[kind]; + return { ...metadata(kind), fn: formDispatcher(declare(read(kind, element), element)) }; +} + +export const PULL_REQUEST_READ_KINDS: readonly PullRequestReadKind[] = Object.freeze([ + "reviews", + "comments", + "checks", +]); diff --git a/packages/git/src/composition/installation.ts b/packages/git/src/composition/installation.ts index 609ca2712..8c9f25cf0 100644 --- a/packages/git/src/composition/installation.ts +++ b/packages/git/src/composition/installation.ts @@ -1,23 +1,43 @@ /** - * The sixteen repository-composition components, as one array of ordinary - * declarations. + * The sixteen repository-composition components, declared once for every host. * * Repository, Worktree, the four Git operations, the four Git reads, - * PullRequest and its three evidence reads, IssueTracker and Issue are - * ordinary registered defaults — not - * reserved and not structural — so a repository-local component may shadow one - * for its own scope. `` is not among them: canonical core reserves that - * name (`core/src/components/fixed-filesystem.ts`), and a document reaches it - * with or without this package. + * PullRequest and its three evidence reads, IssueTracker and Issue are ordinary + * registered defaults — not reserved and not structural — so a + * repository-local component may shadow one for its own scope. `` is not + * among them: canonical core reserves that name + * (`core/src/components/fixed-filesystem.ts`), and a document reaches it with + * or without this package. * - * One array, three consumers, because three descriptions of one vocabulary - * would drift. `useCompositionComponents()` registers it inside a workflow - * attachment; `useCommandComponents()` registers it for `xmd syntax` and for - * `xmd plan`'s validation and generation; `installDocumentComponents()` - * registers it for an ordinary run. Registering it installs no provider, - * performs no repository discovery, acquires no lock and reaches no network: - * what a name *does* is the installed provider's, and describing the - * environment mints none. + * ## One registry, and the policy an execution attaches underneath it + * + * Ten of the sixteen reach nothing beyond the selected checkout: Repository and + * Worktree choose one, Git.Switch, Git.Add and Git.Commit change one, the four + * Git reads observe one, and IssueTracker names a destination. + * + * The other six — ``, ``, its three evidence reads and + * `` — reach something no local transaction encloses. Each of those has + * exactly one implementation, here, and it asks a semantic feature operation + * for its work. How that operation is performed, retained and replayed is the + * host's: an ordinary run and a workflow run forward to the public feature + * Apis, a runtime with no provider keeps each Api's own refusing default, and a + * REPL entry retains and reconciles beneath them. + * + * All sixteen are registered here, for every command that carries this + * vocabulary, because this is what says the names exist: it is what `xmd + * syntax` describes, what validation resolves against and what an execution + * inherits. A host that retains one of these operations does not register a + * different component for it — it declares this same body with its own policy + * from inside its own execution's scope, where a nearer registration shadows a + * farther one. Either way the component is the same: one body, one set of + * forms, one return contract, because typing `` means the same authored + * operation wherever it is typed. + * + * Nothing here installs a provider, performs repository discovery, acquires a + * lock or reaches a network: what a name *does* is the installed policy's, and + * describing the environment selects none. Inspection and validation read the + * declarations' metadata without calling a factory, which is what lets + * `xmd syntax` and `` describe this vocabulary. * * A completed root replay attaches no provider and registers nothing, so a * document that already ran through completion re-registers none of these. @@ -37,6 +57,8 @@ import type { DocumentationReader, } from "@executablemd/core"; import { COMPOSITION_ORIGIN } from "./definitions.ts"; +import { Git } from "./git-api.ts"; +import { IssueOperations } from "../issue/operations.ts"; import Repository, { props as repositoryProps } from "./components/Repository.ts"; import Worktree, { props as worktreeProps } from "./components/Worktree.ts"; import GitSwitch, { props as gitSwitchProps } from "./components/GitSwitch.ts"; @@ -45,28 +67,13 @@ import GitCommit, { props as gitCommitProps, returns as gitCommitReturns, } from "./components/GitCommit.ts"; -import GitPush, { - documentation as gitPushDocumentation, - props as gitPushProps, -} from "./components/GitPush.ts"; -import PullRequest, { - props as pullRequestProps, - returns as pullRequestReturns, -} from "./components/PullRequest.ts"; -import Issue, { - documentation as issueDocumentation, - props as issueProps, - returns as issueReturns, -} from "./components/Issue.ts"; +import { pushRegistration } from "./components/GitPush.ts"; +import { pullRequestRegistration } from "./components/PullRequest.ts"; import { - checksForm, - checksReturns, - commentsForm, - commentsReturns, - props as pullRequestReadProps, - reviewsForm, - reviewsReturns, + PULL_REQUEST_READ_KINDS, + pullRequestReadRegistration, } from "./components/PullRequestReads.ts"; +import { issueRegistration } from "./components/Issue.ts"; import IssueTracker, { props as issueTrackerProps } from "./components/IssueTracker.ts"; import { changesForm, diffForm, fileForm, filesForm } from "./components/GitReads.ts"; import { @@ -80,11 +87,24 @@ import { filesReturns, } from "./observation-api.ts"; +/** + * Every name this boundary carries, in catalog order. + * + * Derived from the one registration array rather than listed again, so a + * component added to it cannot disappear from the documentation this package + * contributes or from an inventory held to it. An execution that declares one + * of these operations with policy of its own declares it under this same name, + * which is what makes one catalog true of every host. + */ +function compositionNames(): readonly string[] { + return Object.freeze(COMPOSITION_REGISTRATIONS.map((registration) => registration.name)); +} + /** * This boundary's long-form documentation, and the components it must cover. * - * Derived from `COMPOSITION_REGISTRATIONS` below rather than listed again, so a - * component added to that array cannot be left undocumented here. + * Derived from the registration array rather than listed again, so a component + * added to it cannot be left undocumented here. */ export function* compositionDocumentation( read?: DocumentationReader, @@ -95,12 +115,13 @@ export function* compositionDocumentation( owner: COMPOSITION_ORIGIN, asset: "packages/git/src/composition/components.md", }, - COMPOSITION_REGISTRATIONS.map((registration) => registration.name), + compositionNames(), read, ); } -export const COMPOSITION_REGISTRATIONS: readonly ComponentRegistration[] = [ +/** Repository and Worktree: which checkout the Markdown inside them expands in. */ +const SELECTIONS: readonly ComponentRegistration[] = [ { name: "Repository", origin: COMPOSITION_ORIGIN, @@ -123,6 +144,16 @@ export const COMPOSITION_REGISTRATIONS: readonly ComponentRegistration[] = [ context: "The Markdown expanded in that checkout.", }), }, +]; + +/** + * What one selected checkout answers about itself, or changes inside itself. + * + * Git.Switch, Git.Add and Git.Commit change one and the four reads observe one; + * either way a local transaction encloses the work, so none of them is an + * operation a declaring execution names an invocation of. + */ +const LOCAL_OPERATIONS: readonly ComponentRegistration[] = [ { name: "Git.Switch", origin: COMPOSITION_ORIGIN, @@ -157,13 +188,6 @@ export const COMPOSITION_REGISTRATIONS: readonly ComponentRegistration[] = [ context: "The message body, expanded before the commit is made.", }), }, - { - name: "Git.Push", - origin: COMPOSITION_ORIGIN, - props: gitPushProps, - fn: GitPush, - ...documented(gitPushDocumentation), - }, { name: "Git.Files", origin: COMPOSITION_ORIGIN, @@ -212,83 +236,56 @@ export const COMPOSITION_REGISTRATIONS: readonly ComponentRegistration[] = [ context: null, }), }, - { - name: "PullRequest", - origin: COMPOSITION_ORIGIN, - props: pullRequestProps, - returns: pullRequestReturns, - fn: PullRequest, - ...documented({ - description: `Open or update a pull request. \`…\` uses its content as the body after \`\`.`, - as: "Optional. The pull request's repository, number, url, state and head and base commits.", - context: "The pull request's body.", - }), - }, - { - name: "PullRequest.Reviews", - origin: COMPOSITION_ORIGIN, - props: pullRequestReadProps, - returns: reviewsReturns, - fn: formDispatcher(reviewsForm), - ...documented({ - description: `Read pull request reviews. \`\` reads reviews from the specified pull request.`, - as: "Required. Each review's author, state, body, submission time, commit and url.", - context: null, - }), - }, - { - name: "PullRequest.Comments", - origin: COMPOSITION_ORIGIN, - props: pullRequestReadProps, - returns: commentsReturns, - fn: formDispatcher(commentsForm), - ...documented({ - description: `Read pull request comments. \`\` reads both conversation and review comments.`, - as: "Required. Each comment's kind, author, body, timestamps and url, and a review comment's file, hunk and line.", - context: null, - }), - }, - { - name: "PullRequest.Checks", - origin: COMPOSITION_ORIGIN, - props: pullRequestReadProps, - returns: checksReturns, - fn: formDispatcher(checksForm), - ...documented({ - description: `Read pull request checks. \`\` reads check runs and commit statuses for its head.`, - as: "Required. Each check's kind, name, head commit and outcome.", - context: null, - }), - }, - { - name: "IssueTracker", - origin: COMPOSITION_ORIGIN, - props: issueTrackerProps, - fn: IssueTracker, - ...documented({ - description: `Select an issue tracker. \`…\` expands its content with the specified tracker in scope.`, - as: null, - context: "The Markdown whose issues are filed there.", - }), - }, - { - name: "Issue", - origin: COMPOSITION_ORIGIN, - props: issueProps, - returns: issueReturns, - fn: Issue, - ...documented(issueDocumentation), - }, ]; +/** The destination an `` inside it is filed at. */ +const TRACKER: ComponentRegistration = { + name: "IssueTracker", + origin: COMPOSITION_ORIGIN, + props: issueTrackerProps, + fn: IssueTracker, + ...documented({ + description: `Select an issue tracker. \`…\` expands its content with the specified tracker in scope.`, + as: null, + context: "The Markdown whose issues are filed there.", + }), +}; + +/** + * The six operations no local transaction encloses, as ordinary registrations. + * + * The same bodies an owning execution declares, over the public feature Apis. + * Registered, because this is where the names come from, and because an + * ordinary default is what a nearer registration — a repository-local + * component, an enclosing host, an execution that owns the operation — is + * entitled to shadow. + */ +const EXTERNAL_OPERATIONS: readonly ComponentRegistration[] = [ + pushRegistration(Git.operations.push), + pullRequestRegistration, + ...PULL_REQUEST_READ_KINDS.map(pullRequestReadRegistration), + issueRegistration(IssueOperations.operations), +]; + +/** All sixteen, in catalog order. */ +export const COMPOSITION_REGISTRATIONS: readonly ComponentRegistration[] = Object.freeze([ + ...SELECTIONS, + ...LOCAL_OPERATIONS, + ...EXTERNAL_OPERATIONS, + TRACKER, +]); + /** - * Register the composition vocabulary as ordinary defaults for this scope. + * Register the whole vocabulary as ordinary defaults for this scope. + * + * Every command that carries this vocabulary calls this, `repl` included: one + * registry is what says which names exist, and an execution that owns one of + * these operations attaches its policy to the same body from inside its own + * scope rather than arriving with a vocabulary of its own. * - * The registrations and the documentation that describes them, installed - * together so a scope that has one has the other. Both are declarations: this - * installs no provider, discovers no ambient repository, acquires no lock, - * spawns no Git and reads no credential, which is what lets `xmd syntax` enter - * it to describe the profile. + * Every one of them is a declaration: this installs no provider, discovers no + * ambient repository, acquires no lock, spawns no Git and reads no credential, + * which is what lets `xmd syntax` enter it to describe the profile. */ export function* useCompositionComponents(): Operation { yield* registerComponents(COMPOSITION_REGISTRATIONS); diff --git a/packages/git/src/composition/invocation-binding.ts b/packages/git/src/composition/invocation-binding.ts new file mode 100644 index 000000000..db62b7a1d --- /dev/null +++ b/packages/git/src/composition/invocation-binding.ts @@ -0,0 +1,55 @@ +/** + * How a declaring execution attaches its own policy to a shared component body. + * + * `` and `` are the two operations a REPL entry retains, so + * their bodies take the operation they ask as an argument: an ordinary + * registration passes the feature Api's own operations, and the entry's + * declaration passes an envelope that encloses that Api. A body that reached + * the Api itself could not be enclosed by a durable boundary above it, which is + * what retention here means. The other four — `` and its three + * evidence reads — are retained as well, but underneath their Api rather than + * above it: a workflow run makes the upsert durable in the Git-host + * reconciliation beneath it, and each read durable as policy on + * `PullRequestReadExecution`. No host owns a boundary over the chain for them, + * so they need no binding of their own here and ask their Api directly. + * + * Either way the body receives semantic operations and nothing else: no + * authenticator, no owner, no site, no native access and no record codec. The + * site stays private to the policy it was handed to. + */ + +import type { Operation } from "effection"; +import type { ComponentInvocation, FunctionComponent } from "@executablemd/core"; +import type { IdentityClaimant } from "@executablemd/core/host"; +import type { Json } from "@executablemd/durable-streams"; + +/** The one authored body of a repository operation. */ +export type ComponentBody = ( + props: Record, + operations: Operations, +) => Operation; + +/** How a host answers with one authenticated invocation's own operations. */ +export type BindOperations = (site: string) => Operations; + +/** + * The body a declaration carries, over the policy the declaring execution owns. + * + * `claim` comes from `IdentityComponent.factory`, so it is the authenticator + * that execution minted for this component name. + */ +export function boundComponent( + claim: IdentityClaimant, + bind: BindOperations, + body: ComponentBody, +): FunctionComponent { + return function* ( + props: Record, + invocation: ComponentInvocation, + ): Operation { + // In the frame the engine entered, and before the body reaches any content + // of its own: a projecting ancestor is ineligible for an invocation. + const site = yield* claim(invocation); + return yield* body(props, bind(site)); + }; +} diff --git a/packages/git/src/deno/retained-repository/issues.ts b/packages/git/src/deno/retained-repository/issues.ts index 80743b55b..2f3464fbd 100644 --- a/packages/git/src/deno/retained-repository/issues.ts +++ b/packages/git/src/deno/retained-repository/issues.ts @@ -2,89 +2,48 @@ * One REPL entry's issues: what the entry executing right now may file and read. * * `provider.ts` beside this answers the same question for files and managed - * checkouts. This one is smaller and differs in what it reaches: an issue lives - * at a service rather than in a checkout, so there is no discovery, no lease and - * no managed root here at all — just the GitHub adapter this platform can talk - * to, and the durable envelope that makes one entry's issue work answerable - * again after the person walks away from it. - * - * ## What the entry owns, and what it cannot be told - * - * An entry's issue work is retained under an owner the CLI derives from the - * execution and the entry key, and each invocation inside it is named by the - * identity canonical core minted where it selected this exact implementation. - * The factory closes over the owner and core delivers the identity - * authenticator, so neither - * arrives through a prop, a tracker, a Context, a middleware answer or a - * registry: a party that could name either could file an issue under somebody - * else's entry, or read back the result of one. - * - * ## One envelope, with live composition inside it - * - * The trusted body owns the one durable boundary. Inside a live attempt it - * composes `IssueOperations` — so a document's own middleware still answers, - * transforms, refuses and delegates exactly as it does anywhere else — over a - * transport-facing default of its own that reaches `IssueApi` and adds no - * second envelope. A completed replay invokes none of that: no middleware, no - * transport, no configuration and no credential. - * - * The fallback is installed in a scope of the attempt's own and closes over that - * attempt's idempotency key, so two concurrent `` elements each dispatch - * through their own and neither can present the other's key. Its lifetime ends - * when the attempt's live operation returns, which is before the envelope - * publishes anything. - * - * ## Installing it reaches nothing - * - * No configuration, no token, no network, no Git session, no ambient discovery, - * no checkout lease and no managed root. The adapter reads what this deployment - * authorized when an invoked request turns out to be its own, so an entry that - * writes no `` reads neither variable and obtains no credential, and a - * passive view — a settled history, a frozen position — initializes nothing. + * checkouts. This one differs in what it reaches: an issue lives at a service + * rather than in a checkout, so there is no discovery, no lease and no managed + * root here at all — just the GitHub adapter this platform can talk to, and the + * shared durable envelope in `issue/owned.ts` that makes one entry's issue work + * answerable again after the person walks away from it. + * + * Installing it reaches nothing: no configuration, no token, no network, no Git + * session, no ambient discovery, no checkout lease and no managed root. The + * adapter reads what this deployment authorized when an invoked request turns + * out to be its own, so an entry that writes no `` obtains no credential, + * and a passive view — a settled history, a frozen position — initializes + * nothing. */ - import { scoped } from "effection"; import type { Operation } from "effection"; -import { documented, formDispatcher } from "@executablemd/core"; -import type { ComponentInvocation, FunctionComponent } from "@executablemd/core"; -import type { - ExecutionInstallation, - IdentityClaimant, - IdentityComponent, - JournalAdmission, -} from "@executablemd/core/host"; +import type { ExecutionInstallation, JournalAdmission } from "@executablemd/core/host"; import type { DurableEvent } from "@executablemd/durable-streams"; import { StaleInputError } from "@executablemd/durable-streams"; import type { Json } from "@executablemd/durable-streams"; -import { COMPOSITION_ORIGIN } from "../../composition/definitions.ts"; -import { - documentation as issueDocumentation, - issueMode, - issueReadBinding, - issueReadRequest, - issueUpsertRequest, - props as issueProps, - returns as issueReturns, -} from "../../composition/components/Issue.ts"; -import { IssueApi } from "../../issue/api.ts"; -import type { IssueDetails, IssueReference } from "../../issue/api.ts"; +import { issueDeclaration } from "../../composition/components/Issue.ts"; +import type { BindOperations } from "../../composition/invocation-binding.ts"; import { IssueOperations } from "../../issue/operations.ts"; import type { IssueOperationsApi, IssueReadInvocation, IssueUpsertInvocation, } from "../../issue/operations.ts"; -import { IssueProtocolError } from "../../issue/errors.ts"; import { ISSUE_EFFECT } from "../../issue/effect-type.ts"; import { - issueIdempotencyKey, issueRequestFingerprint, parseIssueDetails, parseIssueRecord, parseIssueRequest, } from "../../issue/records.ts"; import type { IssueRequest } from "../../issue/records.ts"; -import { readOwnedIssue, upsertOwnedIssue } from "../../issue/owned.ts"; +import { + liveIssueRead, + liveIssueUpsert, + ownedUpsertKey, + readOwnedIssue, + upsertOwnedIssue, +} from "../../issue/owned.ts"; import { useGitHubIssues } from "../issue/github.ts"; import type { GitHubIssuesOptions } from "../issue/github.ts"; import { denoGitHubSource } from "../composition/github-host.ts"; @@ -131,163 +90,78 @@ export function replIssueInstallation( options: GitHubIssuesOptions = {}, ): ExecutionInstallation { return { - components: [ownedIssueComponent(owner)], - admissions: [ownedIssueAdmission(owner)], - *install(): Operation { - // The platform's transport, handed to an adapter that knows the protocol - // and not the platform. Naming it here is not building one: it is a - // factory the adapter calls if and when a request turns out to be its own. - yield* useGitHubIssues({ host: denoGitHubSource, ...options }); - }, + // The one operation this entry retains. Every other name stays the + // registered default, so `` and `` in an entry + // whose host recorded only issues reach their own unchanged + // missing-provider refusals. + components: [issueDeclaration(replIssuePolicy(owner))], + admissions: [replIssueAdmission(owner)], + install: () => useEntryIssueTransport(options), }; } /** - * The entry's own `Issue`, declared as an ordinary shadowable component. - * - * It shadows the inherited bundled default for this execution's scope without - * registering two defaults in one scope and without touching that inherited - * registration, so a repository-local `` keeps its ordinary precedence - * over both. The name is neither reserved nor protected: what a replacement - * does not get is an identity authenticator, so it names no durable work of - * this entry's. + * The platform's transport, handed to an adapter that knows the protocol and + * not the platform. * - * Inspection describes one `Issue` — the same origin, props, return contract - * and prose the bundled registration carries — because it is the same component - * with the entry's identity behind it. + * Naming it is not building one: it is a factory the adapter calls if and when + * a request turns out to be its own. */ -function ownedIssueComponent(owner: string): IdentityComponent { - return { - name: "Issue", - origin: COMPOSITION_ORIGIN, - props: issueProps, - returns: issueReturns, - forms: ["self-closing", "paired"], - ...documented(issueDocumentation), - factory: (claim: IdentityClaimant) => - // Canonical form dispatch, so which of the two external effects this - // element asks for is settled by the scan before the body runs. A - // contextual `hasContent()` answer cannot select it: the chain that - // answers one is composed by whoever installed a handler, and an - // irreversible effect is not something a handler may choose. - formDispatcher({ - forms: "both", - "self-closing": readingBody(owner, claim), - paired: upsertingBody(owner, claim), - }), - }; -} - -function readingBody(owner: string, claim: IdentityClaimant): FunctionComponent { - return function* Issue( - props: Record, - invocation: ComponentInvocation, - ): Operation { - // The same input checks the ordinary component makes, for the form that was - // dispatched here. Which of the two this is was decided by the engine, so - // the answer is not read back: what is wanted from this call is its - // refusals, which are the component's settled sentences. - issueMode(props, false); - const site = yield* claim(invocation); - const request = issueReadRequest(props); - const details = yield* readOwnedIssue({ owner, site, request }, liveRead(request)); - return issueReadBinding(details); - }; -} - -function upsertingBody(owner: string, claim: IdentityClaimant): FunctionComponent { - return function* Issue( - props: Record, - invocation: ComponentInvocation, - ): Operation { - issueMode(props, true); - // Before this element projects its own content, because an invocation names - // nothing while it is expanding what is inside it — and after it would be - // naming the identity from a frame the children have already left. - const site = yield* claim(invocation); - // The description is rendered here, outside the envelope below. A child - // `` or any other durable operation inside this content therefore - // finishes at its own journal position, before this upsert is offered. - const request = yield* issueUpsertRequest(props); - const reference = yield* upsertOwnedIssue( - { owner, site, request }, - liveUpsert( - request, - issueIdempotencyKey({ runId: owner, expansionId: site }, "upsert", request.target), - ), - ); - return { url: reference.url }; - }; +export function useEntryIssueTransport(options: GitHubIssuesOptions): Operation { + return useGitHubIssues({ host: denoGitHubSource, ...options }); } /** - * One live read attempt: the composed `IssueOperations` question, answered - * beneath by this entry's transport-facing default. - * - * Scoped, so the default is gone before the envelope publishes anything, and - * installed at `min`, so every public handler a document or a plugin composed - * still wraps the dispatch. + * How this entry performs one ``, in either of its two forms. + * + * The owner is closed over and the site arrives as the argument the authenticated + * binding hands in, so neither reaches here through a prop, a tracker, a Context, + * a middleware answer or a registry: a party that could name either could file an + * issue under somebody else's entry, or read back the result of one. + * + * This is a policy rather than `IssueOperations` middleware — which is where the + * workflow profile installs the same envelope — because an entry's envelope has + * to enclose the public chain rather than sit beneath it. A handler that answers + * a read itself is retained as this entry's answer, and a completed replay + * invokes no handler at all. */ -function liveRead(request: IssueReadInvocation): () => Operation { - return () => - scoped(function* (): Operation { - yield* IssueOperations.around(transportRead(), { at: "min" }); - return yield* IssueOperations.operations.read(request); - }); -} - -/** One live upsert attempt, on the same terms, under this attempt's own key. */ -function liveUpsert(request: IssueUpsertInvocation, key: string): () => Operation { - return () => - scoped(function* (): Operation { - yield* IssueOperations.around(transportUpsert(key), { at: "min" }); - return yield* IssueOperations.operations.upsert(request); - }); -} - -function transportRead(): Partial> { - return { - *read([invocation]: [IssueReadInvocation]): Operation { - const answered = yield* IssueApi.operations.read(invocation.url, { - ...(invocation.provider === undefined ? {} : { provider: invocation.provider }), - }); - const details = parseIssueDetails(answered); - if (details === undefined) { - throw new IssueProtocolError( - "the issue provider answered a read with something that is not an issue's shared " + - "fields", - ); - } - return details; +export function replIssuePolicy(owner: string): BindOperations { + return (site: string) => ({ + read: (invocation: IssueReadInvocation) => + readOwnedIssue({ owner, site, request: invocation }, () => + attempted({ read: ([request]) => liveIssueRead(request) }, () => + IssueOperations.operations.read(invocation), + ), + ), + upsert: (invocation: IssueUpsertInvocation) => { + const key = ownedUpsertKey(owner, site, invocation.target); + return upsertOwnedIssue({ owner, site, request: invocation }, () => + attempted({ upsert: ([request]) => liveIssueUpsert(request, key) }, () => + IssueOperations.operations.upsert(invocation), + ), + ); }, - }; + }); } /** - * The transport-facing default for one upsert attempt. + * One live attempt: the public `IssueOperations` chain, over a transport-facing + * default of this attempt's own. * - * The key is this attempt's, derived from the complete request's own identity - * and target before any middleware saw it. A handler that transformed the - * issue, the target or the discriminator changes what is sent — that is what - * middleware is for — and changes nothing about which attempt this is. + * Scoped, so the default is gone before the envelope publishes anything, and + * installed at `min`, so every public handler a document or a plugin composed + * still wraps the dispatch. The default closes over this attempt's key, so two + * concurrent `` elements each dispatch through their own and neither can + * present the other's. */ -function transportUpsert(key: string): Partial> { - return { - *upsert([invocation]: [IssueUpsertInvocation]): Operation { - const answered = yield* IssueApi.operations.upsert(invocation.issue, { - url: invocation.target, - ...(invocation.provider === undefined ? {} : { provider: invocation.provider }), - idempotencyKey: key, - }); - const record = parseIssueRecord(answered); - if (record === undefined) { - throw new IssueProtocolError( - "the issue provider answered an upsert with something that is not a URL", - ); - } - return record; - }, - }; +function attempted( + transport: Partial>, + ask: () => Operation, +): Operation { + return scoped(function* (): Operation { + yield* IssueOperations.around(transport, { at: "min" }); + return yield* ask(); + }); } /** @@ -306,7 +180,7 @@ function transportUpsert(key: string): Partial> { * refused rather than continued — and borrowing it is exactly what is not * allowed, however the record came to be in this segment. */ -function ownedIssueAdmission(owner: string): JournalAdmission { +export function replIssueAdmission(owner: string): JournalAdmission { return function* (retained: readonly DurableEvent[]): Operation { for (const event of retained) { if (event.type !== "yield" || event.description.type !== ISSUE_EFFECT) { diff --git a/packages/git/src/deno/retained-repository/provider.ts b/packages/git/src/deno/retained-repository/provider.ts index a2466dcbd..4e21f6351 100644 --- a/packages/git/src/deno/retained-repository/provider.ts +++ b/packages/git/src/deno/retained-repository/provider.ts @@ -26,11 +26,12 @@ * ``, and, for an entry whose capability was built per entry below, * the publication an authored `` asks for. * - * A pull request and an Issue tracker are not here: each has its own protocol - * and its own evidence, and a local result and a publication authorize neither. - * `` stays declared where it already is and refuses through its - * own unchanged missing-provider default, and what is installed here is not the - * ordinary all-feature provider, which would enable it. + * A pull request is not here: it has its own protocol and its own evidence, and + * a local result and a publication authorize neither. `` and its + * three evidence reads are declared with the ordinary policy beneath them, so + * they refuse through their own unchanged missing-provider default — and what + * is installed here is not the ordinary all-feature provider, which would + * enable them. * * ## Installing it reaches nothing * @@ -80,9 +81,11 @@ import type { ReplGitCheckout, ReplGitEntry } from "./git.ts"; import { replObservationMiddleware } from "./observations.ts"; import type { ReplObservationEntry } from "./observations.ts"; import { GitQuery } from "../../git.ts"; -import { replPushAdmission, replPushComponent } from "./push.ts"; +import { replPushAdmission, replPushPolicy } from "./push.ts"; import type { ReplPushEntry } from "./push.ts"; -import { replIssueInstallation } from "./issues.ts"; +import { replIssueAdmission, replIssuePolicy, useEntryIssueTransport } from "./issues.ts"; +import { pushDeclaration } from "../../composition/components/GitPush.ts"; +import { issueDeclaration } from "../../composition/components/Issue.ts"; import { Execution } from "@executablemd/core/api"; import type { ExecutionInstallation } from "@executablemd/core/host"; import type { IdentityComponent, JournalAdmission } from "@executablemd/core/host"; @@ -168,9 +171,11 @@ export function useReplRepositoryRecording(options: ReplRecordingOptions = {}): * never returned to the CLI, published through Context or reachable from a * document. * - * The issue capability an entry runs with is composed in whole, declarations, - * admissions and installation together, because one entry installs one - * capability and neither feature's internals belong to the other. + * One entry declares the common vocabulary once, with this host's policy under + * the two operations it retains and the ordinary policy under the rest. There + * is no inherited executable default here for an entry registration to shadow: + * the declarations an execution captures are these, and a host that recorded + * nothing for an operation leaves that operation its own refusal. */ export function* replRepositoryInstallation( owner: string, @@ -196,17 +201,26 @@ export function* replRepositoryInstallation( }, }); - const issues = replIssueInstallation(owner); - const components: IdentityComponent[] = [ - ...(issues.components ?? []), - replPushComponent(owner, () => (usable ? held.entry : undefined)), + // The two names this entry declares, and only those. Every name this + // vocabulary has is already registered in the command's scope; what an entry + // adds here is the policy it owns, attached to those same two bodies from + // inside its own execution. + // + // Retaining an operation is not by itself a reason to declare its name. This + // entry retains the pages its four Git reads answer with and the file work + // beneath them too, and both are middleware under the component rather than a + // policy named after an invocation — so they keep the registered default and + // appear nowhere here. These two need the invocation: a durable boundary that + // has to enclose the public chain cannot be installed inside it. + const components: readonly IdentityComponent[] = [ + pushDeclaration(replPushPolicy(owner, () => (usable ? held.entry : undefined))), + issueDeclaration(replIssuePolicy(owner)), ]; - const admissions: JournalAdmission[] = [...(issues.admissions ?? []), replPushAdmission(owner)]; - const install = issues.install; + const admissions: JournalAdmission[] = [replIssueAdmission(owner), replPushAdmission(owner)]; return { components, admissions, - ...(install === undefined ? {} : { install: () => install.call(issues) }), + install: () => useEntryIssueTransport({}), }; } diff --git a/packages/git/src/deno/retained-repository/push.ts b/packages/git/src/deno/retained-repository/push.ts index f41a4a515..abbdd71fa 100644 --- a/packages/git/src/deno/retained-repository/push.ts +++ b/packages/git/src/deno/retained-repository/push.ts @@ -17,14 +17,13 @@ * * ## What a document writes, and what it cannot reach * - * `` stays an ordinary shadowable component with the origin, props - * and prose it already had. What the entry's own declaration adds is the - * identity canonical core mints for each invocation: the trusted body takes that - * identity from the invocation once, snapshots the Repository in scope and the - * contextual working directory, and owns its private owner and site throughout. None of - * them reaches public Git input, so a replacement registration — or a direct - * `Git.operations.push(...)` from anywhere — names no durable work of this - * entry's and reaches the ordinary missing-provider refusal. + * `` is the one shared component, with the origin, props and prose + * it already had. What this host adds is policy beneath it: the common + * invocation binding authenticates each invocation and hands this policy the + * site it authenticated, and the policy owns its private owner and site + * throughout. Neither reaches public Git input, so a replacement registration — + * or a direct `Git.operations.push(...)` from anywhere — names no durable work + * of this entry's and reaches the ordinary missing-provider refusal. * * A fresh invocation dispatches through an operation-local default under the * stable Git Api name, so every public handler a document or a plugin composed @@ -67,30 +66,17 @@ import type { LiveDurableOperationCoordinator, Result as ProtocolResult, } from "@executablemd/durable-streams"; -import { documented, formDispatcher } from "@executablemd/core"; -import type { ComponentInvocation, FunctionComponent } from "@executablemd/core"; -import type { - IdentityClaimant, - IdentityComponent, - JournalAdmission, -} from "@executablemd/core/host"; -import { cwd } from "@executablemd/runtime"; -import { COMPOSITION_ORIGIN } from "../../composition/definitions.ts"; -import { - documentation as gitPushDocumentation, - PUSH, - props as gitPushProps, -} from "../../composition/components/GitPush.ts"; -import { selectedRepository } from "../../composition/context.ts"; +import type { JournalAdmission } from "@executablemd/core/host"; +import { PUSH } from "../../composition/components/GitPush.ts"; import { GitCompositionProviderError, GitOperationAdmissionError, - GitOperationError, GitOperationInfrastructureError, GitOperationProtocolError, } from "../../composition/errors.ts"; import { GIT_API } from "../../composition/git-api.ts"; import type { GitApi, GitPushInvocation } from "../../composition/git-api.ts"; +import type { BindOperations } from "../../composition/invocation-binding.ts"; import { destinationRefFor, parseGitPushRecord, @@ -183,76 +169,30 @@ export function replPushAdmission(owner: string): JournalAdmission { } /** - * The entry's own `Git.Push`, declared as an ordinary shadowable component. + * How this entry performs one ``. * - * It shadows the inherited bundled registration for this execution's scope - * without touching it, so a repository-local `` keeps its ordinary - * precedence over both. The name is neither reserved nor protected: what a - * replacement does not get is an identity authenticator. + * The policy the common declaration is built with. It is called once per + * invocation, with the site the binding authenticated, and what it answers with + * is that invocation's own publication operation: the shared component asks it, + * and everything about retention, dispatch and reconciliation happens below + * this line. * - * Inspection describes one `Git.Push` — the same origin, props, forms and prose - * the bundled registration carries — because it is the same component with this - * entry's identity behind it. + * The site reaches no public Git input and no journal name of its own. Its + * parsed facts are what the saved request carries, because the opaque + * identifier this entry minted names nothing once the entry ends. */ -export function replPushComponent(owner: string, access: ReplPushAccess): IdentityComponent { - return { - name: "Git.Push", - origin: COMPOSITION_ORIGIN, - props: gitPushProps, - forms: ["self-closing"], - // The bundled registration's own prose, read from the component module both - // of them read: inspection describes one `Git.Push`, so the two cannot - // drift into describing two. - ...documented(gitPushDocumentation), - // Canonical form dispatch, so the form is settled by the scan before the - // body runs. A contextual `hasContent()` answer cannot admit a paired - // publication: the chain that answers one is composed by whoever installed - // a handler, and an irreversible external effect is not something a handler - // may choose. - factory: (claim: IdentityClaimant) => - formDispatcher({ - forms: "self-closing", - fn: publishingBody(owner, access, claim), - refuse: () => - new GitOperationError( - PUSH, - "invalid-invocation", - `${PUSH} renders nothing, so it takes no content. Write it as .`, - ), - }), - }; -} - -function publishingBody( +export function replPushPolicy( owner: string, access: ReplPushAccess, - claim: IdentityClaimant, -): FunctionComponent { - return function* GitPush( - _props: Record, - invocation: ComponentInvocation, - ): Operation { - // The same lexical refusal the ordinary component makes, in the same order - // and with the same sentence: there is nowhere to publish to before there - // is anything to publish. - const repository = yield* selectedRepository(); - if (repository === undefined) { - throw new GitOperationAdmissionError( - PUSH, - "it is written outside a lexical , so there is no repository in scope whose " + - "origin a branch could be published to", - ); - } - const site = yield* claim(invocation); - yield* publish({ +): BindOperations { + return (site: string) => (request: GitPushInvocation) => + publish({ owner, site, access, - repository, - workingDirectory: yield* cwd(), + repository: request.repository, + workingDirectory: request.workingDirectory, }); - return ""; - }; } interface Invocation { @@ -271,7 +211,7 @@ interface Invocation { * request, and only an invocation with nothing saved dispatches, resolves and * writes one down. */ -function* publish(invocation: Invocation): Operation { +function* publish(invocation: Invocation): Operation { const entry = held(invocation.access); // Authenticated before anything else, and its *parsed* facts are what the // request carries. The opaque identifier this entry minted never appears: it @@ -294,8 +234,7 @@ function* publish(invocation: Invocation): Operation { if (yield* retainedRecordAhead(REPL_REPOSITORY_PUSH_REQUEST, name)) { const saved = yield* restored(entry, invocation, requested, name); - yield* reconcile(entry, invocation, saved); - return; + return yield* reconcile(entry, invocation, saved); } // One operation-local default under the stable Api name: the shared @@ -316,10 +255,10 @@ function* publish(invocation: Invocation): Operation { commit: refusing(""), }); try { - // What a handler answers with is not read. A synthetic answer has answered - // the document; it is not a receipt, and this operation retains nothing for - // it. - yield* local.operations.push({ + // What a handler answers with is not read as evidence. A synthetic answer + // has answered the document; it is not a receipt, and this operation + // retains nothing for it. + return yield* local.operations.push({ repository: invocation.repository, workingDirectory: invocation.workingDirectory, }); diff --git a/packages/git/src/issue/effect.ts b/packages/git/src/issue/effect.ts index f87290b10..82b05fec8 100644 --- a/packages/git/src/issue/effect.ts +++ b/packages/git/src/issue/effect.ts @@ -41,13 +41,18 @@ import { getExpansion } from "@executablemd/core"; import { getWorkflowRun } from "@executablemd/workflow"; import { retainedIssueIdentitiesHere } from "../identities.ts"; import { claimRetainedIssueIdentity, exhaustRetainedIssueIdentities } from "./identities.ts"; -import { IssueApi } from "./api.ts"; import { IssueOperations } from "./operations.ts"; import type { IssueReadInvocation, IssueUpsertInvocation } from "./operations.ts"; import type { IssueDetails, IssueReference } from "./api.ts"; import { IssueProtocolError } from "./errors.ts"; -import { issueIdempotencyKey } from "./records.ts"; -import { ownedUpsertRequest, readOwnedIssue, upsertOwnedIssue } from "./owned.ts"; +import { + liveIssueRead, + liveIssueUpsert, + ownedUpsertKey, + ownedUpsertRequest, + readOwnedIssue, + upsertOwnedIssue, +} from "./owned.ts"; import type { IssueReadEffectRequest, IssueUpsertEffectRequest } from "./owned.ts"; export type { IssueReadEffectRequest, IssueUpsertEffectRequest } from "./owned.ts"; @@ -63,11 +68,9 @@ export function* readIssue(request: IssueReadEffectRequest): Operation { - return yield* IssueApi.operations.read(request.url, { - ...(request.provider === undefined ? {} : { provider: request.provider }), - }); - }, + // Directly: this profile installs the envelope at the `IssueOperations` + // seam, so the public chain has already wrapped it. + () => liveIssueRead(request), ); } @@ -95,11 +98,7 @@ export function* upsertIssue(request: IssueUpsertEffectRequest): Operation { + return IssueApi.operations.read(request.url, { + ...(request.provider === undefined ? {} : { provider: request.provider }), + }); +} + +/** The live transport call one upsert ends in, under this attempt's own key. */ +export function liveIssueUpsert( + request: IssueUpsertEffectRequest, + key: string, +): Operation { + return IssueApi.operations.upsert(request.issue, { + url: request.target, + ...(request.provider === undefined ? {} : { provider: request.provider }), + idempotencyKey: key, + }); +} + +/** + * What makes a second upsert attempt the same attempt. + * + * Derived from the complete request's own identity and target before any + * middleware saw it: a handler that transformed the issue, the target or the + * discriminator changes what is sent and changes nothing about which attempt + * this is. + */ +export function ownedUpsertKey(owner: string, site: string, target: string): string { + return issueIdempotencyKey({ runId: owner, expansionId: site }, "upsert", target); +} + /** * The complete read request this owner and site name. * diff --git a/packages/git/src/plugin.ts b/packages/git/src/plugin.ts index 85ce6bb56..cbbe2fc2e 100644 --- a/packages/git/src/plugin.ts +++ b/packages/git/src/plugin.ts @@ -196,6 +196,11 @@ export const gitPlugin: Plugin = Plugin({ // Declarations only. Every one of these is a name and a description; the // provider that performs what they name is attached by the host, lazily, // when a run actually has a Workspace to perform it in. + // + // Every command that carries this vocabulary carries all of it, `repl` + // included: what a name means is the same wherever it is typed, and an + // execution that retains one of these operations attaches its policy to + // this same body from inside its own scope. yield* useCompositionComponents(); return { admissions: [ diff --git a/packages/git/tests/composition-component-binding.test.ts b/packages/git/tests/composition-component-binding.test.ts new file mode 100644 index 000000000..8d54a9bc8 --- /dev/null +++ b/packages/git/tests/composition-component-binding.test.ts @@ -0,0 +1,389 @@ +/** + * One registry, one authored body, and the invocation an owning execution + * authenticates for it. + * + * Every row drives the actual shared components through canonical execution, + * over the whole vocabulary registered as the Plugin registers it. What differs + * between rows is the policy an owning execution declares underneath: these are + * neutral constructors and fake policies, so what is proven is the authored + * behavior and the identity each invocation runs under rather than any host's + * retention. The real native assembly, its retention and its cancellation are + * covered by the CLI and native suites. + * + * These rows carry A1, A2 and the binding half of A5 of the component-boundary + * repair matrix. + */ + +import { describe, it } from "@executablemd/test-support/bdd"; +import { expect } from "@executablemd/test-support/expect"; +import { + ensure, + race, + resource, + scoped, + sleep, + spawn, + suspend, + until, + withResolvers, +} from "effection"; +import type { Operation } from "effection"; +import { writeTextFile } from "@effectionx/fs"; +import { mkdtemp, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { collect, Component, registerComponents } from "@executablemd/core"; +import type { ComponentRegistration, Json } from "@executablemd/core"; +import { executeInstalled } from "@executablemd/core/host"; +import type { ExecutionInstallation, IdentityComponent } from "@executablemd/core/host"; +import { InMemoryStream } from "@executablemd/durable-streams"; +import { COMPOSITION_REGISTRATIONS } from "../src/composition/installation.ts"; +import { pushDeclaration } from "../src/composition/components/GitPush.ts"; +import { issueDeclaration } from "../src/composition/components/Issue.ts"; +import type { BindOperations } from "../src/composition/invocation-binding.ts"; +// From the subpath that publishes them rather than from the source modules: +// these are public types, and reaching them by relative path would bypass the +// export map the partition test holds every consumer to. +import type { IssueDetails, IssueReference } from "@executablemd/git/api"; +import type { IssueOperationsApi } from "../src/issue/operations.ts"; +import type { PullRequestOperationsApi } from "../src/composition/pull-request-operations.ts"; +import { COMPOSITION_ORIGIN } from "../src/composition/definitions.ts"; + +/** The two operations A's retained entry owns, by the names a document writes. */ +const OWNED: readonly string[] = ["Git.Push", "Issue"]; + +/** The six operations no local transaction encloses, by the names a document writes. */ +const OPERATIONS: readonly string[] = [ + "Git.Push", + "Issue", + "PullRequest", + "PullRequest.Checks", + "PullRequest.Comments", + "PullRequest.Reviews", +]; + +/** The tracker every upsert row files in. */ +const TRACKER = "https://github.com/octo/project"; + +/** What one row watched its policy be asked for. */ +interface Watched { + /** Each authenticated site a facade was built for, in the order built. */ + readonly sites: string[]; + /** Each operation performed, in the order the bodies asked for them. */ + readonly performed: string[]; +} + +function watched(): Watched { + return { sites: [], performed: [] }; +} + +/** One issue reference, built out of nothing: these rows retain no history. */ +function reference(url: string): IssueReference { + return { url }; +} + +function details(url: string): IssueDetails { + return { url, title: "fake", description: "fake", tags: [], assignee: null }; +} + +/** + * A policy that answers both issue operations itself and records what it was + * asked. + * + * Fake on purpose: what these rows are about is the handoff, so the policy + * below reaches no service, no journal and no provider. A real one is the + * entry's, and the native suites drive that. + */ +function fakeIssuePolicy(seen: Watched) { + return (site: string): IssueOperationsApi => { + seen.sites.push(site); + return { + read: function* (invocation): Operation { + seen.performed.push(`read ${invocation.url}`); + return details(invocation.url); + }, + upsert: function* (invocation): Operation { + seen.performed.push(`upsert ${invocation.target}`); + return reference(`${invocation.target}/issues/1`); + }, + }; + }; +} + +/** + * The declarations one row installs, as an owning execution installs them. + * + * One constructor per operation, the same ones the retained entry calls: an + * execution declares what it owns and nothing else, and every other name in + * this vocabulary stays the ordinary registration {@link run} makes below. + */ +function declaring(owned: { + readonly issue: BindOperations; +}): ExecutionInstallation { + return { components: [issueDeclaration(owned.issue)] }; +} + +interface Ran { + readonly output: string; + readonly refusal: string | undefined; +} + +/** One document, through canonical execution, under this row's declarations. */ +function run( + root: string, + source: string, + installations: readonly ExecutionInstallation[], + before?: () => Operation, +): Operation { + return scoped(function* (): Operation { + const path = join(root, `entry-${crypto.randomUUID()}.md`); + yield* writeTextFile(path, source); + // The whole vocabulary, as the Plugin registers it for every command. What + // a row declares below is attached inside the execution, over the ordinary + // registration of that same name — so a declaration that failed to reach + // its policy would reach the registered default's missing provider here + // rather than silently resolving to nothing. + yield* registerComponents(COMPOSITION_REGISTRATIONS); + if (before !== undefined) { + yield* before(); + } + let refusal: string | undefined; + let output = ""; + try { + output = String( + yield* collect( + yield* executeInstalled({ path, stream: new InMemoryStream(), includes: [root] }, [ + ...installations, + ]), + ), + ); + } catch (error) { + refusal = error instanceof Error ? error.message : String(error); + } + return { output, refusal }; + }); +} + +/** This suite's own directory, for the documents it writes. */ +function useRoot(): Operation { + return resource(function* (provide) { + const root = yield* until(mkdtemp(join(tmpdir(), "composition-binding-"))); + yield* ensure(() => until(rm(root, { recursive: true, force: true }))); + yield* provide(root); + }); +} + +/** + * One declaration of each operation A's retained entry owns. + * + * Built with policy nothing reaches: these rows are about what a declaration + * says, and a constructor that had to perform in order to describe would be + * telling an inspector something only a run can know. + */ +function ownedDeclarations(): readonly IdentityComponent[] { + return [ + pushDeclaration(() => () => { + throw new Error("a described declaration performed"); + }), + issueDeclaration(fakeIssuePolicy(watched())), + ]; +} + +/** The registration and the owned declaration of one name, for comparison. */ +function paired(name: string): { + registration: ComponentRegistration | undefined; + declaration: IdentityComponent | undefined; +} { + return { + registration: COMPOSITION_REGISTRATIONS.find((entry) => entry.name === name), + declaration: ownedDeclarations().find((entry) => entry.name === name), + }; +} + +describe("one registry for every repository operation, and the policy an execution owns", () => { + it("A1: describes one component whichever path carries it", function* (): Operation { + // The whole vocabulary is registered ordinarily, and an execution that owns + // one of these operations declares that same name again with its own policy + // under it. What a reader is told is the same either way: same origin, same + // props, same return contract, same forms, same prose. A metadata + // difference here would be two components under one name. + for (const name of OWNED) { + const { registration, declaration } = paired(name); + expect(registration).toBeDefined(); + expect(declaration).toBeDefined(); + expect(declaration?.origin).toBe(registration?.origin); + expect(declaration?.origin).toBe(COMPOSITION_ORIGIN); + expect(declaration?.props).toEqual(registration?.props); + expect(declaration?.returns).toEqual(registration?.returns); + expect(declaration?.forms).toEqual(registration?.forms); + expect(declaration?.description).toBe(registration?.description); + } + }); + + it("A1: one registry carries all sixteen names, the six operations included", function* (): Operation { + // There is no second catalog and no reduced one. Every command that carries + // this vocabulary registers the same sixteen, which is what says the names + // exist — to a run, to validation and to `xmd syntax` alike. + const registered = COMPOSITION_REGISTRATIONS.map((entry) => entry.name); + expect(registered).toHaveLength(16); + expect(new Set(registered).size).toBe(16); + for (const name of OPERATIONS) { + expect(registered).toContain(name); + } + // What an owning execution adds is policy for the operations it retains, + // under names this registry already has. It establishes no name of its own. + const owned = ownedDeclarations().map((entry) => entry.name); + expect(owned).toEqual([...OWNED]); + for (const name of owned) { + expect(registered).toContain(name); + } + }); + + it("A1: building declarations calls no factory and initializes no policy", function* (): Operation { + let built = 0; + const declarations = [ + issueDeclaration((site: string): IssueOperationsApi => { + built += 1; + return fakeIssuePolicy(watched())(site); + }), + ]; + expect(declarations).toHaveLength(1); + // Inspection and validation read these. Nothing has been authenticated, + // nothing has been bound, and no policy exists yet. + expect(built).toBe(0); + expect(declarations.map((entry) => entry.name)).toEqual(["Issue"]); + }); + + it("A1: canonical dispatch decides the form, whatever a contextual answer says", function* (): Operation { + const root = yield* useRoot(); + const seen = watched(); + + // A handler that reports the opposite shape to anything that asks. The form + // this element was written as is the scan's, so the read body runs and the + // upsert body does not. + const lying = function* (): Operation { + yield* Component.around({ + // deno-lint-ignore require-yield + *hasContent(): Operation { + return true; + }, + }); + }; + + const ran = yield* run( + root, + [ + ``, + ``, + ``, + "", + ].join("\n"), + [declaring({ issue: fakeIssuePolicy(seen) })], + lying, + ); + + expect(ran.refusal).toBe(undefined); + expect(seen.performed).toEqual([`read ${TRACKER}/issues/7`]); + }); + + it("A2: a nested read finishes before the upsert around it is performed", function* (): Operation { + const root = yield* useRoot(); + const seen = watched(); + + const ran = yield* run( + root, + [ + ``, + ``, + `what the inner read found: {inner.url}`, + ``, + ``, + "", + ].join("\n"), + [declaring({ issue: fakeIssuePolicy(seen) })], + ); + + expect(ran.refusal).toBe(undefined); + // The child's own operation first: an upsert renders its description before + // it is offered, so a child that retains work of its own finishes first. + expect(seen.performed).toEqual([`read ${TRACKER}/issues/3`, `upsert ${TRACKER}`]); + // Two invocations, two facades, two sites — the outer one built first. + expect(seen.sites).toHaveLength(2); + expect(new Set(seen.sites).size).toBe(2); + }); + + it("A2: two concurrent invocations keep their own sites", function* (): Operation { + const root = yield* useRoot(); + const seen = watched(); + + const ran = yield* run( + root, + [ + ``, + ``, + `first`, + `second`, + ``, + ``, + "", + ].join("\n"), + [declaring({ issue: fakeIssuePolicy(seen) })], + ); + + expect(ran.refusal).toBe(undefined); + expect(seen.sites).toHaveLength(2); + expect(new Set(seen.sites).size).toBe(2); + }); + + it("A5: cancelling an invocation stops the operation it was performing", function* (): Operation { + const root = yield* useRoot(); + const seen = watched(); + const reached = withResolvers(); + + // The upsert suspends where a slow service would, and this row halts the + // whole execution while it is standing there. + const suspending = (site: string): IssueOperationsApi => { + seen.sites.push(site); + return { + read: function* (invocation): Operation { + return details(invocation.url); + }, + upsert: function* (invocation): Operation { + seen.performed.push(`upsert ${invocation.target}`); + reached.resolve(); + yield* suspend(); + throw new Error("a suspended upsert resumed"); + }, + }; + }; + + yield* scoped(function* (): Operation { + const task = yield* spawn(() => + run( + root, + [ + ``, + `described`, + ``, + "", + ].join("\n"), + [declaring({ issue: suspending })], + ), + ); + yield* race([ + reached.operation, + (function* (): Operation { + yield* sleep(30_000); + throw new Error("the upsert never reached its policy"); + })(), + ]); + // Joined here: the halt returns once the suspended upsert has been torn + // down, so what follows cannot be read before cancellation finished. + yield* task.halt(); + }); + + // The upsert was entered once, never resumed and never performed again. + expect(seen.performed).toEqual([`upsert ${TRACKER}`]); + expect(seen.sites).toHaveLength(1); + }); +}); diff --git a/packages/git/tests/fixtures/retained-repository-history/h1-push-completed/journal.json b/packages/git/tests/fixtures/retained-repository-history/h1-push-completed/journal.json new file mode 100644 index 000000000..f3fdb61d4 --- /dev/null +++ b/packages/git/tests/fixtures/retained-repository-history/h1-push-completed/journal.json @@ -0,0 +1,542 @@ +[ + { + "type": "yield", + "coroutineId": "root", + "description": { + "type": "import_component", + "name": "__root__" + }, + "result": { + "status": "ok", + "value": { + "kind": "repository", + "path": "/private/tmp/xmd-retained-history/h1-push-completed/entry.md", + "content": "\n \n ready\n \n \n \n \nThe branch was published. Continue?\n \n continued\n\n" + } + } + }, + { + "type": "yield", + "coroutineId": "root", + "description": { + "type": "import_component", + "name": "Repository", + "executablemd.source-position": { + "path": "/private/tmp/xmd-retained-history/h1-push-completed/entry.md", + "offset": 0, + "line": 1, + "column": 1 + } + }, + "result": { + "status": "ok", + "value": { + "kind": "registered", + "origin": "@executablemd/workflow/composition", + "reserved": false + } + } + }, + { + "type": "yield", + "coroutineId": "root", + "description": { + "type": "repl_repository_placement", + "name": "[\"executablemd.git.repl-repository\",\"placement\",1,\"repository\",\"a11c187456d5257ffca2eaed19edd562b1e7b22ba0a67629e100b3dc89c93b5d\"]" + }, + "result": { + "status": "ok", + "value": { + "version": 1, + "kind": "repository", + "fingerprint": "a11c187456d5257ffca2eaed19edd562b1e7b22ba0a67629e100b3dc89c93b5d", + "request": { + "name": "project", + "locatorFingerprint": "dff3cfc11f18b2dacddf94962cac900c96fa6eb791888f1c491d795e7d74ea5a", + "base": null + }, + "placement": { + "managedRoot": "/private/tmp/xmd-retained-history/h1-push-completed/repositories", + "checkoutRoot": "/private/tmp/xmd-retained-history/h1-push-completed/repositories/repositories/66df1c09a6887c9b3de07b3b710ea14b86f91570b55ec069c9340004c4a43b4a/checkout", + "commonDirectory": "/private/tmp/xmd-retained-history/h1-push-completed/repositories/repositories/66df1c09a6887c9b3de07b3b710ea14b86f91570b55ec069c9340004c4a43b4a/checkout/.git", + "identity": { + "name": "project", + "locatorFingerprint": "dff3cfc11f18b2dacddf94962cac900c96fa6eb791888f1c491d795e7d74ea5a", + "requestedBase": null, + "creationCommit": "785495034b274fb9c8521e92105001eed072f916", + "primaryBranch": "main", + "objectFormat": "sha1" + }, + "displayName": "project", + "ownerCheckout": "/private/tmp/xmd-retained-history/h1-push-completed/repositories/repositories/66df1c09a6887c9b3de07b3b710ea14b86f91570b55ec069c9340004c4a43b4a/checkout" + } + } + } + }, + { + "type": "yield", + "coroutineId": "root", + "description": { + "type": "import_component", + "name": "Git.Switch", + "executablemd.source-position": { + "path": "/private/tmp/xmd-retained-history/h1-push-completed/entry.md", + "offset": 99, + "line": 2, + "column": 3 + } + }, + "result": { + "status": "ok", + "value": { + "kind": "registered", + "origin": "@executablemd/workflow/composition", + "reserved": false + } + } + }, + { + "type": "yield", + "coroutineId": "root", + "description": { + "type": "repl_repository_git", + "name": "[\"executablemd.git.repl-repository\",\"git\",1,\"switch\",\"880af1c7a2a3a2c20754022bac3ef106b8a659e4c6602aaee10f2c2881f93a8e\"]" + }, + "result": { + "status": "ok", + "value": { + "version": 1, + "operation": "switch", + "fingerprint": "880af1c7a2a3a2c20754022bac3ef106b8a659e4c6602aaee10f2c2881f93a8e", + "request": { + "repository": { + "name": "project", + "locatorFingerprint": "dff3cfc11f18b2dacddf94962cac900c96fa6eb791888f1c491d795e7d74ea5a", + "requestedBase": null, + "creationCommit": "785495034b274fb9c8521e92105001eed072f916", + "primaryBranch": "main", + "objectFormat": "sha1" + }, + "selectionCheckout": "/private/tmp/xmd-retained-history/h1-push-completed/repositories/repositories/66df1c09a6887c9b3de07b3b710ea14b86f91570b55ec069c9340004c4a43b4a/checkout", + "commonDirectory": "/private/tmp/xmd-retained-history/h1-push-completed/repositories/repositories/66df1c09a6887c9b3de07b3b710ea14b86f91570b55ec069c9340004c4a43b4a/checkout/.git", + "workingDirectory": "/private/tmp/xmd-retained-history/h1-push-completed/repositories/repositories/66df1c09a6887c9b3de07b3b710ea14b86f91570b55ec069c9340004c4a43b4a/checkout", + "branch": "review/completed", + "base": null + }, + "outcome": { + "ok": true, + "value": { + "checkout": { + "repositoryName": "project", + "worktreeName": null, + "checkoutPath": "/private/tmp/xmd-retained-history/h1-push-completed/repositories/repositories/66df1c09a6887c9b3de07b3b710ea14b86f91570b55ec069c9340004c4a43b4a/checkout" + }, + "requestedBranch": "review/completed", + "resolvedBranch": "review/completed", + "requestedBase": null, + "resolvedBase": "785495034b274fb9c8521e92105001eed072f916", + "before": { + "branch": "main", + "commit": "785495034b274fb9c8521e92105001eed072f916", + "headTree": "15587c8d6ad8e073e7178240a8117246bdfba4eb", + "indexTree": "15587c8d6ad8e073e7178240a8117246bdfba4eb" + }, + "after": { + "branch": "review/completed", + "commit": "785495034b274fb9c8521e92105001eed072f916", + "headTree": "15587c8d6ad8e073e7178240a8117246bdfba4eb", + "indexTree": "15587c8d6ad8e073e7178240a8117246bdfba4eb" + } + } + } + } + } + }, + { + "type": "yield", + "coroutineId": "root", + "description": { + "type": "import_component", + "name": "File", + "executablemd.source-position": { + "path": "/private/tmp/xmd-retained-history/h1-push-completed/entry.md", + "offset": 142, + "line": 3, + "column": 3 + } + }, + "result": { + "status": "ok", + "value": { + "kind": "protected" + } + } + }, + { + "type": "yield", + "coroutineId": "root", + "description": { + "type": "repl_repository_local", + "name": "[\"executablemd.git.repl-repository\",\"local\",1,\"file-write\",\"80b0867e3d0168bb06e69e581f9ccb9b5e663a15ad0380769f5322a4cce4408c\"]" + }, + "result": { + "status": "ok", + "value": { + "version": 1, + "operation": "file-write", + "fingerprint": "80b0867e3d0168bb06e69e581f9ccb9b5e663a15ad0380769f5322a4cce4408c", + "request": { + "cwd": "/private/tmp/xmd-retained-history/h1-push-completed/repositories/repositories/66df1c09a6887c9b3de07b3b710ea14b86f91570b55ec069c9340004c4a43b4a/checkout", + "path": "notes.md", + "content": "ready" + }, + "success": { + "type": "executablemd.runtime.files-write-success/v1", + "publication": "host-committed" + } + } + } + }, + { + "type": "yield", + "coroutineId": "root", + "description": { + "type": "import_component", + "name": "Git.Add", + "executablemd.source-position": { + "path": "/private/tmp/xmd-retained-history/h1-push-completed/entry.md", + "offset": 179, + "line": 4, + "column": 3 + } + }, + "result": { + "status": "ok", + "value": { + "kind": "registered", + "origin": "@executablemd/workflow/composition", + "reserved": false + } + } + }, + { + "type": "yield", + "coroutineId": "root", + "description": { + "type": "repl_repository_git", + "name": "[\"executablemd.git.repl-repository\",\"git\",1,\"add\",\"3fc2169288cf2b9bce669f261a406697223f68547067ff0c3314c992bed64333\"]" + }, + "result": { + "status": "ok", + "value": { + "version": 1, + "operation": "add", + "fingerprint": "3fc2169288cf2b9bce669f261a406697223f68547067ff0c3314c992bed64333", + "request": { + "repository": { + "name": "project", + "locatorFingerprint": "dff3cfc11f18b2dacddf94962cac900c96fa6eb791888f1c491d795e7d74ea5a", + "requestedBase": null, + "creationCommit": "785495034b274fb9c8521e92105001eed072f916", + "primaryBranch": "main", + "objectFormat": "sha1" + }, + "selectionCheckout": "/private/tmp/xmd-retained-history/h1-push-completed/repositories/repositories/66df1c09a6887c9b3de07b3b710ea14b86f91570b55ec069c9340004c4a43b4a/checkout", + "commonDirectory": "/private/tmp/xmd-retained-history/h1-push-completed/repositories/repositories/66df1c09a6887c9b3de07b3b710ea14b86f91570b55ec069c9340004c4a43b4a/checkout/.git", + "workingDirectory": "/private/tmp/xmd-retained-history/h1-push-completed/repositories/repositories/66df1c09a6887c9b3de07b3b710ea14b86f91570b55ec069c9340004c4a43b4a/checkout", + "paths": [ + "notes.md" + ] + }, + "outcome": { + "ok": true, + "value": { + "checkout": { + "repositoryName": "project", + "worktreeName": null, + "checkoutPath": "/private/tmp/xmd-retained-history/h1-push-completed/repositories/repositories/66df1c09a6887c9b3de07b3b710ea14b86f91570b55ec069c9340004c4a43b4a/checkout" + }, + "paths": [ + "notes.md" + ], + "before": { + "branch": "review/completed", + "commit": "785495034b274fb9c8521e92105001eed072f916", + "headTree": "15587c8d6ad8e073e7178240a8117246bdfba4eb", + "indexTree": "15587c8d6ad8e073e7178240a8117246bdfba4eb" + }, + "after": { + "branch": "review/completed", + "commit": "785495034b274fb9c8521e92105001eed072f916", + "headTree": "15587c8d6ad8e073e7178240a8117246bdfba4eb", + "indexTree": "17b0bdfbe7c7c448d7214f8910598edd1321ea6f" + } + } + } + } + } + }, + { + "type": "yield", + "coroutineId": "root", + "description": { + "type": "import_component", + "name": "Git.Commit", + "executablemd.source-position": { + "path": "/private/tmp/xmd-retained-history/h1-push-completed/entry.md", + "offset": 210, + "line": 5, + "column": 3 + } + }, + "result": { + "status": "ok", + "value": { + "kind": "registered", + "origin": "@executablemd/workflow/composition", + "reserved": false + } + } + }, + { + "type": "yield", + "coroutineId": "root", + "description": { + "type": "repl_repository_git", + "name": "[\"executablemd.git.repl-repository\",\"git\",1,\"commit\",\"d31cd38eb4739b5977e47fa19bd9ff85a2542dee29fa0bf1d948be530ef38937\"]" + }, + "result": { + "status": "ok", + "value": { + "version": 1, + "operation": "commit", + "fingerprint": "d31cd38eb4739b5977e47fa19bd9ff85a2542dee29fa0bf1d948be530ef38937", + "request": { + "repository": { + "name": "project", + "locatorFingerprint": "dff3cfc11f18b2dacddf94962cac900c96fa6eb791888f1c491d795e7d74ea5a", + "requestedBase": null, + "creationCommit": "785495034b274fb9c8521e92105001eed072f916", + "primaryBranch": "main", + "objectFormat": "sha1" + }, + "selectionCheckout": "/private/tmp/xmd-retained-history/h1-push-completed/repositories/repositories/66df1c09a6887c9b3de07b3b710ea14b86f91570b55ec069c9340004c4a43b4a/checkout", + "commonDirectory": "/private/tmp/xmd-retained-history/h1-push-completed/repositories/repositories/66df1c09a6887c9b3de07b3b710ea14b86f91570b55ec069c9340004c4a43b4a/checkout/.git", + "workingDirectory": "/private/tmp/xmd-retained-history/h1-push-completed/repositories/repositories/66df1c09a6887c9b3de07b3b710ea14b86f91570b55ec069c9340004c4a43b4a/checkout", + "messageSource": "prop", + "messageDigest": "352f8f95e3173a75a2f287e20aa909e8207d03e6d9fac50a5cef8aa14b28ef88", + "messageLength": 14 + }, + "outcome": { + "ok": true, + "value": { + "checkout": { + "repositoryName": "project", + "worktreeName": null, + "checkoutPath": "/private/tmp/xmd-retained-history/h1-push-completed/repositories/repositories/66df1c09a6887c9b3de07b3b710ea14b86f91570b55ec069c9340004c4a43b4a/checkout" + }, + "messageSource": "prop", + "messageDigest": "352f8f95e3173a75a2f287e20aa909e8207d03e6d9fac50a5cef8aa14b28ef88", + "messageLength": 14, + "parent": "785495034b274fb9c8521e92105001eed072f916", + "tree": "17b0bdfbe7c7c448d7214f8910598edd1321ea6f", + "commit": "953925a9da72933365d566ee724593c97b7e939b", + "committedAt": 1792337425, + "before": { + "branch": "review/completed", + "commit": "785495034b274fb9c8521e92105001eed072f916", + "headTree": "15587c8d6ad8e073e7178240a8117246bdfba4eb", + "indexTree": "17b0bdfbe7c7c448d7214f8910598edd1321ea6f" + }, + "after": { + "branch": "review/completed", + "commit": "953925a9da72933365d566ee724593c97b7e939b", + "headTree": "17b0bdfbe7c7c448d7214f8910598edd1321ea6f", + "indexTree": "17b0bdfbe7c7c448d7214f8910598edd1321ea6f" + } + } + } + } + } + }, + { + "type": "yield", + "coroutineId": "root", + "description": { + "type": "import_component", + "name": "Git.Push", + "executablemd.source-position": { + "path": "/private/tmp/xmd-retained-history/h1-push-completed/entry.md", + "offset": 263, + "line": 6, + "column": 3 + } + }, + "result": { + "status": "ok", + "value": { + "kind": "registered", + "origin": "@executablemd/workflow/composition", + "reserved": false + } + } + }, + { + "type": "yield", + "coroutineId": "root", + "description": { + "type": "repl_repository_push_request", + "name": "c3db77f72e355c281b62e7dcd862837027f679d6279501797dc29e7c031dca24" + }, + "result": { + "status": "ok", + "value": { + "version": 1, + "owner": "[\"executablemd.repl.issue-owner\",1,\"h1\",\"1\"]", + "site": "974906d21df50bec66c33ccad4b16bab9da9743275ad3d12240d7132ffa42b9b", + "requested": { + "repository": { + "name": "project", + "locatorFingerprint": "dff3cfc11f18b2dacddf94962cac900c96fa6eb791888f1c491d795e7d74ea5a", + "requestedBase": null, + "creationCommit": "785495034b274fb9c8521e92105001eed072f916", + "primaryBranch": "main", + "objectFormat": "sha1" + }, + "selectionCheckout": "/private/tmp/xmd-retained-history/h1-push-completed/repositories/repositories/66df1c09a6887c9b3de07b3b710ea14b86f91570b55ec069c9340004c4a43b4a/checkout", + "commonDirectory": "/private/tmp/xmd-retained-history/h1-push-completed/repositories/repositories/66df1c09a6887c9b3de07b3b710ea14b86f91570b55ec069c9340004c4a43b4a/checkout/.git", + "workingDirectory": "/private/tmp/xmd-retained-history/h1-push-completed/repositories/repositories/66df1c09a6887c9b3de07b3b710ea14b86f91570b55ec069c9340004c4a43b4a/checkout" + }, + "received": { + "repository": { + "name": "project", + "locatorFingerprint": "dff3cfc11f18b2dacddf94962cac900c96fa6eb791888f1c491d795e7d74ea5a", + "requestedBase": null, + "creationCommit": "785495034b274fb9c8521e92105001eed072f916", + "primaryBranch": "main", + "objectFormat": "sha1" + }, + "selectionCheckout": "/private/tmp/xmd-retained-history/h1-push-completed/repositories/repositories/66df1c09a6887c9b3de07b3b710ea14b86f91570b55ec069c9340004c4a43b4a/checkout", + "commonDirectory": "/private/tmp/xmd-retained-history/h1-push-completed/repositories/repositories/66df1c09a6887c9b3de07b3b710ea14b86f91570b55ec069c9340004c4a43b4a/checkout/.git", + "workingDirectory": "/private/tmp/xmd-retained-history/h1-push-completed/repositories/repositories/66df1c09a6887c9b3de07b3b710ea14b86f91570b55ec069c9340004c4a43b4a/checkout" + }, + "resolved": { + "checkoutRoot": "/private/tmp/xmd-retained-history/h1-push-completed/repositories/repositories/66df1c09a6887c9b3de07b3b710ea14b86f91570b55ec069c9340004c4a43b4a/checkout", + "commonDirectory": "/private/tmp/xmd-retained-history/h1-push-completed/repositories/repositories/66df1c09a6887c9b3de07b3b710ea14b86f91570b55ec069c9340004c4a43b4a/checkout/.git", + "origin": "/private/tmp/xmd-retained-history/h1-push-completed/origin.git", + "inputs": { + "repository": { + "name": "project", + "locatorFingerprint": "dff3cfc11f18b2dacddf94962cac900c96fa6eb791888f1c491d795e7d74ea5a", + "requestedBase": null, + "creationCommit": "785495034b274fb9c8521e92105001eed072f916", + "primaryBranch": "main", + "objectFormat": "sha1" + }, + "remote": "origin", + "branch": "review/completed", + "destinationRef": "refs/heads/review/completed", + "sourceCommit": "953925a9da72933365d566ee724593c97b7e939b" + } + } + } + } + }, + { + "type": "yield", + "coroutineId": "root", + "description": { + "type": "git_host_effect", + "name": "730d9587e32009d4b1f34002ed66a26f320841fee3e934f9163e4023e6a95401", + "executablemd.source-position": { + "path": "/private/tmp/xmd-retained-history/h1-push-completed/entry.md", + "offset": 263, + "line": 6, + "column": 3 + } + }, + "result": { + "status": "ok", + "value": { + "request": { + "identity": { + "runId": "[\"executablemd.repl.issue-owner\",1,\"h1\",\"1\"]", + "expansionId": "974906d21df50bec66c33ccad4b16bab9da9743275ad3d12240d7132ffa42b9b" + }, + "kind": "git-push", + "inputs": { + "repository": { + "name": "project", + "locatorFingerprint": "dff3cfc11f18b2dacddf94962cac900c96fa6eb791888f1c491d795e7d74ea5a", + "requestedBase": null, + "creationCommit": "785495034b274fb9c8521e92105001eed072f916", + "primaryBranch": "main", + "objectFormat": "sha1" + }, + "remote": "origin", + "branch": "review/completed", + "destinationRef": "refs/heads/review/completed", + "sourceCommit": "953925a9da72933365d566ee724593c97b7e939b" + }, + "naturalKey": { + "repository": { + "name": "project", + "locatorFingerprint": "dff3cfc11f18b2dacddf94962cac900c96fa6eb791888f1c491d795e7d74ea5a", + "requestedBase": null, + "creationCommit": "785495034b274fb9c8521e92105001eed072f916", + "primaryBranch": "main", + "objectFormat": "sha1" + }, + "remote": "origin", + "destinationRef": "refs/heads/review/completed" + } + }, + "preState": { + "remoteCommit": null + }, + "observations": { + "remoteCommit": "953925a9da72933365d566ee724593c97b7e939b" + }, + "decision": "performed", + "result": { + "repository": { + "name": "project", + "locatorFingerprint": "dff3cfc11f18b2dacddf94962cac900c96fa6eb791888f1c491d795e7d74ea5a", + "requestedBase": null, + "creationCommit": "785495034b274fb9c8521e92105001eed072f916", + "primaryBranch": "main", + "objectFormat": "sha1" + }, + "remote": "origin", + "branch": "review/completed", + "destinationRef": "refs/heads/review/completed", + "refspec": "953925a9da72933365d566ee724593c97b7e939b:refs/heads/review/completed", + "sourceCommit": "953925a9da72933365d566ee724593c97b7e939b", + "observedRemoteCommit": "953925a9da72933365d566ee724593c97b7e939b" + } + } + } + }, + { + "type": "yield", + "coroutineId": "root", + "description": { + "type": "import_component", + "name": "Elicit", + "executablemd.source-position": { + "path": "/private/tmp/xmd-retained-history/h1-push-completed/entry.md", + "offset": 278, + "line": 7, + "column": 3 + } + }, + "result": { + "status": "ok", + "value": { + "kind": "registered", + "origin": "@executablemd/core", + "reserved": false + } + } + } +] diff --git a/packages/git/tests/fixtures/retained-repository-history/h1-push-completed/source.md b/packages/git/tests/fixtures/retained-repository-history/h1-push-completed/source.md new file mode 100644 index 000000000..d7799543e --- /dev/null +++ b/packages/git/tests/fixtures/retained-repository-history/h1-push-completed/source.md @@ -0,0 +1,12 @@ + + + ready + + + + +The branch was published. Continue? + + continued + diff --git a/packages/git/tests/fixtures/retained-repository-history/h2-push-pending/journal.json b/packages/git/tests/fixtures/retained-repository-history/h2-push-pending/journal.json new file mode 100644 index 000000000..316f8832b --- /dev/null +++ b/packages/git/tests/fixtures/retained-repository-history/h2-push-pending/journal.json @@ -0,0 +1,445 @@ +[ + { + "type": "yield", + "coroutineId": "root", + "description": { + "type": "import_component", + "name": "__root__" + }, + "result": { + "status": "ok", + "value": { + "kind": "repository", + "path": "/private/tmp/xmd-retained-history/h2-push-pending/entry.md", + "content": "\n \n ready\n \n \n \n \nThe branch was published. Continue?\n \n continued\n\n" + } + } + }, + { + "type": "yield", + "coroutineId": "root", + "description": { + "type": "import_component", + "name": "Repository", + "executablemd.source-position": { + "path": "/private/tmp/xmd-retained-history/h2-push-pending/entry.md", + "offset": 0, + "line": 1, + "column": 1 + } + }, + "result": { + "status": "ok", + "value": { + "kind": "registered", + "origin": "@executablemd/workflow/composition", + "reserved": false + } + } + }, + { + "type": "yield", + "coroutineId": "root", + "description": { + "type": "repl_repository_placement", + "name": "[\"executablemd.git.repl-repository\",\"placement\",1,\"repository\",\"e8b94a37898295599aaacc48ea75f9d81d9cd992fd7be6abe714d0a15d3c688c\"]" + }, + "result": { + "status": "ok", + "value": { + "version": 1, + "kind": "repository", + "fingerprint": "e8b94a37898295599aaacc48ea75f9d81d9cd992fd7be6abe714d0a15d3c688c", + "request": { + "name": "project", + "locatorFingerprint": "3381fbae1def29f0c9e4f75d6ee98db4ddcf8329afe1a472fa826e89ab4c8db3", + "base": null + }, + "placement": { + "managedRoot": "/private/tmp/xmd-retained-history/h2-push-pending/repositories", + "checkoutRoot": "/private/tmp/xmd-retained-history/h2-push-pending/repositories/repositories/85e770fa023edae002ab459e9fcaf91b1e79b2addaf2d89ba6104ae3fe3e9f90/checkout", + "commonDirectory": "/private/tmp/xmd-retained-history/h2-push-pending/repositories/repositories/85e770fa023edae002ab459e9fcaf91b1e79b2addaf2d89ba6104ae3fe3e9f90/checkout/.git", + "identity": { + "name": "project", + "locatorFingerprint": "3381fbae1def29f0c9e4f75d6ee98db4ddcf8329afe1a472fa826e89ab4c8db3", + "requestedBase": null, + "creationCommit": "68dde90e63566966c4b3a51b254211f44894a21b", + "primaryBranch": "main", + "objectFormat": "sha1" + }, + "displayName": "project", + "ownerCheckout": "/private/tmp/xmd-retained-history/h2-push-pending/repositories/repositories/85e770fa023edae002ab459e9fcaf91b1e79b2addaf2d89ba6104ae3fe3e9f90/checkout" + } + } + } + }, + { + "type": "yield", + "coroutineId": "root", + "description": { + "type": "import_component", + "name": "Git.Switch", + "executablemd.source-position": { + "path": "/private/tmp/xmd-retained-history/h2-push-pending/entry.md", + "offset": 97, + "line": 2, + "column": 3 + } + }, + "result": { + "status": "ok", + "value": { + "kind": "registered", + "origin": "@executablemd/workflow/composition", + "reserved": false + } + } + }, + { + "type": "yield", + "coroutineId": "root", + "description": { + "type": "repl_repository_git", + "name": "[\"executablemd.git.repl-repository\",\"git\",1,\"switch\",\"d43fe962f69ffb482b3935a1c5feb19c48e4aec69d363969f2d3ddb67b07f226\"]" + }, + "result": { + "status": "ok", + "value": { + "version": 1, + "operation": "switch", + "fingerprint": "d43fe962f69ffb482b3935a1c5feb19c48e4aec69d363969f2d3ddb67b07f226", + "request": { + "repository": { + "name": "project", + "locatorFingerprint": "3381fbae1def29f0c9e4f75d6ee98db4ddcf8329afe1a472fa826e89ab4c8db3", + "requestedBase": null, + "creationCommit": "68dde90e63566966c4b3a51b254211f44894a21b", + "primaryBranch": "main", + "objectFormat": "sha1" + }, + "selectionCheckout": "/private/tmp/xmd-retained-history/h2-push-pending/repositories/repositories/85e770fa023edae002ab459e9fcaf91b1e79b2addaf2d89ba6104ae3fe3e9f90/checkout", + "commonDirectory": "/private/tmp/xmd-retained-history/h2-push-pending/repositories/repositories/85e770fa023edae002ab459e9fcaf91b1e79b2addaf2d89ba6104ae3fe3e9f90/checkout/.git", + "workingDirectory": "/private/tmp/xmd-retained-history/h2-push-pending/repositories/repositories/85e770fa023edae002ab459e9fcaf91b1e79b2addaf2d89ba6104ae3fe3e9f90/checkout", + "branch": "review/pending", + "base": null + }, + "outcome": { + "ok": true, + "value": { + "checkout": { + "repositoryName": "project", + "worktreeName": null, + "checkoutPath": "/private/tmp/xmd-retained-history/h2-push-pending/repositories/repositories/85e770fa023edae002ab459e9fcaf91b1e79b2addaf2d89ba6104ae3fe3e9f90/checkout" + }, + "requestedBranch": "review/pending", + "resolvedBranch": "review/pending", + "requestedBase": null, + "resolvedBase": "68dde90e63566966c4b3a51b254211f44894a21b", + "before": { + "branch": "main", + "commit": "68dde90e63566966c4b3a51b254211f44894a21b", + "headTree": "15587c8d6ad8e073e7178240a8117246bdfba4eb", + "indexTree": "15587c8d6ad8e073e7178240a8117246bdfba4eb" + }, + "after": { + "branch": "review/pending", + "commit": "68dde90e63566966c4b3a51b254211f44894a21b", + "headTree": "15587c8d6ad8e073e7178240a8117246bdfba4eb", + "indexTree": "15587c8d6ad8e073e7178240a8117246bdfba4eb" + } + } + } + } + } + }, + { + "type": "yield", + "coroutineId": "root", + "description": { + "type": "import_component", + "name": "File", + "executablemd.source-position": { + "path": "/private/tmp/xmd-retained-history/h2-push-pending/entry.md", + "offset": 138, + "line": 3, + "column": 3 + } + }, + "result": { + "status": "ok", + "value": { + "kind": "protected" + } + } + }, + { + "type": "yield", + "coroutineId": "root", + "description": { + "type": "repl_repository_local", + "name": "[\"executablemd.git.repl-repository\",\"local\",1,\"file-write\",\"97c8b89196fee15328fc0418f85d79c42827ef3ad8b9abce68201866d4d0dd68\"]" + }, + "result": { + "status": "ok", + "value": { + "version": 1, + "operation": "file-write", + "fingerprint": "97c8b89196fee15328fc0418f85d79c42827ef3ad8b9abce68201866d4d0dd68", + "request": { + "cwd": "/private/tmp/xmd-retained-history/h2-push-pending/repositories/repositories/85e770fa023edae002ab459e9fcaf91b1e79b2addaf2d89ba6104ae3fe3e9f90/checkout", + "path": "notes.md", + "content": "ready" + }, + "success": { + "type": "executablemd.runtime.files-write-success/v1", + "publication": "host-committed" + } + } + } + }, + { + "type": "yield", + "coroutineId": "root", + "description": { + "type": "import_component", + "name": "Git.Add", + "executablemd.source-position": { + "path": "/private/tmp/xmd-retained-history/h2-push-pending/entry.md", + "offset": 175, + "line": 4, + "column": 3 + } + }, + "result": { + "status": "ok", + "value": { + "kind": "registered", + "origin": "@executablemd/workflow/composition", + "reserved": false + } + } + }, + { + "type": "yield", + "coroutineId": "root", + "description": { + "type": "repl_repository_git", + "name": "[\"executablemd.git.repl-repository\",\"git\",1,\"add\",\"320a65867fe565a759e5e4a6af961808dab79163f788470be1b16329dafaa412\"]" + }, + "result": { + "status": "ok", + "value": { + "version": 1, + "operation": "add", + "fingerprint": "320a65867fe565a759e5e4a6af961808dab79163f788470be1b16329dafaa412", + "request": { + "repository": { + "name": "project", + "locatorFingerprint": "3381fbae1def29f0c9e4f75d6ee98db4ddcf8329afe1a472fa826e89ab4c8db3", + "requestedBase": null, + "creationCommit": "68dde90e63566966c4b3a51b254211f44894a21b", + "primaryBranch": "main", + "objectFormat": "sha1" + }, + "selectionCheckout": "/private/tmp/xmd-retained-history/h2-push-pending/repositories/repositories/85e770fa023edae002ab459e9fcaf91b1e79b2addaf2d89ba6104ae3fe3e9f90/checkout", + "commonDirectory": "/private/tmp/xmd-retained-history/h2-push-pending/repositories/repositories/85e770fa023edae002ab459e9fcaf91b1e79b2addaf2d89ba6104ae3fe3e9f90/checkout/.git", + "workingDirectory": "/private/tmp/xmd-retained-history/h2-push-pending/repositories/repositories/85e770fa023edae002ab459e9fcaf91b1e79b2addaf2d89ba6104ae3fe3e9f90/checkout", + "paths": [ + "notes.md" + ] + }, + "outcome": { + "ok": true, + "value": { + "checkout": { + "repositoryName": "project", + "worktreeName": null, + "checkoutPath": "/private/tmp/xmd-retained-history/h2-push-pending/repositories/repositories/85e770fa023edae002ab459e9fcaf91b1e79b2addaf2d89ba6104ae3fe3e9f90/checkout" + }, + "paths": [ + "notes.md" + ], + "before": { + "branch": "review/pending", + "commit": "68dde90e63566966c4b3a51b254211f44894a21b", + "headTree": "15587c8d6ad8e073e7178240a8117246bdfba4eb", + "indexTree": "15587c8d6ad8e073e7178240a8117246bdfba4eb" + }, + "after": { + "branch": "review/pending", + "commit": "68dde90e63566966c4b3a51b254211f44894a21b", + "headTree": "15587c8d6ad8e073e7178240a8117246bdfba4eb", + "indexTree": "17b0bdfbe7c7c448d7214f8910598edd1321ea6f" + } + } + } + } + } + }, + { + "type": "yield", + "coroutineId": "root", + "description": { + "type": "import_component", + "name": "Git.Commit", + "executablemd.source-position": { + "path": "/private/tmp/xmd-retained-history/h2-push-pending/entry.md", + "offset": 206, + "line": 5, + "column": 3 + } + }, + "result": { + "status": "ok", + "value": { + "kind": "registered", + "origin": "@executablemd/workflow/composition", + "reserved": false + } + } + }, + { + "type": "yield", + "coroutineId": "root", + "description": { + "type": "repl_repository_git", + "name": "[\"executablemd.git.repl-repository\",\"git\",1,\"commit\",\"6f829a1a8a1ee464163de3014b6ca8c4878739231a77b8023bcd34408e7b40c5\"]" + }, + "result": { + "status": "ok", + "value": { + "version": 1, + "operation": "commit", + "fingerprint": "6f829a1a8a1ee464163de3014b6ca8c4878739231a77b8023bcd34408e7b40c5", + "request": { + "repository": { + "name": "project", + "locatorFingerprint": "3381fbae1def29f0c9e4f75d6ee98db4ddcf8329afe1a472fa826e89ab4c8db3", + "requestedBase": null, + "creationCommit": "68dde90e63566966c4b3a51b254211f44894a21b", + "primaryBranch": "main", + "objectFormat": "sha1" + }, + "selectionCheckout": "/private/tmp/xmd-retained-history/h2-push-pending/repositories/repositories/85e770fa023edae002ab459e9fcaf91b1e79b2addaf2d89ba6104ae3fe3e9f90/checkout", + "commonDirectory": "/private/tmp/xmd-retained-history/h2-push-pending/repositories/repositories/85e770fa023edae002ab459e9fcaf91b1e79b2addaf2d89ba6104ae3fe3e9f90/checkout/.git", + "workingDirectory": "/private/tmp/xmd-retained-history/h2-push-pending/repositories/repositories/85e770fa023edae002ab459e9fcaf91b1e79b2addaf2d89ba6104ae3fe3e9f90/checkout", + "messageSource": "prop", + "messageDigest": "352f8f95e3173a75a2f287e20aa909e8207d03e6d9fac50a5cef8aa14b28ef88", + "messageLength": 14 + }, + "outcome": { + "ok": true, + "value": { + "checkout": { + "repositoryName": "project", + "worktreeName": null, + "checkoutPath": "/private/tmp/xmd-retained-history/h2-push-pending/repositories/repositories/85e770fa023edae002ab459e9fcaf91b1e79b2addaf2d89ba6104ae3fe3e9f90/checkout" + }, + "messageSource": "prop", + "messageDigest": "352f8f95e3173a75a2f287e20aa909e8207d03e6d9fac50a5cef8aa14b28ef88", + "messageLength": 14, + "parent": "68dde90e63566966c4b3a51b254211f44894a21b", + "tree": "17b0bdfbe7c7c448d7214f8910598edd1321ea6f", + "commit": "3d7500e397cde7a3e3b2e54d61638583b399dd3d", + "committedAt": 1792337426, + "before": { + "branch": "review/pending", + "commit": "68dde90e63566966c4b3a51b254211f44894a21b", + "headTree": "15587c8d6ad8e073e7178240a8117246bdfba4eb", + "indexTree": "17b0bdfbe7c7c448d7214f8910598edd1321ea6f" + }, + "after": { + "branch": "review/pending", + "commit": "3d7500e397cde7a3e3b2e54d61638583b399dd3d", + "headTree": "17b0bdfbe7c7c448d7214f8910598edd1321ea6f", + "indexTree": "17b0bdfbe7c7c448d7214f8910598edd1321ea6f" + } + } + } + } + } + }, + { + "type": "yield", + "coroutineId": "root", + "description": { + "type": "import_component", + "name": "Git.Push", + "executablemd.source-position": { + "path": "/private/tmp/xmd-retained-history/h2-push-pending/entry.md", + "offset": 259, + "line": 6, + "column": 3 + } + }, + "result": { + "status": "ok", + "value": { + "kind": "registered", + "origin": "@executablemd/workflow/composition", + "reserved": false + } + } + }, + { + "type": "yield", + "coroutineId": "root", + "description": { + "type": "repl_repository_push_request", + "name": "8bbdd12933043ff35a6a73dd9998c5a207f129a534231bffd30235c726e2a4c7" + }, + "result": { + "status": "ok", + "value": { + "version": 1, + "owner": "[\"executablemd.repl.issue-owner\",1,\"h2\",\"1\"]", + "site": "1391dd54773007cbd258b6c673f38fa44536906472b34a69d064d438b9e23ea0", + "requested": { + "repository": { + "name": "project", + "locatorFingerprint": "3381fbae1def29f0c9e4f75d6ee98db4ddcf8329afe1a472fa826e89ab4c8db3", + "requestedBase": null, + "creationCommit": "68dde90e63566966c4b3a51b254211f44894a21b", + "primaryBranch": "main", + "objectFormat": "sha1" + }, + "selectionCheckout": "/private/tmp/xmd-retained-history/h2-push-pending/repositories/repositories/85e770fa023edae002ab459e9fcaf91b1e79b2addaf2d89ba6104ae3fe3e9f90/checkout", + "commonDirectory": "/private/tmp/xmd-retained-history/h2-push-pending/repositories/repositories/85e770fa023edae002ab459e9fcaf91b1e79b2addaf2d89ba6104ae3fe3e9f90/checkout/.git", + "workingDirectory": "/private/tmp/xmd-retained-history/h2-push-pending/repositories/repositories/85e770fa023edae002ab459e9fcaf91b1e79b2addaf2d89ba6104ae3fe3e9f90/checkout" + }, + "received": { + "repository": { + "name": "project", + "locatorFingerprint": "3381fbae1def29f0c9e4f75d6ee98db4ddcf8329afe1a472fa826e89ab4c8db3", + "requestedBase": null, + "creationCommit": "68dde90e63566966c4b3a51b254211f44894a21b", + "primaryBranch": "main", + "objectFormat": "sha1" + }, + "selectionCheckout": "/private/tmp/xmd-retained-history/h2-push-pending/repositories/repositories/85e770fa023edae002ab459e9fcaf91b1e79b2addaf2d89ba6104ae3fe3e9f90/checkout", + "commonDirectory": "/private/tmp/xmd-retained-history/h2-push-pending/repositories/repositories/85e770fa023edae002ab459e9fcaf91b1e79b2addaf2d89ba6104ae3fe3e9f90/checkout/.git", + "workingDirectory": "/private/tmp/xmd-retained-history/h2-push-pending/repositories/repositories/85e770fa023edae002ab459e9fcaf91b1e79b2addaf2d89ba6104ae3fe3e9f90/checkout" + }, + "resolved": { + "checkoutRoot": "/private/tmp/xmd-retained-history/h2-push-pending/repositories/repositories/85e770fa023edae002ab459e9fcaf91b1e79b2addaf2d89ba6104ae3fe3e9f90/checkout", + "commonDirectory": "/private/tmp/xmd-retained-history/h2-push-pending/repositories/repositories/85e770fa023edae002ab459e9fcaf91b1e79b2addaf2d89ba6104ae3fe3e9f90/checkout/.git", + "origin": "/private/tmp/xmd-retained-history/h2-push-pending/origin.git", + "inputs": { + "repository": { + "name": "project", + "locatorFingerprint": "3381fbae1def29f0c9e4f75d6ee98db4ddcf8329afe1a472fa826e89ab4c8db3", + "requestedBase": null, + "creationCommit": "68dde90e63566966c4b3a51b254211f44894a21b", + "primaryBranch": "main", + "objectFormat": "sha1" + }, + "remote": "origin", + "branch": "review/pending", + "destinationRef": "refs/heads/review/pending", + "sourceCommit": "3d7500e397cde7a3e3b2e54d61638583b399dd3d" + } + } + } + } + } +] diff --git a/packages/git/tests/fixtures/retained-repository-history/h2-push-pending/source.md b/packages/git/tests/fixtures/retained-repository-history/h2-push-pending/source.md new file mode 100644 index 000000000..b09e7eb23 --- /dev/null +++ b/packages/git/tests/fixtures/retained-repository-history/h2-push-pending/source.md @@ -0,0 +1,12 @@ + + + ready + + + + +The branch was published. Continue? + + continued + diff --git a/packages/git/tests/fixtures/retained-repository-history/h3-push-unacknowledged/journal.json b/packages/git/tests/fixtures/retained-repository-history/h3-push-unacknowledged/journal.json new file mode 100644 index 000000000..4ad56cb73 --- /dev/null +++ b/packages/git/tests/fixtures/retained-repository-history/h3-push-unacknowledged/journal.json @@ -0,0 +1,445 @@ +[ + { + "type": "yield", + "coroutineId": "root", + "description": { + "type": "import_component", + "name": "__root__" + }, + "result": { + "status": "ok", + "value": { + "kind": "repository", + "path": "/private/tmp/xmd-retained-history/h3-push-unacknowledged/entry.md", + "content": "\n \n ready\n \n \n \n \nThe branch was published. Continue?\n \n continued\n\n" + } + } + }, + { + "type": "yield", + "coroutineId": "root", + "description": { + "type": "import_component", + "name": "Repository", + "executablemd.source-position": { + "path": "/private/tmp/xmd-retained-history/h3-push-unacknowledged/entry.md", + "offset": 0, + "line": 1, + "column": 1 + } + }, + "result": { + "status": "ok", + "value": { + "kind": "registered", + "origin": "@executablemd/workflow/composition", + "reserved": false + } + } + }, + { + "type": "yield", + "coroutineId": "root", + "description": { + "type": "repl_repository_placement", + "name": "[\"executablemd.git.repl-repository\",\"placement\",1,\"repository\",\"b5b15bcdb981a05bedbef7b6f3b6cf2aa11be0aad3d140f7356225db9b7029b8\"]" + }, + "result": { + "status": "ok", + "value": { + "version": 1, + "kind": "repository", + "fingerprint": "b5b15bcdb981a05bedbef7b6f3b6cf2aa11be0aad3d140f7356225db9b7029b8", + "request": { + "name": "project", + "locatorFingerprint": "04891fd8d3054394dea246f089541b4482c804645175ffe315b47d6567f554df", + "base": null + }, + "placement": { + "managedRoot": "/private/tmp/xmd-retained-history/h3-push-unacknowledged/repositories", + "checkoutRoot": "/private/tmp/xmd-retained-history/h3-push-unacknowledged/repositories/repositories/d6a48dd563a972b779463a1dc6c62060aba1f1861af4ebbe6110d52658e2c3d9/checkout", + "commonDirectory": "/private/tmp/xmd-retained-history/h3-push-unacknowledged/repositories/repositories/d6a48dd563a972b779463a1dc6c62060aba1f1861af4ebbe6110d52658e2c3d9/checkout/.git", + "identity": { + "name": "project", + "locatorFingerprint": "04891fd8d3054394dea246f089541b4482c804645175ffe315b47d6567f554df", + "requestedBase": null, + "creationCommit": "c3556c9f8e2a8e7ec144abd7ab116282ae120839", + "primaryBranch": "main", + "objectFormat": "sha1" + }, + "displayName": "project", + "ownerCheckout": "/private/tmp/xmd-retained-history/h3-push-unacknowledged/repositories/repositories/d6a48dd563a972b779463a1dc6c62060aba1f1861af4ebbe6110d52658e2c3d9/checkout" + } + } + } + }, + { + "type": "yield", + "coroutineId": "root", + "description": { + "type": "import_component", + "name": "Git.Switch", + "executablemd.source-position": { + "path": "/private/tmp/xmd-retained-history/h3-push-unacknowledged/entry.md", + "offset": 104, + "line": 2, + "column": 3 + } + }, + "result": { + "status": "ok", + "value": { + "kind": "registered", + "origin": "@executablemd/workflow/composition", + "reserved": false + } + } + }, + { + "type": "yield", + "coroutineId": "root", + "description": { + "type": "repl_repository_git", + "name": "[\"executablemd.git.repl-repository\",\"git\",1,\"switch\",\"0172e6c6560ad5798d060239c5fd1f54b121dd052d5528d974a591735be66aeb\"]" + }, + "result": { + "status": "ok", + "value": { + "version": 1, + "operation": "switch", + "fingerprint": "0172e6c6560ad5798d060239c5fd1f54b121dd052d5528d974a591735be66aeb", + "request": { + "repository": { + "name": "project", + "locatorFingerprint": "04891fd8d3054394dea246f089541b4482c804645175ffe315b47d6567f554df", + "requestedBase": null, + "creationCommit": "c3556c9f8e2a8e7ec144abd7ab116282ae120839", + "primaryBranch": "main", + "objectFormat": "sha1" + }, + "selectionCheckout": "/private/tmp/xmd-retained-history/h3-push-unacknowledged/repositories/repositories/d6a48dd563a972b779463a1dc6c62060aba1f1861af4ebbe6110d52658e2c3d9/checkout", + "commonDirectory": "/private/tmp/xmd-retained-history/h3-push-unacknowledged/repositories/repositories/d6a48dd563a972b779463a1dc6c62060aba1f1861af4ebbe6110d52658e2c3d9/checkout/.git", + "workingDirectory": "/private/tmp/xmd-retained-history/h3-push-unacknowledged/repositories/repositories/d6a48dd563a972b779463a1dc6c62060aba1f1861af4ebbe6110d52658e2c3d9/checkout", + "branch": "review/adopt", + "base": null + }, + "outcome": { + "ok": true, + "value": { + "checkout": { + "repositoryName": "project", + "worktreeName": null, + "checkoutPath": "/private/tmp/xmd-retained-history/h3-push-unacknowledged/repositories/repositories/d6a48dd563a972b779463a1dc6c62060aba1f1861af4ebbe6110d52658e2c3d9/checkout" + }, + "requestedBranch": "review/adopt", + "resolvedBranch": "review/adopt", + "requestedBase": null, + "resolvedBase": "c3556c9f8e2a8e7ec144abd7ab116282ae120839", + "before": { + "branch": "main", + "commit": "c3556c9f8e2a8e7ec144abd7ab116282ae120839", + "headTree": "15587c8d6ad8e073e7178240a8117246bdfba4eb", + "indexTree": "15587c8d6ad8e073e7178240a8117246bdfba4eb" + }, + "after": { + "branch": "review/adopt", + "commit": "c3556c9f8e2a8e7ec144abd7ab116282ae120839", + "headTree": "15587c8d6ad8e073e7178240a8117246bdfba4eb", + "indexTree": "15587c8d6ad8e073e7178240a8117246bdfba4eb" + } + } + } + } + } + }, + { + "type": "yield", + "coroutineId": "root", + "description": { + "type": "import_component", + "name": "File", + "executablemd.source-position": { + "path": "/private/tmp/xmd-retained-history/h3-push-unacknowledged/entry.md", + "offset": 143, + "line": 3, + "column": 3 + } + }, + "result": { + "status": "ok", + "value": { + "kind": "protected" + } + } + }, + { + "type": "yield", + "coroutineId": "root", + "description": { + "type": "repl_repository_local", + "name": "[\"executablemd.git.repl-repository\",\"local\",1,\"file-write\",\"a1f719e4102a1347d6c29b2a17184fc9b6ac5e97564f2dae047944e7a052336e\"]" + }, + "result": { + "status": "ok", + "value": { + "version": 1, + "operation": "file-write", + "fingerprint": "a1f719e4102a1347d6c29b2a17184fc9b6ac5e97564f2dae047944e7a052336e", + "request": { + "cwd": "/private/tmp/xmd-retained-history/h3-push-unacknowledged/repositories/repositories/d6a48dd563a972b779463a1dc6c62060aba1f1861af4ebbe6110d52658e2c3d9/checkout", + "path": "notes.md", + "content": "ready" + }, + "success": { + "type": "executablemd.runtime.files-write-success/v1", + "publication": "host-committed" + } + } + } + }, + { + "type": "yield", + "coroutineId": "root", + "description": { + "type": "import_component", + "name": "Git.Add", + "executablemd.source-position": { + "path": "/private/tmp/xmd-retained-history/h3-push-unacknowledged/entry.md", + "offset": 180, + "line": 4, + "column": 3 + } + }, + "result": { + "status": "ok", + "value": { + "kind": "registered", + "origin": "@executablemd/workflow/composition", + "reserved": false + } + } + }, + { + "type": "yield", + "coroutineId": "root", + "description": { + "type": "repl_repository_git", + "name": "[\"executablemd.git.repl-repository\",\"git\",1,\"add\",\"d3a1f237217281bd2cb5bb47240769b8bdf536a92e2d6346feb2b7c3e154a0aa\"]" + }, + "result": { + "status": "ok", + "value": { + "version": 1, + "operation": "add", + "fingerprint": "d3a1f237217281bd2cb5bb47240769b8bdf536a92e2d6346feb2b7c3e154a0aa", + "request": { + "repository": { + "name": "project", + "locatorFingerprint": "04891fd8d3054394dea246f089541b4482c804645175ffe315b47d6567f554df", + "requestedBase": null, + "creationCommit": "c3556c9f8e2a8e7ec144abd7ab116282ae120839", + "primaryBranch": "main", + "objectFormat": "sha1" + }, + "selectionCheckout": "/private/tmp/xmd-retained-history/h3-push-unacknowledged/repositories/repositories/d6a48dd563a972b779463a1dc6c62060aba1f1861af4ebbe6110d52658e2c3d9/checkout", + "commonDirectory": "/private/tmp/xmd-retained-history/h3-push-unacknowledged/repositories/repositories/d6a48dd563a972b779463a1dc6c62060aba1f1861af4ebbe6110d52658e2c3d9/checkout/.git", + "workingDirectory": "/private/tmp/xmd-retained-history/h3-push-unacknowledged/repositories/repositories/d6a48dd563a972b779463a1dc6c62060aba1f1861af4ebbe6110d52658e2c3d9/checkout", + "paths": [ + "notes.md" + ] + }, + "outcome": { + "ok": true, + "value": { + "checkout": { + "repositoryName": "project", + "worktreeName": null, + "checkoutPath": "/private/tmp/xmd-retained-history/h3-push-unacknowledged/repositories/repositories/d6a48dd563a972b779463a1dc6c62060aba1f1861af4ebbe6110d52658e2c3d9/checkout" + }, + "paths": [ + "notes.md" + ], + "before": { + "branch": "review/adopt", + "commit": "c3556c9f8e2a8e7ec144abd7ab116282ae120839", + "headTree": "15587c8d6ad8e073e7178240a8117246bdfba4eb", + "indexTree": "15587c8d6ad8e073e7178240a8117246bdfba4eb" + }, + "after": { + "branch": "review/adopt", + "commit": "c3556c9f8e2a8e7ec144abd7ab116282ae120839", + "headTree": "15587c8d6ad8e073e7178240a8117246bdfba4eb", + "indexTree": "17b0bdfbe7c7c448d7214f8910598edd1321ea6f" + } + } + } + } + } + }, + { + "type": "yield", + "coroutineId": "root", + "description": { + "type": "import_component", + "name": "Git.Commit", + "executablemd.source-position": { + "path": "/private/tmp/xmd-retained-history/h3-push-unacknowledged/entry.md", + "offset": 211, + "line": 5, + "column": 3 + } + }, + "result": { + "status": "ok", + "value": { + "kind": "registered", + "origin": "@executablemd/workflow/composition", + "reserved": false + } + } + }, + { + "type": "yield", + "coroutineId": "root", + "description": { + "type": "repl_repository_git", + "name": "[\"executablemd.git.repl-repository\",\"git\",1,\"commit\",\"c34fb246cdb3336e115db3e438d6e6a3f2b685c36a7e24f5e72dfb22089e2a3e\"]" + }, + "result": { + "status": "ok", + "value": { + "version": 1, + "operation": "commit", + "fingerprint": "c34fb246cdb3336e115db3e438d6e6a3f2b685c36a7e24f5e72dfb22089e2a3e", + "request": { + "repository": { + "name": "project", + "locatorFingerprint": "04891fd8d3054394dea246f089541b4482c804645175ffe315b47d6567f554df", + "requestedBase": null, + "creationCommit": "c3556c9f8e2a8e7ec144abd7ab116282ae120839", + "primaryBranch": "main", + "objectFormat": "sha1" + }, + "selectionCheckout": "/private/tmp/xmd-retained-history/h3-push-unacknowledged/repositories/repositories/d6a48dd563a972b779463a1dc6c62060aba1f1861af4ebbe6110d52658e2c3d9/checkout", + "commonDirectory": "/private/tmp/xmd-retained-history/h3-push-unacknowledged/repositories/repositories/d6a48dd563a972b779463a1dc6c62060aba1f1861af4ebbe6110d52658e2c3d9/checkout/.git", + "workingDirectory": "/private/tmp/xmd-retained-history/h3-push-unacknowledged/repositories/repositories/d6a48dd563a972b779463a1dc6c62060aba1f1861af4ebbe6110d52658e2c3d9/checkout", + "messageSource": "prop", + "messageDigest": "352f8f95e3173a75a2f287e20aa909e8207d03e6d9fac50a5cef8aa14b28ef88", + "messageLength": 14 + }, + "outcome": { + "ok": true, + "value": { + "checkout": { + "repositoryName": "project", + "worktreeName": null, + "checkoutPath": "/private/tmp/xmd-retained-history/h3-push-unacknowledged/repositories/repositories/d6a48dd563a972b779463a1dc6c62060aba1f1861af4ebbe6110d52658e2c3d9/checkout" + }, + "messageSource": "prop", + "messageDigest": "352f8f95e3173a75a2f287e20aa909e8207d03e6d9fac50a5cef8aa14b28ef88", + "messageLength": 14, + "parent": "c3556c9f8e2a8e7ec144abd7ab116282ae120839", + "tree": "17b0bdfbe7c7c448d7214f8910598edd1321ea6f", + "commit": "ae88e480853fe1ab3e600c085c0ecf2519f1fa0d", + "committedAt": 1792337427, + "before": { + "branch": "review/adopt", + "commit": "c3556c9f8e2a8e7ec144abd7ab116282ae120839", + "headTree": "15587c8d6ad8e073e7178240a8117246bdfba4eb", + "indexTree": "17b0bdfbe7c7c448d7214f8910598edd1321ea6f" + }, + "after": { + "branch": "review/adopt", + "commit": "ae88e480853fe1ab3e600c085c0ecf2519f1fa0d", + "headTree": "17b0bdfbe7c7c448d7214f8910598edd1321ea6f", + "indexTree": "17b0bdfbe7c7c448d7214f8910598edd1321ea6f" + } + } + } + } + } + }, + { + "type": "yield", + "coroutineId": "root", + "description": { + "type": "import_component", + "name": "Git.Push", + "executablemd.source-position": { + "path": "/private/tmp/xmd-retained-history/h3-push-unacknowledged/entry.md", + "offset": 264, + "line": 6, + "column": 3 + } + }, + "result": { + "status": "ok", + "value": { + "kind": "registered", + "origin": "@executablemd/workflow/composition", + "reserved": false + } + } + }, + { + "type": "yield", + "coroutineId": "root", + "description": { + "type": "repl_repository_push_request", + "name": "22cd3e9f6ef80e9f7db277de8a67c39c1655bc4f601ea41dc953038394ac7b17" + }, + "result": { + "status": "ok", + "value": { + "version": 1, + "owner": "[\"executablemd.repl.issue-owner\",1,\"h3\",\"1\"]", + "site": "97a2ec6c878610f58a0b2262d6f57e80213251ef590379f2de89ac0cb6ceadfc", + "requested": { + "repository": { + "name": "project", + "locatorFingerprint": "04891fd8d3054394dea246f089541b4482c804645175ffe315b47d6567f554df", + "requestedBase": null, + "creationCommit": "c3556c9f8e2a8e7ec144abd7ab116282ae120839", + "primaryBranch": "main", + "objectFormat": "sha1" + }, + "selectionCheckout": "/private/tmp/xmd-retained-history/h3-push-unacknowledged/repositories/repositories/d6a48dd563a972b779463a1dc6c62060aba1f1861af4ebbe6110d52658e2c3d9/checkout", + "commonDirectory": "/private/tmp/xmd-retained-history/h3-push-unacknowledged/repositories/repositories/d6a48dd563a972b779463a1dc6c62060aba1f1861af4ebbe6110d52658e2c3d9/checkout/.git", + "workingDirectory": "/private/tmp/xmd-retained-history/h3-push-unacknowledged/repositories/repositories/d6a48dd563a972b779463a1dc6c62060aba1f1861af4ebbe6110d52658e2c3d9/checkout" + }, + "received": { + "repository": { + "name": "project", + "locatorFingerprint": "04891fd8d3054394dea246f089541b4482c804645175ffe315b47d6567f554df", + "requestedBase": null, + "creationCommit": "c3556c9f8e2a8e7ec144abd7ab116282ae120839", + "primaryBranch": "main", + "objectFormat": "sha1" + }, + "selectionCheckout": "/private/tmp/xmd-retained-history/h3-push-unacknowledged/repositories/repositories/d6a48dd563a972b779463a1dc6c62060aba1f1861af4ebbe6110d52658e2c3d9/checkout", + "commonDirectory": "/private/tmp/xmd-retained-history/h3-push-unacknowledged/repositories/repositories/d6a48dd563a972b779463a1dc6c62060aba1f1861af4ebbe6110d52658e2c3d9/checkout/.git", + "workingDirectory": "/private/tmp/xmd-retained-history/h3-push-unacknowledged/repositories/repositories/d6a48dd563a972b779463a1dc6c62060aba1f1861af4ebbe6110d52658e2c3d9/checkout" + }, + "resolved": { + "checkoutRoot": "/private/tmp/xmd-retained-history/h3-push-unacknowledged/repositories/repositories/d6a48dd563a972b779463a1dc6c62060aba1f1861af4ebbe6110d52658e2c3d9/checkout", + "commonDirectory": "/private/tmp/xmd-retained-history/h3-push-unacknowledged/repositories/repositories/d6a48dd563a972b779463a1dc6c62060aba1f1861af4ebbe6110d52658e2c3d9/checkout/.git", + "origin": "/private/tmp/xmd-retained-history/h3-push-unacknowledged/origin.git", + "inputs": { + "repository": { + "name": "project", + "locatorFingerprint": "04891fd8d3054394dea246f089541b4482c804645175ffe315b47d6567f554df", + "requestedBase": null, + "creationCommit": "c3556c9f8e2a8e7ec144abd7ab116282ae120839", + "primaryBranch": "main", + "objectFormat": "sha1" + }, + "remote": "origin", + "branch": "review/adopt", + "destinationRef": "refs/heads/review/adopt", + "sourceCommit": "ae88e480853fe1ab3e600c085c0ecf2519f1fa0d" + } + } + } + } + } +] diff --git a/packages/git/tests/fixtures/retained-repository-history/h3-push-unacknowledged/source.md b/packages/git/tests/fixtures/retained-repository-history/h3-push-unacknowledged/source.md new file mode 100644 index 000000000..7947e362c --- /dev/null +++ b/packages/git/tests/fixtures/retained-repository-history/h3-push-unacknowledged/source.md @@ -0,0 +1,12 @@ + + + ready + + + + +The branch was published. Continue? + + continued + diff --git a/packages/git/tests/fixtures/retained-repository-history/h4-push-failed/journal.json b/packages/git/tests/fixtures/retained-repository-history/h4-push-failed/journal.json new file mode 100644 index 000000000..4c27852cd --- /dev/null +++ b/packages/git/tests/fixtures/retained-repository-history/h4-push-failed/journal.json @@ -0,0 +1,467 @@ +[ + { + "type": "yield", + "coroutineId": "root", + "description": { + "type": "import_component", + "name": "__root__" + }, + "result": { + "status": "ok", + "value": { + "kind": "repository", + "path": "/private/tmp/xmd-retained-history/h4-push-failed/entry.md", + "content": "\n \n ready\n \n \n \n \nThe branch was published. Continue?\n \n continued\n\n" + } + } + }, + { + "type": "yield", + "coroutineId": "root", + "description": { + "type": "import_component", + "name": "Repository", + "executablemd.source-position": { + "path": "/private/tmp/xmd-retained-history/h4-push-failed/entry.md", + "offset": 0, + "line": 1, + "column": 1 + } + }, + "result": { + "status": "ok", + "value": { + "kind": "registered", + "origin": "@executablemd/workflow/composition", + "reserved": false + } + } + }, + { + "type": "yield", + "coroutineId": "root", + "description": { + "type": "repl_repository_placement", + "name": "[\"executablemd.git.repl-repository\",\"placement\",1,\"repository\",\"bdc7b9d0a31b6eeb807528dcedd423fe46fe173eefa8bf545b59474ff10a232a\"]" + }, + "result": { + "status": "ok", + "value": { + "version": 1, + "kind": "repository", + "fingerprint": "bdc7b9d0a31b6eeb807528dcedd423fe46fe173eefa8bf545b59474ff10a232a", + "request": { + "name": "project", + "locatorFingerprint": "63795a9624d8c13b2b3c59f9603c65e945ef841d0a239704d052388372d9c9d9", + "base": null + }, + "placement": { + "managedRoot": "/private/tmp/xmd-retained-history/h4-push-failed/repositories", + "checkoutRoot": "/private/tmp/xmd-retained-history/h4-push-failed/repositories/repositories/efe1211d5a599a3dae1e51da703e2a8e3903ba4c77e3a093913b30bca3cc1033/checkout", + "commonDirectory": "/private/tmp/xmd-retained-history/h4-push-failed/repositories/repositories/efe1211d5a599a3dae1e51da703e2a8e3903ba4c77e3a093913b30bca3cc1033/checkout/.git", + "identity": { + "name": "project", + "locatorFingerprint": "63795a9624d8c13b2b3c59f9603c65e945ef841d0a239704d052388372d9c9d9", + "requestedBase": null, + "creationCommit": "21e1b841c8c804e61b2a1f3f931c486d5d765eb9", + "primaryBranch": "main", + "objectFormat": "sha1" + }, + "displayName": "project", + "ownerCheckout": "/private/tmp/xmd-retained-history/h4-push-failed/repositories/repositories/efe1211d5a599a3dae1e51da703e2a8e3903ba4c77e3a093913b30bca3cc1033/checkout" + } + } + } + }, + { + "type": "yield", + "coroutineId": "root", + "description": { + "type": "import_component", + "name": "Git.Switch", + "executablemd.source-position": { + "path": "/private/tmp/xmd-retained-history/h4-push-failed/entry.md", + "offset": 96, + "line": 2, + "column": 3 + } + }, + "result": { + "status": "ok", + "value": { + "kind": "registered", + "origin": "@executablemd/workflow/composition", + "reserved": false + } + } + }, + { + "type": "yield", + "coroutineId": "root", + "description": { + "type": "repl_repository_git", + "name": "[\"executablemd.git.repl-repository\",\"git\",1,\"switch\",\"18c51820273363169dec3febab2f7dc4218c098ee2a16fd68e1cc15f8efb0100\"]" + }, + "result": { + "status": "ok", + "value": { + "version": 1, + "operation": "switch", + "fingerprint": "18c51820273363169dec3febab2f7dc4218c098ee2a16fd68e1cc15f8efb0100", + "request": { + "repository": { + "name": "project", + "locatorFingerprint": "63795a9624d8c13b2b3c59f9603c65e945ef841d0a239704d052388372d9c9d9", + "requestedBase": null, + "creationCommit": "21e1b841c8c804e61b2a1f3f931c486d5d765eb9", + "primaryBranch": "main", + "objectFormat": "sha1" + }, + "selectionCheckout": "/private/tmp/xmd-retained-history/h4-push-failed/repositories/repositories/efe1211d5a599a3dae1e51da703e2a8e3903ba4c77e3a093913b30bca3cc1033/checkout", + "commonDirectory": "/private/tmp/xmd-retained-history/h4-push-failed/repositories/repositories/efe1211d5a599a3dae1e51da703e2a8e3903ba4c77e3a093913b30bca3cc1033/checkout/.git", + "workingDirectory": "/private/tmp/xmd-retained-history/h4-push-failed/repositories/repositories/efe1211d5a599a3dae1e51da703e2a8e3903ba4c77e3a093913b30bca3cc1033/checkout", + "branch": "review/divergent", + "base": null + }, + "outcome": { + "ok": true, + "value": { + "checkout": { + "repositoryName": "project", + "worktreeName": null, + "checkoutPath": "/private/tmp/xmd-retained-history/h4-push-failed/repositories/repositories/efe1211d5a599a3dae1e51da703e2a8e3903ba4c77e3a093913b30bca3cc1033/checkout" + }, + "requestedBranch": "review/divergent", + "resolvedBranch": "review/divergent", + "requestedBase": null, + "resolvedBase": null, + "before": { + "branch": "review/divergent", + "commit": "d0868e870b9b02aae353c4a5a59ffd905c405787", + "headTree": "7a866ef129320f8135d0914db9cd3e55c7f60951", + "indexTree": "7a866ef129320f8135d0914db9cd3e55c7f60951" + }, + "after": { + "branch": "review/divergent", + "commit": "d0868e870b9b02aae353c4a5a59ffd905c405787", + "headTree": "7a866ef129320f8135d0914db9cd3e55c7f60951", + "indexTree": "7a866ef129320f8135d0914db9cd3e55c7f60951" + } + } + } + } + } + }, + { + "type": "yield", + "coroutineId": "root", + "description": { + "type": "import_component", + "name": "File", + "executablemd.source-position": { + "path": "/private/tmp/xmd-retained-history/h4-push-failed/entry.md", + "offset": 139, + "line": 3, + "column": 3 + } + }, + "result": { + "status": "ok", + "value": { + "kind": "protected" + } + } + }, + { + "type": "yield", + "coroutineId": "root", + "description": { + "type": "repl_repository_local", + "name": "[\"executablemd.git.repl-repository\",\"local\",1,\"file-write\",\"2db6f78cb8ea8583beb61c55312a6fd3f90b2b3a7336fbc780d495039dd8646f\"]" + }, + "result": { + "status": "ok", + "value": { + "version": 1, + "operation": "file-write", + "fingerprint": "2db6f78cb8ea8583beb61c55312a6fd3f90b2b3a7336fbc780d495039dd8646f", + "request": { + "cwd": "/private/tmp/xmd-retained-history/h4-push-failed/repositories/repositories/efe1211d5a599a3dae1e51da703e2a8e3903ba4c77e3a093913b30bca3cc1033/checkout", + "path": "notes.md", + "content": "ready" + }, + "success": { + "type": "executablemd.runtime.files-write-success/v1", + "publication": "host-committed" + } + } + } + }, + { + "type": "yield", + "coroutineId": "root", + "description": { + "type": "import_component", + "name": "Git.Add", + "executablemd.source-position": { + "path": "/private/tmp/xmd-retained-history/h4-push-failed/entry.md", + "offset": 176, + "line": 4, + "column": 3 + } + }, + "result": { + "status": "ok", + "value": { + "kind": "registered", + "origin": "@executablemd/workflow/composition", + "reserved": false + } + } + }, + { + "type": "yield", + "coroutineId": "root", + "description": { + "type": "repl_repository_git", + "name": "[\"executablemd.git.repl-repository\",\"git\",1,\"add\",\"ad0ffd1b71b214c11911ff7d1bd7c8a59b6c354fa0e7dfa26faad954ac66d8fa\"]" + }, + "result": { + "status": "ok", + "value": { + "version": 1, + "operation": "add", + "fingerprint": "ad0ffd1b71b214c11911ff7d1bd7c8a59b6c354fa0e7dfa26faad954ac66d8fa", + "request": { + "repository": { + "name": "project", + "locatorFingerprint": "63795a9624d8c13b2b3c59f9603c65e945ef841d0a239704d052388372d9c9d9", + "requestedBase": null, + "creationCommit": "21e1b841c8c804e61b2a1f3f931c486d5d765eb9", + "primaryBranch": "main", + "objectFormat": "sha1" + }, + "selectionCheckout": "/private/tmp/xmd-retained-history/h4-push-failed/repositories/repositories/efe1211d5a599a3dae1e51da703e2a8e3903ba4c77e3a093913b30bca3cc1033/checkout", + "commonDirectory": "/private/tmp/xmd-retained-history/h4-push-failed/repositories/repositories/efe1211d5a599a3dae1e51da703e2a8e3903ba4c77e3a093913b30bca3cc1033/checkout/.git", + "workingDirectory": "/private/tmp/xmd-retained-history/h4-push-failed/repositories/repositories/efe1211d5a599a3dae1e51da703e2a8e3903ba4c77e3a093913b30bca3cc1033/checkout", + "paths": [ + "notes.md" + ] + }, + "outcome": { + "ok": true, + "value": { + "checkout": { + "repositoryName": "project", + "worktreeName": null, + "checkoutPath": "/private/tmp/xmd-retained-history/h4-push-failed/repositories/repositories/efe1211d5a599a3dae1e51da703e2a8e3903ba4c77e3a093913b30bca3cc1033/checkout" + }, + "paths": [ + "notes.md" + ], + "before": { + "branch": "review/divergent", + "commit": "d0868e870b9b02aae353c4a5a59ffd905c405787", + "headTree": "7a866ef129320f8135d0914db9cd3e55c7f60951", + "indexTree": "7a866ef129320f8135d0914db9cd3e55c7f60951" + }, + "after": { + "branch": "review/divergent", + "commit": "d0868e870b9b02aae353c4a5a59ffd905c405787", + "headTree": "7a866ef129320f8135d0914db9cd3e55c7f60951", + "indexTree": "5554eaaccabd1fc76ed5b370a80034cfb1db92d6" + } + } + } + } + } + }, + { + "type": "yield", + "coroutineId": "root", + "description": { + "type": "import_component", + "name": "Git.Commit", + "executablemd.source-position": { + "path": "/private/tmp/xmd-retained-history/h4-push-failed/entry.md", + "offset": 207, + "line": 5, + "column": 3 + } + }, + "result": { + "status": "ok", + "value": { + "kind": "registered", + "origin": "@executablemd/workflow/composition", + "reserved": false + } + } + }, + { + "type": "yield", + "coroutineId": "root", + "description": { + "type": "repl_repository_git", + "name": "[\"executablemd.git.repl-repository\",\"git\",1,\"commit\",\"692e8e11c03f9738badd26c07629f8614f5105101d9c1289e89dde22fcd75378\"]" + }, + "result": { + "status": "ok", + "value": { + "version": 1, + "operation": "commit", + "fingerprint": "692e8e11c03f9738badd26c07629f8614f5105101d9c1289e89dde22fcd75378", + "request": { + "repository": { + "name": "project", + "locatorFingerprint": "63795a9624d8c13b2b3c59f9603c65e945ef841d0a239704d052388372d9c9d9", + "requestedBase": null, + "creationCommit": "21e1b841c8c804e61b2a1f3f931c486d5d765eb9", + "primaryBranch": "main", + "objectFormat": "sha1" + }, + "selectionCheckout": "/private/tmp/xmd-retained-history/h4-push-failed/repositories/repositories/efe1211d5a599a3dae1e51da703e2a8e3903ba4c77e3a093913b30bca3cc1033/checkout", + "commonDirectory": "/private/tmp/xmd-retained-history/h4-push-failed/repositories/repositories/efe1211d5a599a3dae1e51da703e2a8e3903ba4c77e3a093913b30bca3cc1033/checkout/.git", + "workingDirectory": "/private/tmp/xmd-retained-history/h4-push-failed/repositories/repositories/efe1211d5a599a3dae1e51da703e2a8e3903ba4c77e3a093913b30bca3cc1033/checkout", + "messageSource": "prop", + "messageDigest": "352f8f95e3173a75a2f287e20aa909e8207d03e6d9fac50a5cef8aa14b28ef88", + "messageLength": 14 + }, + "outcome": { + "ok": true, + "value": { + "checkout": { + "repositoryName": "project", + "worktreeName": null, + "checkoutPath": "/private/tmp/xmd-retained-history/h4-push-failed/repositories/repositories/efe1211d5a599a3dae1e51da703e2a8e3903ba4c77e3a093913b30bca3cc1033/checkout" + }, + "messageSource": "prop", + "messageDigest": "352f8f95e3173a75a2f287e20aa909e8207d03e6d9fac50a5cef8aa14b28ef88", + "messageLength": 14, + "parent": "d0868e870b9b02aae353c4a5a59ffd905c405787", + "tree": "5554eaaccabd1fc76ed5b370a80034cfb1db92d6", + "commit": "e87288a89f54e0e7927649039656af7918e20a2e", + "committedAt": 1792337430, + "before": { + "branch": "review/divergent", + "commit": "d0868e870b9b02aae353c4a5a59ffd905c405787", + "headTree": "7a866ef129320f8135d0914db9cd3e55c7f60951", + "indexTree": "5554eaaccabd1fc76ed5b370a80034cfb1db92d6" + }, + "after": { + "branch": "review/divergent", + "commit": "e87288a89f54e0e7927649039656af7918e20a2e", + "headTree": "5554eaaccabd1fc76ed5b370a80034cfb1db92d6", + "indexTree": "5554eaaccabd1fc76ed5b370a80034cfb1db92d6" + } + } + } + } + } + }, + { + "type": "yield", + "coroutineId": "root", + "description": { + "type": "import_component", + "name": "Git.Push", + "executablemd.source-position": { + "path": "/private/tmp/xmd-retained-history/h4-push-failed/entry.md", + "offset": 260, + "line": 6, + "column": 3 + } + }, + "result": { + "status": "ok", + "value": { + "kind": "registered", + "origin": "@executablemd/workflow/composition", + "reserved": false + } + } + }, + { + "type": "yield", + "coroutineId": "root", + "description": { + "type": "repl_repository_push_request", + "name": "5494bb45fc35876d97584c0582dae9bf7d6789fb0edeea3991d094e04b55174d" + }, + "result": { + "status": "ok", + "value": { + "version": 1, + "owner": "[\"executablemd.repl.issue-owner\",1,\"h4\",\"1\"]", + "site": "5f1ae2248c6f42f5e28db560ea25100ae707a4ee9dca21af22cd0213e2a8d811", + "requested": { + "repository": { + "name": "project", + "locatorFingerprint": "63795a9624d8c13b2b3c59f9603c65e945ef841d0a239704d052388372d9c9d9", + "requestedBase": null, + "creationCommit": "21e1b841c8c804e61b2a1f3f931c486d5d765eb9", + "primaryBranch": "main", + "objectFormat": "sha1" + }, + "selectionCheckout": "/private/tmp/xmd-retained-history/h4-push-failed/repositories/repositories/efe1211d5a599a3dae1e51da703e2a8e3903ba4c77e3a093913b30bca3cc1033/checkout", + "commonDirectory": "/private/tmp/xmd-retained-history/h4-push-failed/repositories/repositories/efe1211d5a599a3dae1e51da703e2a8e3903ba4c77e3a093913b30bca3cc1033/checkout/.git", + "workingDirectory": "/private/tmp/xmd-retained-history/h4-push-failed/repositories/repositories/efe1211d5a599a3dae1e51da703e2a8e3903ba4c77e3a093913b30bca3cc1033/checkout" + }, + "received": { + "repository": { + "name": "project", + "locatorFingerprint": "63795a9624d8c13b2b3c59f9603c65e945ef841d0a239704d052388372d9c9d9", + "requestedBase": null, + "creationCommit": "21e1b841c8c804e61b2a1f3f931c486d5d765eb9", + "primaryBranch": "main", + "objectFormat": "sha1" + }, + "selectionCheckout": "/private/tmp/xmd-retained-history/h4-push-failed/repositories/repositories/efe1211d5a599a3dae1e51da703e2a8e3903ba4c77e3a093913b30bca3cc1033/checkout", + "commonDirectory": "/private/tmp/xmd-retained-history/h4-push-failed/repositories/repositories/efe1211d5a599a3dae1e51da703e2a8e3903ba4c77e3a093913b30bca3cc1033/checkout/.git", + "workingDirectory": "/private/tmp/xmd-retained-history/h4-push-failed/repositories/repositories/efe1211d5a599a3dae1e51da703e2a8e3903ba4c77e3a093913b30bca3cc1033/checkout" + }, + "resolved": { + "checkoutRoot": "/private/tmp/xmd-retained-history/h4-push-failed/repositories/repositories/efe1211d5a599a3dae1e51da703e2a8e3903ba4c77e3a093913b30bca3cc1033/checkout", + "commonDirectory": "/private/tmp/xmd-retained-history/h4-push-failed/repositories/repositories/efe1211d5a599a3dae1e51da703e2a8e3903ba4c77e3a093913b30bca3cc1033/checkout/.git", + "origin": "/private/tmp/xmd-retained-history/h4-push-failed/origin.git", + "inputs": { + "repository": { + "name": "project", + "locatorFingerprint": "63795a9624d8c13b2b3c59f9603c65e945ef841d0a239704d052388372d9c9d9", + "requestedBase": null, + "creationCommit": "21e1b841c8c804e61b2a1f3f931c486d5d765eb9", + "primaryBranch": "main", + "objectFormat": "sha1" + }, + "remote": "origin", + "branch": "review/divergent", + "destinationRef": "refs/heads/review/divergent", + "sourceCommit": "e87288a89f54e0e7927649039656af7918e20a2e" + } + } + } + } + }, + { + "type": "yield", + "coroutineId": "root", + "description": { + "type": "git_host_effect", + "name": "4686933e0a1055e6a444d7985549d8a4b4d58d16cd41cde9253e5f73d01da9b1", + "executablemd.source-position": { + "path": "/private/tmp/xmd-retained-history/h4-push-failed/entry.md", + "offset": 260, + "line": 6, + "column": 3 + } + }, + "result": { + "status": "err", + "error": { + "message": "the Git host already holds state this effect conflicts with. Nothing was performed: conflicting external state is diagnosed rather than overwritten or duplicated.", + "name": "GitHostConflictError", + "stack": "GitHostConflictError: the Git host already holds state this effect conflicts with. Nothing was performed: conflicting external state is diagnosed rather than overwritten or duplicated.\n at reconcile (file:///Users/tarasmankovski/Repositories/taras/executable.md/.claude/worktrees/issue-181-boundary-a/packages/git/src/git-host/effect.ts:429:30)\n at reconcile.next ()\n at validated (file:///Users/tarasmankovski/Repositories/taras/executable.md/.claude/worktrees/issue-181-boundary-a/packages/git/src/git-host/effect.ts:826:27)\n at validated.next ()\n at file:///Users/tarasmankovski/Repositories/taras/executable.md/.claude/worktrees/issue-181-boundary-a/packages/git/src/git-host/effect.ts:184:5\n at Generator.next ()\n at trap (file:///Users/tarasmankovski/Repositories/taras/executable.md/.claude/worktrees/issue-181-boundary-a/node_modules/.deno/effection@4.1.0/node_modules/effection/esm/lib/trap.js:13:28)\n at trap.next ()\n at Object.scoped (file:///Users/tarasmankovski/Repositories/taras/executable.md/.claude/worktrees/issue-181-boundary-a/node_modules/.deno/effection@4.1.0/node_modules/effection/esm/lib/scoped.js:38:44)\n at scoped.next ()" + } + } + } +] diff --git a/packages/git/tests/fixtures/retained-repository-history/h4-push-failed/source.md b/packages/git/tests/fixtures/retained-repository-history/h4-push-failed/source.md new file mode 100644 index 000000000..cc8719ee9 --- /dev/null +++ b/packages/git/tests/fixtures/retained-repository-history/h4-push-failed/source.md @@ -0,0 +1,12 @@ + + + ready + + + + +The branch was published. Continue? + + continued + diff --git a/packages/git/tests/fixtures/retained-repository-history/h5-issue-completed/journal.json b/packages/git/tests/fixtures/retained-repository-history/h5-issue-completed/journal.json new file mode 100644 index 000000000..f476c7df2 --- /dev/null +++ b/packages/git/tests/fixtures/retained-repository-history/h5-issue-completed/journal.json @@ -0,0 +1,177 @@ +[ + { + "type": "yield", + "coroutineId": "root", + "description": { + "type": "import_component", + "name": "__root__" + }, + "result": { + "status": "ok", + "value": { + "kind": "repository", + "path": "/private/tmp/xmd-retained-history/h5-issue-completed/entry.md", + "content": "\na described issue\n\n\n\nContinue?\n\ncontinued\n" + } + } + }, + { + "type": "yield", + "coroutineId": "root", + "description": { + "type": "import_component", + "name": "IssueTracker", + "executablemd.source-position": { + "path": "/private/tmp/xmd-retained-history/h5-issue-completed/entry.md", + "offset": 0, + "line": 1, + "column": 1 + } + }, + "result": { + "status": "ok", + "value": { + "kind": "registered", + "origin": "@executablemd/workflow/composition", + "reserved": false + } + } + }, + { + "type": "yield", + "coroutineId": "root", + "description": { + "type": "import_component", + "name": "Issue", + "executablemd.source-position": { + "path": "/private/tmp/xmd-retained-history/h5-issue-completed/entry.md", + "offset": 53, + "line": 2, + "column": 1 + } + }, + "result": { + "status": "ok", + "value": { + "kind": "registered", + "origin": "@executablemd/workflow/composition", + "reserved": false + } + } + }, + { + "type": "yield", + "coroutineId": "root", + "description": { + "type": "issue_effect", + "name": "98d2a35d8f3f04011ad86bcdff746499d26ab5f7be99e67b32cc5829ae59d23c", + "request": { + "operation": "upsert", + "identity": { + "runId": "[\"executablemd.repl.issue-owner\",1,\"h5\",\"1\"]", + "expansionId": "4e08c2338224512e2ad865715d1b98d7af2000a0f5c40ec1b730c8eabff4dd18" + }, + "target": "https://github.com/octo/project", + "provider": null, + "issue": { + "title": "Filed", + "description": "a described issue", + "tags": [], + "assignee": null + } + }, + "executablemd.source-position": { + "path": "/private/tmp/xmd-retained-history/h5-issue-completed/entry.md", + "offset": 53, + "line": 2, + "column": 1 + } + }, + "result": { + "status": "ok", + "value": { + "url": "https://github.com/octo/project/issues/1" + } + } + }, + { + "type": "yield", + "coroutineId": "root", + "description": { + "type": "import_component", + "name": "Issue", + "executablemd.source-position": { + "path": "/private/tmp/xmd-retained-history/h5-issue-completed/entry.md", + "offset": 111, + "line": 3, + "column": 1 + } + }, + "result": { + "status": "ok", + "value": { + "kind": "registered", + "origin": "@executablemd/workflow/composition", + "reserved": false + } + } + }, + { + "type": "yield", + "coroutineId": "root", + "description": { + "type": "issue_effect", + "name": "19d1eeb0fb33b427db6f0dbb7bc9d2ebc51dd4391c09f94b3500526deeb6f2af", + "request": { + "operation": "read", + "identity": { + "runId": "[\"executablemd.repl.issue-owner\",1,\"h5\",\"1\"]", + "expansionId": "8239678a5e2945ecb9c53ff66eed177aeb4232d66d1d3ca18c47d3816f790957" + }, + "url": "https://github.com/octo/project/issues/1", + "provider": null + }, + "executablemd.source-position": { + "path": "/private/tmp/xmd-retained-history/h5-issue-completed/entry.md", + "offset": 111, + "line": 3, + "column": 1 + } + }, + "result": { + "status": "ok", + "value": { + "url": "https://github.com/octo/project/issues/1", + "title": "What the middleware answered", + "description": "answered without reaching a tracker", + "tags": [ + "alpha", + "beta" + ], + "assignee": null + } + } + }, + { + "type": "yield", + "coroutineId": "root", + "description": { + "type": "import_component", + "name": "Elicit", + "executablemd.source-position": { + "path": "/private/tmp/xmd-retained-history/h5-issue-completed/entry.md", + "offset": 195, + "line": 5, + "column": 1 + } + }, + "result": { + "status": "ok", + "value": { + "kind": "registered", + "origin": "@executablemd/core", + "reserved": false + } + } + } +] diff --git a/packages/git/tests/fixtures/retained-repository-history/h5-issue-completed/source.md b/packages/git/tests/fixtures/retained-repository-history/h5-issue-completed/source.md new file mode 100644 index 000000000..59d0bd732 --- /dev/null +++ b/packages/git/tests/fixtures/retained-repository-history/h5-issue-completed/source.md @@ -0,0 +1,9 @@ + +a described issue + + + +Continue? + +continued diff --git a/packages/git/tests/fixtures/retained-repository-history/h6-issue-unacknowledged/journal.json b/packages/git/tests/fixtures/retained-repository-history/h6-issue-unacknowledged/journal.json new file mode 100644 index 000000000..fbde0de73 --- /dev/null +++ b/packages/git/tests/fixtures/retained-repository-history/h6-issue-unacknowledged/journal.json @@ -0,0 +1,62 @@ +[ + { + "type": "yield", + "coroutineId": "root", + "description": { + "type": "import_component", + "name": "__root__" + }, + "result": { + "status": "ok", + "value": { + "kind": "repository", + "path": "/private/tmp/xmd-retained-history/h6-issue-unacknowledged/entry.md", + "content": "\na described issue\n\n\nContinue?\n\ncontinued\n" + } + } + }, + { + "type": "yield", + "coroutineId": "root", + "description": { + "type": "import_component", + "name": "IssueTracker", + "executablemd.source-position": { + "path": "/private/tmp/xmd-retained-history/h6-issue-unacknowledged/entry.md", + "offset": 0, + "line": 1, + "column": 1 + } + }, + "result": { + "status": "ok", + "value": { + "kind": "registered", + "origin": "@executablemd/workflow/composition", + "reserved": false + } + } + }, + { + "type": "yield", + "coroutineId": "root", + "description": { + "type": "import_component", + "name": "Issue", + "executablemd.source-position": { + "path": "/private/tmp/xmd-retained-history/h6-issue-unacknowledged/entry.md", + "offset": 53, + "line": 2, + "column": 1 + } + }, + "result": { + "status": "ok", + "value": { + "kind": "registered", + "origin": "@executablemd/workflow/composition", + "reserved": false + } + } + } +] diff --git a/packages/git/tests/fixtures/retained-repository-history/h6-issue-unacknowledged/source.md b/packages/git/tests/fixtures/retained-repository-history/h6-issue-unacknowledged/source.md new file mode 100644 index 000000000..5afb60f3c --- /dev/null +++ b/packages/git/tests/fixtures/retained-repository-history/h6-issue-unacknowledged/source.md @@ -0,0 +1,8 @@ + +a described issue + + +Continue? + +continued diff --git a/packages/git/tests/fixtures/retained-repository-history/h7-issue-failed/journal.json b/packages/git/tests/fixtures/retained-repository-history/h7-issue-failed/journal.json new file mode 100644 index 000000000..960f07d42 --- /dev/null +++ b/packages/git/tests/fixtures/retained-repository-history/h7-issue-failed/journal.json @@ -0,0 +1,99 @@ +[ + { + "type": "yield", + "coroutineId": "root", + "description": { + "type": "import_component", + "name": "__root__" + }, + "result": { + "status": "ok", + "value": { + "kind": "repository", + "path": "/private/tmp/xmd-retained-history/h7-issue-failed/entry.md", + "content": "\na described issue\n\n\nContinue?\n\ncontinued\n" + } + } + }, + { + "type": "yield", + "coroutineId": "root", + "description": { + "type": "import_component", + "name": "IssueTracker", + "executablemd.source-position": { + "path": "/private/tmp/xmd-retained-history/h7-issue-failed/entry.md", + "offset": 0, + "line": 1, + "column": 1 + } + }, + "result": { + "status": "ok", + "value": { + "kind": "registered", + "origin": "@executablemd/workflow/composition", + "reserved": false + } + } + }, + { + "type": "yield", + "coroutineId": "root", + "description": { + "type": "import_component", + "name": "Issue", + "executablemd.source-position": { + "path": "/private/tmp/xmd-retained-history/h7-issue-failed/entry.md", + "offset": 55, + "line": 2, + "column": 1 + } + }, + "result": { + "status": "ok", + "value": { + "kind": "registered", + "origin": "@executablemd/workflow/composition", + "reserved": false + } + } + }, + { + "type": "yield", + "coroutineId": "root", + "description": { + "type": "issue_effect", + "name": "2766aaca6683d5ea4405752dd0070163af48dc7ba865b58c445944f5525a98a6", + "request": { + "operation": "upsert", + "identity": { + "runId": "[\"executablemd.repl.issue-owner\",1,\"h7\",\"1\"]", + "expansionId": "a369ea1cb4ae5100e19fe60c9819e31b736d641d7c6972888358936b51004eee" + }, + "target": "https://github.com/octo/elsewhere", + "provider": null, + "issue": { + "title": "Filed", + "description": "a described issue", + "tags": [], + "assignee": null + } + }, + "executablemd.source-position": { + "path": "/private/tmp/xmd-retained-history/h7-issue-failed/entry.md", + "offset": 55, + "line": 2, + "column": 1 + } + }, + "result": { + "status": "err", + "error": { + "message": "the issue tracker is temporarily unavailable, so this request's state is unknown. Nothing was created on this answer, and nothing was retried: an explicit retry observes again before it may create.", + "name": "IssueUnavailableError", + "stack": "IssueUnavailableError: the issue tracker is temporarily unavailable, so this request's state is unknown. Nothing was created on this answer, and nothing was retried: an explicit retry observes again before it may create.\n at upsert (file:///Users/tarasmankovski/Repositories/taras/executable.md/.claude/worktrees/issue-181-boundary-a/packages/git/src/deno/issue/github.ts:442:17)\n at upsert.next ()\n at Object.[Symbol.iterator] (file:///Users/tarasmankovski/Repositories/taras/executable.md/.claude/worktrees/issue-181-boundary-a/node_modules/.deno/@effectionx+context-api@0.6.0/node_modules/@effectionx/context-api/dist/mod.js:20:61)\n at [Symbol.iterator].next ()\n at upsert (file:///Users/tarasmankovski/Repositories/taras/executable.md/.claude/worktrees/issue-181-boundary-a/packages/git/src/deno/retained-repository/issues.ts:277:51)\n at upsert.next ()\n at Object.[Symbol.iterator] (file:///Users/tarasmankovski/Repositories/taras/executable.md/.claude/worktrees/issue-181-boundary-a/node_modules/.deno/@effectionx+context-api@0.6.0/node_modules/@effectionx/context-api/dist/mod.js:20:61)\n at [Symbol.iterator].next ()\n at file:///Users/tarasmankovski/Repositories/taras/executable.md/.claude/worktrees/issue-181-boundary-a/packages/git/src/deno/retained-repository/issues.ts:244:48\n at Generator.next ()" + } + } + } +] diff --git a/packages/git/tests/fixtures/retained-repository-history/h7-issue-failed/source.md b/packages/git/tests/fixtures/retained-repository-history/h7-issue-failed/source.md new file mode 100644 index 000000000..81d95e524 --- /dev/null +++ b/packages/git/tests/fixtures/retained-repository-history/h7-issue-failed/source.md @@ -0,0 +1,8 @@ + +a described issue + + +Continue? + +continued diff --git a/packages/git/tests/fixtures/retained-repository-history/receipt.json b/packages/git/tests/fixtures/retained-repository-history/receipt.json new file mode 100644 index 000000000..0da1636e8 --- /dev/null +++ b/packages/git/tests/fixtures/retained-repository-history/receipt.json @@ -0,0 +1,386 @@ +{ + "capturedFrom": { + "commit": "7ee526665dcadd9ec79dca9f8b6a4726903d9a4f", + "tree": "942739ff98195a0113ad6508da48b05874deaf97", + "describedAs": "merged deliveries 4 (#937) and 6 (#926), before the component-boundary repair" + }, + "command": "deno run --allow-all --frozen .capture/capture-history.ts", + "captureScriptSha256": "5b100a485ef784a32d85a22393da89eb8d22ae498b9ea7ee2a5ad212e5b1ba3e", + "captureRoot": "/private/tmp/xmd-retained-history", + "trackerTarget": "https://github.com/octo/project", + "continuation": "Every recorded path is kept exactly as captured. A continuation recreates only the document at documentPath and addresses every other recorded path through a deterministic leaf host or service substitution. No fixture is regenerated by verification.", + "scenarios": [ + { + "fixture": "h1-push-completed", + "cut": "suspended inside the Elicit that follows the publication, so the root is unfinished and the publication's request and receipt are both acknowledged", + "owner": "[\"executablemd.repl.issue-owner\",1,\"h1\",\"1\"]", + "events": 15, + "journalSha256": "643585abfdea1dc2098932d5c89c7d56d09ef49e98eb2cae0b576c64a26d0f22", + "sourceSha256": "014f11f0110af3d9a5eab83694b4c6e66db14899530a325a3574d23c695cbd73", + "documentPath": "/private/tmp/xmd-retained-history/h1-push-completed/entry.md", + "paths": { + "root": "/private/tmp/xmd-retained-history/h1-push-completed", + "origin": "/private/tmp/xmd-retained-history/h1-push-completed/origin.git", + "checkout": "/private/tmp/xmd-retained-history/h1-push-completed/checkout", + "managedRoot": "/private/tmp/xmd-retained-history/h1-push-completed/repositories", + "enteredThrough": "/private/tmp/xmd-retained-history/h1-push-completed/entered" + }, + "service": { + "branch": "review/completed", + "remoteCommit": "953925a9da72933365d566ee724593c97b7e939b" + }, + "nativeSubcommands": [ + "clone", + "symbolic-ref", + "rev-parse", + "checkout", + "symbolic-ref", + "rev-parse", + "rev-parse", + "symbolic-ref", + "rev-parse", + "rev-parse", + "write-tree", + "rev-parse", + "rev-parse", + "rev-parse", + "switch", + "symbolic-ref", + "symbolic-ref", + "rev-parse", + "rev-parse", + "write-tree", + "symbolic-ref", + "rev-parse", + "rev-parse", + "write-tree", + "add", + "symbolic-ref", + "rev-parse", + "rev-parse", + "write-tree", + "symbolic-ref", + "rev-parse", + "rev-parse", + "write-tree", + "commit", + "rev-parse", + "log", + "log", + "symbolic-ref", + "rev-parse", + "rev-parse", + "write-tree", + "symbolic-ref", + "rev-parse", + "rev-parse", + "write-tree", + "symbolic-ref", + "rev-parse", + "rev-parse", + "init", + "ls-remote", + "push" + ] + }, + { + "fixture": "h2-push-pending", + "cut": "suspended inside the first ls-remote, so the publication request is acknowledged and the remote was never contacted", + "owner": "[\"executablemd.repl.issue-owner\",1,\"h2\",\"1\"]", + "events": 13, + "journalSha256": "939a714728d8ec3d9ab2bb7932668f5bfecccfe9df5a0c019778e170df0d765d", + "sourceSha256": "e5290d1984b63b5f54470abf32178a978c7c375b54d3c39a7e20ca2750eda6eb", + "documentPath": "/private/tmp/xmd-retained-history/h2-push-pending/entry.md", + "paths": { + "root": "/private/tmp/xmd-retained-history/h2-push-pending", + "origin": "/private/tmp/xmd-retained-history/h2-push-pending/origin.git", + "checkout": "/private/tmp/xmd-retained-history/h2-push-pending/checkout", + "managedRoot": "/private/tmp/xmd-retained-history/h2-push-pending/repositories", + "enteredThrough": "/private/tmp/xmd-retained-history/h2-push-pending/entered" + }, + "service": { + "branch": "review/pending", + "remoteCommit": null, + "movedCheckoutHead": "1d36b4fa3dcd9a6ab2f7563be00acf2e83052dc2" + }, + "nativeSubcommands": [ + "clone", + "symbolic-ref", + "rev-parse", + "checkout", + "symbolic-ref", + "rev-parse", + "rev-parse", + "symbolic-ref", + "rev-parse", + "rev-parse", + "write-tree", + "rev-parse", + "rev-parse", + "rev-parse", + "switch", + "symbolic-ref", + "symbolic-ref", + "rev-parse", + "rev-parse", + "write-tree", + "symbolic-ref", + "rev-parse", + "rev-parse", + "write-tree", + "add", + "symbolic-ref", + "rev-parse", + "rev-parse", + "write-tree", + "symbolic-ref", + "rev-parse", + "rev-parse", + "write-tree", + "commit", + "rev-parse", + "log", + "log", + "symbolic-ref", + "rev-parse", + "rev-parse", + "write-tree", + "symbolic-ref", + "rev-parse", + "rev-parse", + "write-tree", + "symbolic-ref", + "rev-parse", + "rev-parse", + "init", + "ls-remote" + ] + }, + { + "fixture": "h3-push-unacknowledged", + "cut": "the journal refused the git_host_effect append after the remote had accepted the branch, so the request stands acknowledged with no outcome and no root close", + "owner": "[\"executablemd.repl.issue-owner\",1,\"h3\",\"1\"]", + "events": 13, + "journalSha256": "d26fa13ab164f5c9a7c322f84d748b8162e6dc1f13227601dd72ee82931b57ee", + "sourceSha256": "6e8f525ca35b8255fe993b128260d0601b63306cb37a5bd68dbdad3626de2c97", + "documentPath": "/private/tmp/xmd-retained-history/h3-push-unacknowledged/entry.md", + "paths": { + "root": "/private/tmp/xmd-retained-history/h3-push-unacknowledged", + "origin": "/private/tmp/xmd-retained-history/h3-push-unacknowledged/origin.git", + "checkout": "/private/tmp/xmd-retained-history/h3-push-unacknowledged/checkout", + "managedRoot": "/private/tmp/xmd-retained-history/h3-push-unacknowledged/repositories", + "enteredThrough": "/private/tmp/xmd-retained-history/h3-push-unacknowledged/entered" + }, + "service": { + "branch": "review/adopt", + "remoteCommit": "ae88e480853fe1ab3e600c085c0ecf2519f1fa0d" + }, + "nativeSubcommands": [ + "clone", + "symbolic-ref", + "rev-parse", + "checkout", + "symbolic-ref", + "rev-parse", + "rev-parse", + "symbolic-ref", + "rev-parse", + "rev-parse", + "write-tree", + "rev-parse", + "rev-parse", + "rev-parse", + "switch", + "symbolic-ref", + "symbolic-ref", + "rev-parse", + "rev-parse", + "write-tree", + "symbolic-ref", + "rev-parse", + "rev-parse", + "write-tree", + "add", + "symbolic-ref", + "rev-parse", + "rev-parse", + "write-tree", + "symbolic-ref", + "rev-parse", + "rev-parse", + "write-tree", + "commit", + "rev-parse", + "log", + "log", + "symbolic-ref", + "rev-parse", + "rev-parse", + "write-tree", + "symbolic-ref", + "rev-parse", + "rev-parse", + "write-tree", + "symbolic-ref", + "rev-parse", + "rev-parse", + "init", + "ls-remote", + "push" + ] + }, + { + "fixture": "h4-push-failed", + "cut": "the shared worker acknowledged a definitive publication failure over a destination this commit does not descend from, and the document failed on it, so the root has no close", + "owner": "[\"executablemd.repl.issue-owner\",1,\"h4\",\"1\"]", + "events": 14, + "journalSha256": "b7a735af972e0e78f8a15b92fc1eec0dca0f511396edd014f40a878323852a97", + "sourceSha256": "c7b8535ebd860d98543452571a1cd4ff8023259f35ae1669fd7ed539ab9d3d34", + "documentPath": "/private/tmp/xmd-retained-history/h4-push-failed/entry.md", + "paths": { + "root": "/private/tmp/xmd-retained-history/h4-push-failed", + "origin": "/private/tmp/xmd-retained-history/h4-push-failed/origin.git", + "checkout": "/private/tmp/xmd-retained-history/h4-push-failed/checkout", + "managedRoot": "/private/tmp/xmd-retained-history/h4-push-failed/repositories", + "enteredThrough": "/private/tmp/xmd-retained-history/h4-push-failed/entered" + }, + "service": { + "branch": "review/divergent", + "remoteCommit": "630b930030715d9755ed643b9ae8cb22e4ae4887", + "refusal": "the Git host already holds state this effect conflicts with. Nothing was performed: conflicting external state is diagnosed rather than overwritten or duplicated." + }, + "nativeSubcommands": [ + "rev-parse", + "rev-parse", + "rev-parse", + "rev-parse", + "config", + "rev-parse", + "symbolic-ref", + "rev-parse", + "rev-parse", + "write-tree", + "rev-parse", + "switch", + "symbolic-ref", + "symbolic-ref", + "rev-parse", + "rev-parse", + "write-tree", + "symbolic-ref", + "rev-parse", + "rev-parse", + "write-tree", + "add", + "symbolic-ref", + "rev-parse", + "rev-parse", + "write-tree", + "symbolic-ref", + "rev-parse", + "rev-parse", + "write-tree", + "commit", + "rev-parse", + "log", + "log", + "symbolic-ref", + "rev-parse", + "rev-parse", + "write-tree", + "symbolic-ref", + "rev-parse", + "rev-parse", + "write-tree", + "symbolic-ref", + "rev-parse", + "rev-parse", + "init", + "ls-remote", + "rev-parse" + ] + }, + { + "fixture": "h5-issue-completed", + "cut": "suspended inside the Elicit after both issue operations were acknowledged, so the root is unfinished and the read holds the answer public middleware composed", + "owner": "[\"executablemd.repl.issue-owner\",1,\"h5\",\"1\"]", + "events": 7, + "journalSha256": "eefe786d065636199078f76dc38357fe19111d4f2f71d53b748bcaa3e0c2bae7", + "sourceSha256": "ecf6dbae55492eb9ea0771462c3a3120f7e6e67bad07249caf4062d179240443", + "documentPath": "/private/tmp/xmd-retained-history/h5-issue-completed/entry.md", + "paths": { + "root": "/private/tmp/xmd-retained-history/h5-issue-completed", + "origin": "/private/tmp/xmd-retained-history/h5-issue-completed/origin.git", + "checkout": "/private/tmp/xmd-retained-history/h5-issue-completed/checkout", + "managedRoot": "/private/tmp/xmd-retained-history/h5-issue-completed/repositories", + "enteredThrough": "/private/tmp/xmd-retained-history/h5-issue-completed/entered" + }, + "service": { + "issues": [ + { + "number": 1, + "state": "open", + "title": "Filed", + "body": "a described issue\n\n\n", + "labels": [], + "assignee": null + } + ], + "requests": 2 + }, + "nativeSubcommands": [] + }, + { + "fixture": "h6-issue-unacknowledged", + "cut": "suspended with the issue_effect append in flight, so the tracker holds the issue this entry filed and the journal holds no record of it", + "owner": "[\"executablemd.repl.issue-owner\",1,\"h6\",\"1\"]", + "events": 3, + "journalSha256": "94143e7d286447cbd9631cec71c25e9181d2e3ba518b36663b6686733fff356e", + "sourceSha256": "22741c593a9e0db72573a41fb28725ff44ab0284a0b85e01d9783b8e8bbd16ae", + "documentPath": "/private/tmp/xmd-retained-history/h6-issue-unacknowledged/entry.md", + "paths": { + "root": "/private/tmp/xmd-retained-history/h6-issue-unacknowledged", + "origin": "/private/tmp/xmd-retained-history/h6-issue-unacknowledged/origin.git", + "checkout": "/private/tmp/xmd-retained-history/h6-issue-unacknowledged/checkout", + "managedRoot": "/private/tmp/xmd-retained-history/h6-issue-unacknowledged/repositories", + "enteredThrough": "/private/tmp/xmd-retained-history/h6-issue-unacknowledged/entered" + }, + "service": { + "issues": [ + { + "number": 1, + "state": "open", + "title": "Filed", + "body": "a described issue\n\n\n", + "labels": [], + "assignee": null + } + ], + "requests": 2 + }, + "nativeSubcommands": [] + }, + { + "fixture": "h7-issue-failed", + "cut": "the issue effect acknowledged a definitive ceiling refusal and the document failed on it, so the root has no close", + "owner": "[\"executablemd.repl.issue-owner\",1,\"h7\",\"1\"]", + "events": 4, + "journalSha256": "bbf1eea08409cc6c986cb2ec02db77e0b9f5c958e363116179dbdf3fc9672c16", + "sourceSha256": "902ed7fdb2f80639041a4667fedccf9884d75af4ff18e37d668bf2ab685f4253", + "documentPath": "/private/tmp/xmd-retained-history/h7-issue-failed/entry.md", + "paths": { + "root": "/private/tmp/xmd-retained-history/h7-issue-failed", + "origin": "/private/tmp/xmd-retained-history/h7-issue-failed/origin.git", + "checkout": "/private/tmp/xmd-retained-history/h7-issue-failed/checkout", + "managedRoot": "/private/tmp/xmd-retained-history/h7-issue-failed/repositories", + "enteredThrough": "/private/tmp/xmd-retained-history/h7-issue-failed/entered" + }, + "service": { + "issues": [], + "requests": 0, + "refusal": "the issue tracker is temporarily unavailable, so this request's state is unknown. Nothing was created on this answer, and nothing was retried: an explicit retry observes again before it may create." + }, + "nativeSubcommands": [] + } + ] +} diff --git a/packages/git/tests/module-partition.test.ts b/packages/git/tests/module-partition.test.ts index 5e7161c93..a95cb93b8 100644 --- a/packages/git/tests/module-partition.test.ts +++ b/packages/git/tests/module-partition.test.ts @@ -64,6 +64,7 @@ const PROVIDER_NEUTRAL: readonly string[] = [ "src/composition/git-push-records.ts", "src/composition/git-records.ts", "src/composition/installation.ts", + "src/composition/invocation-binding.ts", "src/composition/locator.ts", "src/composition/observation-api.ts", "src/composition/observation-records.ts", diff --git a/packages/git/tests/retained-repository-history.test.ts b/packages/git/tests/retained-repository-history.test.ts new file mode 100644 index 000000000..63c1ccf5b --- /dev/null +++ b/packages/git/tests/retained-repository-history.test.ts @@ -0,0 +1,722 @@ +/** + * Seven histories the merged implementation wrote, four of them continued + * through the shared components that replaced it. + * + * The fixtures beside this file were emitted by old `main` + * `7ee526665dcadd9ec79dca9f8b6a4726903d9a4f` before the component-boundary + * repair touched production, and their receipt records the revision, the + * command, the owner, the paths each record addresses, where each history was + * cut and what service state the capture left behind. All seven are held to the + * bytes that receipt names, source and journal alike. Nothing here regenerates + * one: a fixture a verification rewrote would prove only that the new code + * agrees with itself. + * + * Four are continued: the definitive publication failure, the completed Issue + * read and upsert, the unacknowledged upsert and the definitive Issue failure. + * Each is cut while its root is unfinished, so continuing one re-enters the + * component rather than replaying a settled root. Every recorded path is used + * exactly as captured, and every physical thing a continuation needs belongs to + * that invocation: its own temporary directory, its own filesystem holding the + * recorded logical paths, its own component search path and its own entry + * installation. The root source is supplied by value at the path its record + * holds, so no invocation writes a file another invocation shares, and none of + * them needs anything the capture left on disk. + * + * ## What this suite does not prove + * + * The three publication histories — a completed publication, a pending one and + * one the remote accepted before the journal could record it — are held to their + * captured bytes here and are not continued. Continuing any of them performs + * new covered file work or resumes a pending publication, which takes the + * recorded placement's slot lock and proves that checkout's object database at + * the absolute paths the journal holds. Both belong to the real native host and + * stay there, so no invocation-owned substitute reaches them. + * + * So this suite does not show that those three old histories continue through + * the extraction. The current-code replay and reconciliation evidence — + * `retained-repository-push.test.ts`, `retained-repository-push-records.test.ts` + * and `git-host-owned.test.ts` — remains required, and does not stand in for + * that omitted historical proof. + * + * These rows carry A3 of the component-boundary repair matrix, as amended. + */ + +import { describe, it } from "@executablemd/test-support/bdd"; +import { expect } from "@executablemd/test-support/expect"; +import { all, Err, ensure, Ok, resource, scoped, until } from "effection"; +import type { Operation, Result } from "effection"; +import { readTextFile } from "@effectionx/fs"; +import { mkdir, mkdtemp, readFile, rm } from "node:fs/promises"; +import { createHash } from "node:crypto"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { API, FILES_WRITE_SUCCESS } from "@executablemd/runtime"; +import type { + FilePathInput, + FilesHandler, + FileWriteInput, + FileWriteSuccess, +} from "@executablemd/runtime"; +import { InMemoryStream } from "@executablemd/durable-streams"; +import type { DurableEvent, DurableStream, Json } from "@executablemd/durable-streams"; +import { collect, Elicitation, retainedSource } from "@executablemd/core"; +import type { RetainedRootDocument } from "@executablemd/core"; +import { executeInstalled } from "@executablemd/core/host"; +import { gitPlugin } from "../src/plugin.ts"; +import { replRepositoryInstallation } from "../src/deno/retained-repository/provider.ts"; +import { replIssueInstallation } from "../src/deno/retained-repository/issues.ts"; +import type { GitAuthenticationSession } from "../src/deno/composition/authentication.ts"; +import type { GitInvocation, GitOutcome, RepositoryHost } from "../src/deno/composition/host.ts"; +import { GIT_HOST_EFFECT } from "../src/git-host/effect-type.ts"; +import { ISSUE_EFFECT } from "../src/issue/effect-type.ts"; +import { gitHubSource } from "../src/deno/composition/github.ts"; +// From the subpath that publishes them rather than from the source module: +// these are public, and reaching them by relative path would bypass the export +// map the partition test holds every consumer to. +import { IssueApi } from "@executablemd/git/api"; +import type { IssueDetails, IssueReference } from "@executablemd/git/api"; +import { IssueOperations } from "../src/issue/operations.ts"; +import { parseIssueDetails, parseIssueRecord } from "../src/issue/records.ts"; +import { useIssueTrackerServer } from "./support/issue-tracker-server.ts"; +import type { ServedIssue } from "./support/issue-tracker-server.ts"; +import { gitHubAccessFor } from "./support/issue-providers.ts"; + +/** Where the immutable fixtures live, relative to this file. */ +const FIXTURES = new URL("./fixtures/retained-repository-history/", import.meta.url); + +/** The tracker every captured Issue history filed in. */ +const TRACKER = "https://github.com/octo/project"; + +/** One captured history, as its receipt records it. */ +interface Recorded { + readonly fixture: string; + readonly cut: string; + readonly owner: string; + readonly events: number; + readonly journalSha256: string; + readonly sourceSha256: string; + readonly documentPath: string; + readonly paths: { + readonly root: string; + readonly origin: string; + readonly checkout: string; + readonly managedRoot: string; + readonly enteredThrough: string; + }; + readonly service: Json; + readonly nativeSubcommands: readonly string[]; +} + +interface Receipt { + readonly capturedFrom: { readonly commit: string; readonly tree: string }; + readonly command: string; + readonly scenarios: readonly Recorded[]; +} + +/** The receipt, parsed rather than asserted: it is a file on disk. */ +function* receipt(): Operation { + const text = yield* readTextFile(new URL("receipt.json", FIXTURES).pathname); + const value: unknown = JSON.parse(text); + if (typeof value !== "object" || value === null || !("scenarios" in value)) { + throw new Error("the history receipt does not describe its scenarios"); + } + const scenarios = (value as { scenarios: unknown }).scenarios; + if (!Array.isArray(scenarios)) { + throw new Error("the history receipt's scenarios are not a list"); + } + const parsed: Recorded[] = []; + for (const scenario of scenarios) { + parsed.push(scenario as Recorded); + } + const captured = (value as { capturedFrom?: unknown }).capturedFrom; + const command = (value as { command?: unknown }).command; + if (typeof command !== "string" || typeof captured !== "object" || captured === null) { + throw new Error("the history receipt records no provenance"); + } + return { + capturedFrom: captured as Receipt["capturedFrom"], + command, + scenarios: parsed, + }; +} + +/** One scenario's recorded facts, by fixture name. */ +function* recorded(fixture: string): Operation { + const found = (yield* receipt()).scenarios.find((scenario) => scenario.fixture === fixture); + if (found === undefined) { + throw new Error(`the history receipt records no ${fixture}`); + } + return found; +} + +/** The exact source bytes that history ran. */ +function* source(fixture: string): Operation { + return yield* readTextFile(new URL(`${fixture}/source.md`, FIXTURES).pathname); +} + +/** The SHA-256 of one fixture file, over its bytes as they are on disk. */ +function* fileDigest(fixture: string, name: string): Operation { + const bytes = yield* until(readFile(new URL(`${fixture}/${name}`, FIXTURES).pathname)); + return createHash("sha256").update(bytes).digest("hex"); +} + +/** The exact journal bytes that history left behind. */ +function* journal(fixture: string): Operation { + const text = yield* readTextFile(new URL(`${fixture}/journal.json`, FIXTURES).pathname); + const events: unknown = JSON.parse(text); + if (!Array.isArray(events)) { + throw new Error(`${fixture} holds no journal`); + } + const parsed: DurableEvent[] = []; + for (const event of events) { + parsed.push(event as DurableEvent); + } + return parsed; +} + +/** + * The source each history ran, supplied by value at the path its own record + * holds. + * + * A root import record is the path and the exact bytes it was read from, so a + * continuation standing somewhere else would be a different run of a different + * document. `retainedSource` states both without reading or writing a file, so + * the record matches and no invocation writes into a path another invocation + * shares. + */ +function recordedRoot(scenario: Recorded, text: string): RetainedRootDocument { + return retainedSource(scenario.documentPath, text); +} + +/** + * One continuation's own filesystem, holding the logical paths its history + * records. + * + * Every write, read, deletion and directory this history's document asks for + * is keyed by the absolute logical path the recorded run used, and lives here + * for this invocation alone. The retained file middleware above it records the + * path it was asked for, which is what makes the record identical to the + * recorded run's — and nothing reaches the directory the capture left behind, + * so two of these running at once cannot see each other's work. + */ +interface InvocationFiles { + readonly handler: FilesHandler; + /** Each logical path written, in order, with what was written to it. */ + readonly written: Map; + /** Each logical directory this document asked to exist. */ + readonly directories: Set; +} + +function invocationFiles(temporary: string): InvocationFiles { + const written = new Map(); + const directories = new Set(); + const resolved = (input: FilePathInput): string => + input.path.startsWith("/") ? input.path : join(input.cwd, input.path); + const handler: FilesHandler = { + // deno-lint-ignore require-yield + *checkFilePath(): Operation> { + return Ok(undefined); + }, + // deno-lint-ignore require-yield + *readTextFile(input: FilePathInput): Operation> { + const held = written.get(resolved(input)); + return held === undefined + ? Err(new Error(`this invocation holds no ${resolved(input)}`)) + : Ok(held); + }, + // deno-lint-ignore require-yield + *writeTextFile(input: FileWriteInput): Operation> { + written.set(resolved(input), input.content); + return Ok({ type: FILES_WRITE_SUCCESS, publication: "host-committed" }); + }, + // deno-lint-ignore require-yield + *deleteFile(input: FilePathInput): Operation> { + written.delete(resolved(input)); + return Ok(undefined); + }, + // deno-lint-ignore require-yield + *ensureDirectory(input: FilePathInput): Operation> { + directories.add(resolved(input)); + return Ok(undefined); + }, + // deno-lint-ignore require-yield + *globFiles(): Operation> { + return Ok([]); + }, + // deno-lint-ignore require-yield + *temporaryDirectory(): Operation> { + return Ok(temporary); + }, + }; + return { handler, written, directories }; +} + +/** What a continuation asked of its substituted leaves. */ +interface Asked { + /** Every native invocation, in order, as its argument list. */ + readonly commands: string[][]; + /** Every isolation directory acquired. */ + directories: number; + /** Every authentication session opened, by locator. */ + readonly sessions: string[]; +} + +function asked(): Asked { + return { commands: [], directories: 0, sessions: [] }; +} + +/** + * A native host that answers nothing and records everything. + * + * What the four provider-free rows below need: any invocation at all defeats + * what those rows assert, so the refusal says so rather than pretending to be + * `git`. + */ +function refusingHost(seen: Asked): RepositoryHost { + return { + // deno-lint-ignore require-yield + *git(invocation: GitInvocation): Operation { + seen.commands.push([...invocation.args]); + throw new Error(`a restored history ran git ${invocation.args.join(" ")}`); + }, + // deno-lint-ignore require-yield + *useDirectory(): Operation { + seen.directories += 1; + throw new Error("a restored history acquired an isolation directory"); + }, + // deno-lint-ignore require-yield + *useAuthentication(locator: string): Operation { + seen.sessions.push(locator); + throw new Error("a restored history opened an authentication session"); + }, + }; +} + +interface Continued { + readonly output: string; + readonly refusal: string | undefined; + readonly events: readonly DurableEvent[]; + /** What this invocation's own filesystem holds afterwards. */ + readonly files: InvocationFiles; +} + +/** What one row arranges beneath a continuation. */ +interface Arrangement { + /** The native host, for a history whose continuation may reach one. */ + readonly host?: RepositoryHost; + /** The entry's issue capability, for a history whose work was issues. */ + readonly issues?: { readonly endpoint?: string }; + /** The answer a question in this history receives. */ + readonly answer?: Json; + /** What this row installs between the entry and the document. */ + readonly between?: () => Operation; +} + +/** + * Continue one captured history through the shared components. + * + * The production entry assembly, for the owner the capture recorded, over the + * exact journal bytes it left behind and the exact document it ran. Everything + * physical belongs to this invocation: its own temporary directory, its own + * filesystem holding the recorded logical paths, and its own entry + * installation. Nothing is written at a path another invocation shares, and + * nothing is read from the directories the capture left behind. + */ +function continueHistory( + scenario: Recorded, + text: string, + stream: DurableStream, + arrangement: Arrangement = {}, +): Operation { + return scoped(function* (): Operation { + const temporary = yield* useScratch(); + const files = invocationFiles(temporary); + yield* API.Env.around( + { + // deno-lint-ignore require-yield + *cwd(): Operation { + // The checkout the recorded run was standing in, as its own records + // address it. A Push capture was entered through a symbolic link and + // the recorded request carries the directory that link resolved to, + // so supplying that directory reproduces the routing the capture + // computed without a continuation having to resolve a link only the + // capture machine holds. The receipt records both spellings. + return scenario.paths.checkout; + }, + }, + { at: "min" }, + ); + yield* API.Files.around( + { + checkFilePath: ([input]) => files.handler.checkFilePath(input), + readTextFile: ([input]) => files.handler.readTextFile(input), + globFiles: ([input]) => files.handler.globFiles(input), + temporaryDirectory: () => files.handler.temporaryDirectory(), + writeTextFile: ([input]) => files.handler.writeTextFile(input), + deleteFile: ([input]) => files.handler.deleteFile(input), + ensureDirectory: ([input]) => files.handler.ensureDirectory(input), + }, + { at: "min" }, + ); + if (arrangement.answer !== undefined) { + const answer = arrangement.answer; + yield* Elicitation.around( + { + // deno-lint-ignore require-yield + *elicit(): Operation { + return answer; + }, + }, + { at: "min" }, + ); + } + if (arrangement.between !== undefined) { + yield* arrangement.between(); + } + const contributed = yield* replContribution(); + const host = arrangement.host; + const endpoint = arrangement.issues?.endpoint; + const installation = + host === undefined + ? replIssueInstallation(scenario.owner, { + ...(endpoint === undefined + ? {} + : { access: gitHubSource(gitHubAccessFor(endpoint, { credential: "valid" })) }), + configuration: { ceiling: [TRACKER] }, + }) + : yield* replRepositoryInstallation(scenario.owner, { + host, + identity: statedIdentity, + root: scenario.paths.managedRoot, + }); + let refusal: string | undefined; + let output = ""; + try { + output = String( + yield* collect( + yield* executeInstalled( + { + ...recordedRoot(scenario, text), + stream, + // This invocation's own search path, which holds no component — + // exactly as the capture's did, so every name falls through to + // the same registered default and every recorded selection + // matches. Pointing it at the capture's directory would make a + // continuation depend on a machine that still has it. + includes: [join(temporary, "components")], + }, + [contributed, installation], + ), + ), + ); + } catch (error) { + refusal = error instanceof Error ? error.message : String(error); + } + return { output, refusal, events: yield* stream.readAll(), files }; + }); +} + +/** What the Git Plugin contributes to a `repl` entry's execution. */ +function* replContribution(): Operation<{ + readonly admissions: readonly ((retained: readonly DurableEvent[]) => Operation)[]; +}> { + const install = gitPlugin.install; + if (install === undefined) { + throw new Error("the Git Plugin installs nothing"); + } + const installed = yield* install.call(gitPlugin, { command: "repl", args: [] }); + if (installed === undefined) { + throw new Error("the Git Plugin declared nothing for the repl command"); + } + return { admissions: [...(installed.admissions ?? [])] }; +} + +/** The identity the capture stated, restated here so a replay matches it. */ +const STATED_IDENTITY: Record = { + GIT_AUTHOR_IDENT: "Entry Author 1700000000 +0000", + GIT_COMMITTER_IDENT: "Entry Committer 1700000000 +0000", +}; + +// deno-lint-ignore require-yield +function* statedIdentity(variable: string): Operation { + return STATED_IDENTITY[variable]; +} + +/** Every Git-host outcome this history holds, as the journal holds it. */ +function outcomes(events: readonly DurableEvent[]): DurableEvent[] { + return events.filter( + (event) => event.type === "yield" && event.description.type === GIT_HOST_EFFECT, + ); +} + +/** How many times each public chain was entered, which on a replay is never. */ +interface Chains { + operations: number; + api: number; +} + +/** + * Both Issue chains, set to fail if anything reaches them. + * + * Installed between the entry capability and the document, where a document's + * own middleware goes, so a completed replay that asked either of them is a + * failure of this row rather than a number nobody checked. + */ +function* refusingChains(counted: Chains): Operation { + yield* IssueOperations.around({ + // deno-lint-ignore require-yield + *read(): Operation { + counted.operations += 1; + throw new Error("a replayed read reached the composed Issue chain"); + }, + // deno-lint-ignore require-yield + *upsert(): Operation { + counted.operations += 1; + throw new Error("a replayed upsert reached the composed Issue chain"); + }, + }); + yield* IssueApi.around({ + // deno-lint-ignore require-yield + *read(): Operation { + counted.api += 1; + throw new Error("a replayed read reached the Issue transport"); + }, + // deno-lint-ignore require-yield + *upsert(): Operation { + counted.api += 1; + throw new Error("a replayed upsert reached the Issue transport"); + }, + }); +} + +/** The value one retained record holds, as the journal holds it. */ +function valueOf(event: DurableEvent | undefined): unknown { + return event?.type === "yield" && event.result.status === "ok" ? event.result.value : undefined; +} + +/** Every Issue effect this history holds, in order. */ +function issues(events: readonly DurableEvent[]): DurableEvent[] { + return events.filter( + (event) => event.type === "yield" && event.description.type === ISSUE_EFFECT, + ); +} + +/** The prefix a continuation inherited, which it may not rewrite. */ +function prefix(events: readonly DurableEvent[], length: number): readonly DurableEvent[] { + return events.slice(0, length); +} + +/** + * One invocation's own physical directory, removed with it. + * + * Everything this continuation needs a real path for lives here: the + * substituted host's isolation root, the temporary directory the document + * filesystem hands out, and the component search path. Two continuations + * running at once hold two of these and share none of it. + */ +function useScratch(): Operation { + return resource(function* (provide) { + const root = yield* until(mkdtemp(join(tmpdir(), "retained-history-"))); + yield* ensure(() => until(rm(root, { recursive: true, force: true }))); + yield* until(mkdir(join(root, "components"), { recursive: true })); + yield* provide(root); + }); +} + +describe("histories the merged implementation wrote, held to their bytes and continued", () => { + it("A3: all seven fixtures are the ones old main emitted, and verification rewrites none", function* (): Operation { + // Every captured history, including the three this suite no longer + // continues: what they are held to here is their bytes, which is what makes + // them available to a later continuation unchanged. + const record = yield* receipt(); + expect(record.capturedFrom.commit).toBe("7ee526665dcadd9ec79dca9f8b6a4726903d9a4f"); + expect(record.capturedFrom.tree).toBe("942739ff98195a0113ad6508da48b05874deaf97"); + expect(record.scenarios).toHaveLength(7); + for (const scenario of record.scenarios) { + // The bytes, first and by hash. These files are the evidence, so a + // reserialized journal is a different artifact however equal its parsed + // value is — and a formatter that rewrote one would be caught here + // rather than in a later reviewer's audit. + expect(yield* fileDigest(scenario.fixture, "journal.json")).toBe(scenario.journalSha256); + expect(yield* fileDigest(scenario.fixture, "source.md")).toBe(scenario.sourceSha256); + const events = yield* journal(scenario.fixture); + expect(events).toHaveLength(scenario.events); + // No root close: every one of these was cut while its root was + // unfinished, so a continuation re-enters the component rather than + // replaying a settled root. + expect(events.some((event) => event.type === "close" && event.coroutineId === "root")).toBe( + false, + ); + // The owner every record in it was written under is the one the receipt + // names, which is what a continuation presents again. + expect(scenario.owner).toContain("executablemd.repl.issue-owner"); + } + }); + + it("H4: a definitive publication failure restores as itself, provider-free", function* (): Operation { + const scenario = yield* recorded("h4-push-failed"); + const text = yield* source("h4-push-failed"); + const inherited = yield* journal("h4-push-failed"); + const seen = asked(); + const stream = new InMemoryStream([...inherited]); + + const again = yield* continueHistory(scenario, text, stream, { + host: refusingHost(seen), + answer: { choice: "continue" }, + }); + + // The failure the old implementation acknowledged is the failure this run + // reports, and it reaches no provider to find that out. + expect(again.refusal).not.toBe(undefined); + expect(seen.commands).toEqual([]); + expect(seen.directories).toBe(0); + expect(seen.sessions).toEqual([]); + expect(prefix(again.events, inherited.length)).toEqual(inherited); + const failed = outcomes(again.events); + expect(failed).toHaveLength(1); + expect(failed[0]?.type === "yield" ? failed[0].result.status : undefined).toBe("err"); + }); + + it("H5: restores the exact composed answer and URL, and enters neither chain", function* (): Operation { + const scenario = yield* recorded("h5-issue-completed"); + const text = yield* source("h5-issue-completed"); + const inherited = yield* journal("h5-issue-completed"); + const stream = new InMemoryStream([...inherited]); + const entered: Chains = { operations: 0, api: 0 }; + + // No endpoint at all, and both public chains set to fail if anything + // reaches them. A replay that asked middleware first, or a transport before + // matching its record, fails here rather than passing a count that happened + // to be right. + const again = yield* continueHistory(scenario, text, stream, { + issues: {}, + answer: { choice: "continue" }, + between: () => refusingChains(entered), + }); + + expect(again.refusal).toBe(undefined); + expect(prefix(again.events, inherited.length)).toEqual(inherited); + expect(entered).toEqual({ operations: 0, api: 0 }); + + const records = issues(again.events); + expect(records).toHaveLength(2); + + // What the components restore, stated here rather than read back out of + // the same events: the upsert's URL, and the complete answer the public + // middleware composed for the read without reaching a tracker. A + // schema-valid value that was not this one fails these two assertions. + const upsert = parseIssueRecord(valueOf(records[0])); + expect(upsert).toEqual({ url: `${TRACKER}/issues/1` }); + const read = parseIssueDetails(valueOf(records[1])); + expect(read).toEqual({ + url: `${TRACKER}/issues/1`, + title: "What the middleware answered", + description: "answered without reaching a tracker", + tags: ["alpha", "beta"], + assignee: null, + }); + }); + + it("H7: a definitive issue failure restores without a transport", function* (): Operation { + const scenario = yield* recorded("h7-issue-failed"); + const text = yield* source("h7-issue-failed"); + const inherited = yield* journal("h7-issue-failed"); + const stream = new InMemoryStream([...inherited]); + + const again = yield* continueHistory(scenario, text, stream, { + issues: {}, + answer: { choice: "continue" }, + }); + + expect(again.refusal).not.toBe(undefined); + expect(prefix(again.events, inherited.length)).toEqual(inherited); + const failed = issues(again.events); + expect(failed).toHaveLength(1); + expect(failed[0]?.type === "yield" ? failed[0].result.status : undefined).toBe("err"); + }); + + it("A3: two continuations at once own separate state and share none of it", function* (): Operation { + const completed = yield* recorded("h5-issue-completed"); + const unacknowledged = yield* recorded("h6-issue-unacknowledged"); + const server = yield* useIssueTrackerServer({ + issues: seededIssues(unacknowledged).map((issue) => ({ ...issue })), + }); + + // Two histories continued at the same time. Each writes its suffix at the + // logical path its own record addresses, into its own filesystem, so what + // either one did is invisible to the other and neither needs a path the + // other owns. + const [one, two] = yield* all([ + continueHistory( + completed, + yield* source(completed.fixture), + new InMemoryStream([...(yield* journal(completed.fixture))]), + { issues: {}, answer: { choice: "continue" } }, + ), + continueHistory( + unacknowledged, + yield* source(unacknowledged.fixture), + new InMemoryStream([...(yield* journal(unacknowledged.fixture))]), + { issues: { endpoint: server.url }, answer: { choice: "continue" } }, + ), + ]); + + expect(one?.refusal).toBe(undefined); + expect(two?.refusal).toBe(undefined); + const wrote = (continued: Continued | undefined): string[] => [ + ...(continued?.files.written.keys() ?? []), + ]; + expect(wrote(one)).toEqual([`${completed.paths.checkout}/after.md`]); + expect(wrote(two)).toEqual([`${unacknowledged.paths.checkout}/after.md`]); + // Disjoint, which is what owning its own state means: neither map holds a + // path the other wrote, and no third party holds either. + for (const path of wrote(one)) { + expect(wrote(two)).not.toContain(path); + } + }); + + it("H6: an unacknowledged upsert presents its original key and adopts one issue", function* (): Operation { + const scenario = yield* recorded("h6-issue-unacknowledged"); + const text = yield* source("h6-issue-unacknowledged"); + const inherited = yield* journal("h6-issue-unacknowledged"); + const stream = new InMemoryStream([...inherited]); + + // The service state the capture left behind: the tracker already holds the + // issue this entry filed, carrying the mark the adapter wrote under the + // original key. A deterministic substitute addressing the same tracker. + const served = seededIssues(scenario); + expect(served).toHaveLength(1); + const server = yield* useIssueTrackerServer({ issues: served.map((issue) => ({ ...issue })) }); + + const again = yield* continueHistory(scenario, text, stream, { + issues: { endpoint: server.url }, + answer: { choice: "continue" }, + }); + + expect(again.refusal).toBe(undefined); + expect(prefix(again.events, inherited.length)).toEqual(inherited); + // One issue at the tracker, not two: the continuation presented the key the + // interrupted attempt presented, and the adapter found its own mark. + expect(server.issues).toHaveLength(1); + expect(server.requests.some((request) => request.method === "POST")).toBe(false); + const records = issues(again.events); + expect(records).toHaveLength(1); + }); +}); + +/** The issues the capture's receipt says its tracker was left holding. */ +function seededIssues(scenario: Recorded): ServedIssue[] { + const service = scenario.service; + if (typeof service !== "object" || service === null || Array.isArray(service)) { + throw new Error(`${scenario.fixture} records no service state`); + } + const held = (service as Record)["issues"]; + if (!Array.isArray(held)) { + throw new Error(`${scenario.fixture} records no served issues`); + } + const parsed: ServedIssue[] = []; + for (const issue of held) { + parsed.push(issue as unknown as ServedIssue); + } + return parsed; +} diff --git a/scripts/runtime-test-exclusions.ts b/scripts/runtime-test-exclusions.ts index c31df3d7c..869ac10c5 100644 --- a/scripts/runtime-test-exclusions.ts +++ b/scripts/runtime-test-exclusions.ts @@ -748,6 +748,12 @@ const DENO_ONLY_REPOSITORY_PROVIDER: RuntimeExclusion[] = [ "the subject is a REPL entry's native publication: it clones a managed checkout, reads and pushes to a real bare remote with real `git` children, holds that checkout's managed slot under a kernel-released exclusive advisory lock taken through the Deno runtime, and cuts an entry mid-command. The saved request, its admission and the owner-specified worker are provider-neutral and are proven on every runtime by packages/git/tests/retained-repository-push-records.test.ts and packages/git/tests/git-host-owned.test.ts; that a runtime without the provider refuses before any native operation is proven by packages/cli/tests/repl-push-pty.test.ts, which runs everywhere", issue: DERIVED_SCOPE, }, + { + path: "packages/git/tests/retained-repository-history.test.ts", + reason: + "continues seven histories the merged implementation wrote through the production per-entry capability, so its module graph reaches @executablemd/git/deno — the native repository host, the GitHub issue adapter and Workflow's storage adapter with node:sqlite. The construction those histories are continued through is provider-neutral and is proven on every runtime by packages/git/tests/composition-component-binding.test.ts; the records themselves are proven everywhere by packages/git/tests/retained-repository-push-records.test.ts and packages/git/tests/issue-owned.test.ts", + issue: "https://github.com/taras/executable.md/issues/181", + }, { path: "packages/cli/tests/repl-push.test.ts", reason: diff --git a/specs/executable-mdx-spec.md b/specs/executable-mdx-spec.md index bc28bf38d..e6539eae0 100644 --- a/specs/executable-mdx-spec.md +++ b/specs/executable-mdx-spec.md @@ -3062,11 +3062,24 @@ Markdown or TypeScript component of any of those names is chosen ahead of them, for its own scope alone. `Dir` is a separate protected core component and is available without the Git Plugin. -They are one array declared by the bundled Git Plugin and read wherever its -profile is assembled: an ordinary document execution, `xmd syntax`, `xmd plan`'s -validation and generation, and a workflow action that executes a document. A -Workspace attachment installs that run's providers and durable state and -declares none of them. +They are one declaration source, read wherever the bundled Git Plugin's profile +is assembled: an ordinary document execution, `xmd syntax`, `xmd plan`'s +validation and generation, and a workflow action that executes a document. + +Six of them reach something no local transaction encloses — `Git.Push`, +`PullRequest`, its three evidence reads and `Issue` — and each has exactly one +implementation, which asks a semantic feature operation for its work and +receives nothing else. Every command that carries this vocabulary registers all +sixteen, `repl` included: that registry is what says which names exist, and it is +what a run, `xmd syntax`, `xmd plan` and a REPL entry's preflight all read. An +execution that owns a retained boundary over one of those six operations, +authenticated per invocation, declares that same name again in its own scope +with its own policy under it, where a nearer declaration shadows the registered +default. The names, props, forms, return contracts, origins and prose are the +same either way, and a repository-local component of one of those names is still +chosen ahead of the default. A Workspace attachment installs that run's +providers and durable state — the retention its pull-request lifecycle puts +beneath the feature operation among them — and declares none of them. Registering them installs no provider, discovers no repository, acquires no lock, spawns no Git and reads no credential — describing an environment mints nothing. What each name *does* is decided by whichever repository provider the @@ -10441,11 +10454,12 @@ own: the execution and the entry's admission-order position, derived by the host that opened the session and reaching the component as a trusted closure rather than as a prop, a Context value, a tracker or a middleware answer. The authored site inside it is the identity canonical core minted where it selected that -implementation. The `` and the `` an entry runs are ordinary -shadowable registrations of names the Plugin already declares, so a repository -component of either name keeps its ordinary precedence and obtains no identity -of the entry's — and a direct `Git.operations.push(...)` outside the trusted -invocation reaches the ordinary missing-provider refusal. +implementation. The `` and the `` an entry runs are the same +shared components every other host runs, declared to this execution with the +entry's policy beneath them, so a repository component of either name keeps its +ordinary precedence and obtains no identity of the entry's — and a direct +`Git.operations.push(...)` outside that invocation reaches the ordinary +missing-provider refusal. Node and Bun install none of them. They register the same declarations (§5.3), and every repository operation there reports an absent provider before a lock, a diff --git a/specs/repl-spec.md b/specs/repl-spec.md index 7cf4f18f3..e94b2ba65 100644 --- a/specs/repl-spec.md +++ b/specs/repl-spec.md @@ -2252,10 +2252,14 @@ resolved against the frame that published its coordinates. as it promises: nothing locks the repository, watches it or compares every write that happened while it ran. What continuation checks is whether the *answer* changed, not whether the filesystem did. -- `` is an ordinary shadowable default here, as it is under every other - host. A repository component of that name keeps its ordinary precedence over - it, and what such a component does not get is the identity an entry's own - issue work is named by. +- `` is the same component here as under every other host: one + implementation, the same props, forms and return contract, asking a semantic + issue operation for its work. The `repl` command registers it as an ordinary + default like every other command does; what an entry supplies is the policy + beneath it, declared to that entry's own execution over the default — and a + repository component of that name keeps its ordinary precedence over both. + What such a component does not get is the identity an entry's own issue work + is named by. - Generated selection is unchanged. An admitted generated `` read, write or `` is retained exactly as an authored one is, because both cross the one document filesystem. `` stays ordinary and is retained by diff --git a/specs/workflow-workspace-spec.md b/specs/workflow-workspace-spec.md index 9290fdd9f..e0c928fcc 100644 --- a/specs/workflow-workspace-spec.md +++ b/specs/workflow-workspace-spec.md @@ -206,6 +206,14 @@ it: the objects a publication reads come from the caller's own checkout through read-only door rather than from an exported Workspace family, and no Workspace root, savepoint or frontier takes part. +The components themselves are the same in all three environments. ``, `` and `` have one implementation each: it interprets +its props and forms, expands its own content, normalizes what the document wrote +and asks a semantic feature operation. What differs between a workflow run, an +ordinary run and a REPL entry is the policy beneath that operation — which +journal it writes to, what it reconciles and what it replays — and nothing a +document can see. + `` is not available to a REPL entry and refuses there as it does under any host that installs no provider for it; neither a retained local result nor a retained publication authorizes one. `` is available on the same @@ -2926,7 +2934,7 @@ A REPL entry retains its issue work too, under this same effect type and these same records, and names it after the entry rather than after a run: a REPL entry is not a WorkflowRun and installs no synthetic one. What the entry adds is where the live attempt goes. One durable boundary encloses the operation; inside a -live attempt the trusted implementation composes `IssueOperations` — so a +live attempt the entry's own policy composes `IssueOperations` — so a document's own middleware answers, transforms, refuses and delegates there as it does anywhere — over a transport-facing default of its own that reaches the `IssueApi` provider and adds no second envelope. The composed answer is what the