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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,23 @@ start` and `xmd workflow resume` are the CLI lifecycle, and they resume through
the run storage below. Ordinary `xmd run` carries the bundled Git Plugin, so
the repository vocabulary is available there without an operator naming it.

Git publishes on three subpaths, and which one a consumer imports from says
what kind of thing it is reaching for. `@executablemd/git/api` is the only
route to the contextual Apis — `Git`, `RepositoryComposition`,
`RepositoryContext`, `GitComposition`, `PullRequestAPI`, `IssueApi`,
`IssueTrackerContext` and `GitHost` — together with their named interfaces,
the identity each was minted under, the base refusal each falls back to, the
direct operations and accessors consumers call, and the types those interfaces
are written in. Those are the seams: a consumer reaches one to call an
operation and replaces one to answer it. The root publishes the Plugin and its
profile predicate, the component registrations and definitions, the workflow
installation, the durable effect identifiers, the errors and the record
parsers — what was said, rather than who answers. `@executablemd/git/deno`
publishes the host adapters that implement the seams, and
`@executablemd/git/credential-helper` is the standalone program Git spawns as
itself. An Api is published from exactly one of these, not re-exported from
the others, because a seam reachable two ways is two contracts.

## Workflow run storage

A workflow run recorded only in the journal of the document execution that
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
"@executablemd/code-review-agent": "workspace:*",
"@executablemd/core": "workspace:*",
"@executablemd/durable-streams": "workspace:*",
"@executablemd/git": "workspace:*",
"@executablemd/runtime": "workspace:*",
"@executablemd/test-agent": "workspace:*",
"@executablemd/test-support": "workspace:*",
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/workflow-bundle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,9 @@ import {
} from "@executablemd/core";
import type { WorkflowBundleComponent } from "@executablemd/core/host";
import { decodeSourceText, sourceContentHash } from "@executablemd/workflow";
import { readGitObject, revParse } from "@executablemd/git";
import { readGitObject, revParse } from "@executablemd/git/api";
import type { WorkflowComponentEntry } from "@executablemd/workflow";
import type { GitObjectFormat } from "@executablemd/git";
import type { GitObjectFormat } from "@executablemd/git/api";
import type { EstablishedComponent } from "./workflow-definition.ts";

/** Hexadecimal digits per object id, by the format that names them. */
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/workflow-definition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ import {
sourceBundleHash,
sourceContentHash,
} from "@executablemd/workflow";
import { gitObjectFormat, readGitObject, repositoryRoot, revParse } from "@executablemd/git";
import { gitObjectFormat, readGitObject, repositoryRoot, revParse } from "@executablemd/git/api";
import type {
GitWorkflowDefinitionV1,
SourceBundleEntryV2,
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/tests/testing-activation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { inlineSource, registerComponents, useTempFileCompiler } from "@executab
import type { Json } from "@executablemd/core";
import { executeInstalled } from "@executablemd/core/host";
import { retainedWorkflowInstallation } from "@executablemd/workflow";
import { Git } from "@executablemd/git";
import { Git } from "@executablemd/git/api";
import { installTestingComponents } from "@executablemd/testing";
import type { TestResult } from "@executablemd/testing";

Expand Down
2 changes: 1 addition & 1 deletion packages/cli/tests/workflow-installation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import {
} from "@executablemd/workflow/deno";
import type { WorkflowExecutionTransitions } from "@executablemd/workflow/deno";
import { WorkflowLifecycle, WorkflowRunStorage } from "@executablemd/workflow";
import { Git } from "@executablemd/git";
import { Git } from "@executablemd/git/api";
import type { WorkflowRunDatabase, WorkflowRunStatus } from "@executablemd/workflow";
import { runWorkflow } from "../src/workflow.ts";
import type { WorkflowExecution, WorkflowHost, WorkflowRequest } from "../src/workflow.ts";
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/tests/workflow-lifecycle-control.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import {
} from "@executablemd/workflow/deno";
import type { WorkflowExecutionTransitions } from "@executablemd/workflow/deno";
import { suspendFor, WorkflowLifecycle } from "@executablemd/workflow";
import { Git } from "@executablemd/git";
import { Git } from "@executablemd/git/api";
import type { WorkflowRunDatabase } from "@executablemd/workflow";
import { collect, inlineSource, registerComponents } from "@executablemd/core";
import { executeInstalled } from "@executablemd/core/host";
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/tests/workflow-suspension.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ import {
} from "@executablemd/workflow/deno";
import type { WorkflowExecutionTransitions } from "@executablemd/workflow/deno";
import { SUSPENSION_REQUEST, suspendFor, WorkflowLifecycle } from "@executablemd/workflow";
import { Git } from "@executablemd/git";
import { Git } from "@executablemd/git/api";
import type { WorkflowRunDatabase } from "@executablemd/workflow";
import { workflowRunPath } from "@executablemd/workflow/deno";
import { withWorkflowWorkspace, WORKSPACE_FILE } from "@executablemd/workflow/deno";
Expand Down
141 changes: 141 additions & 0 deletions packages/git/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
/**
* @module
*
* Every contextual Api this package owns, and the contract each is written in.
*
* A contextual Api is a seam: a consumer reaches one to *call* an operation,
* and replaces one — `Git.around({…}, { at: "min" })` — to *answer* it. Both
* sides need the same value, so both sides need one place to import it from.
* Mixed in among records, errors, parsers and component definitions on the
* package root, that value was indistinguishable from data, and which names
* were replaceable seams was something a reader had to already know.
*
* So this is the consumer route, and the only one. Eight Apis live here with
* their named interfaces, the identity each was minted under, the base refusal
* each falls back to when nobody answered, the direct operations and accessors
* consumers actually call, and every type those interfaces are spelled in. A
* name here is something you can implement.
*
* ```ts
* import { Git, RepositoryComposition } from "@executablemd/git/api";
*
* // Call one.
* const ambient = yield* RepositoryComposition.operations.repository();
*
* // Or answer it. Providers install at `min` so a nested replacement wins
* // rather than being shadowed by an outer handler.
* yield* Git.around({ *revParse(revision) { … } }, { at: "min" });
* ```
*
* The types travel with the Apis because an interface you cannot spell is one
* you cannot implement. They stay exported from the package root as well: a
* record that appears in an Api signature is still an ordinary record when a
* consumer only wants to read one, and that is an additive re-export rather
* than a second home.
*
* What is *not* here: the Plugin value and its profile predicate, the
* component registrations and definitions, the workflow installation, the
* durable effect identifiers, the error classes and the record parsers. Those
* describe what was said, not who answers. The runtime providers that
* implement these seams are not here either — they live behind
* `@executablemd/git/deno`, because which host can answer is a different
* question from what the question is.
*/

// The Git capability, and the four operations pre-bound for callers who want
// the operation rather than the seam.
export { Git, gitObjectFormat, readGitObject, repositoryRoot, revParse } from "./src/git.ts";
export type { GitApi, GitObjectFormat } from "./src/git.ts";

// Repository composition: what a `<Repository>` or `<Worktree>` selects, and
// the credential-free selection it hands back.
export { RepositoryComposition } from "./src/composition/api.ts";
export type {
RepositoryCompositionApi,
RepositoryRequest,
WorktreeRequest,
} from "./src/composition/api.ts";
export type { RepositorySelection } from "./src/composition/selection.ts";

// Which repository is lexically in scope, and the accessor that reads it.
export { currentRepository, RepositoryContext } from "./src/composition/context.ts";
export type { RepositoryContextApi } from "./src/composition/context.ts";

// The authored local Git transitions — switch, add, commit, push — with the
// places they are invoked at and the results they record.
export { GitComposition } from "./src/composition/git-api.ts";
export type {
GitAddInvocation,
GitCommitInvocation,
GitCompositionApi,
GitInvocationPlace,
GitPushInvocation,
GitSwitchInvocation,
} from "./src/composition/git-api.ts";
export type {
GitAddResult,
GitCommitMessageSource,
GitCommitResult,
GitSwitchResult,
} from "./src/composition/git-records.ts";
export type { GitPushOutcome } from "./src/composition/git-push-records.ts";

// Pull requests, with the identity the Api was minted under and the refusal a
// request nobody answered reaches.
export {
NoPullRequestProvider,
PULL_REQUEST_API,
PullRequestAPI,
} from "./src/composition/pull-request-api.ts";
export type {
PullRequestApi,
PullRequestInput,
PullRequestOperation,
PullRequestReadOptions,
PullRequestUpsertOptions,
} from "./src/composition/pull-request-api.ts";
export type {
PullRequestReadKind,
PullRequestReadResult,
} from "./src/composition/pull-request-read-records.ts";
export type { PullRequestResult } from "./src/composition/pull-request-records.ts";

// Issues, on the same terms: middleware matches its own targets, and a request
// everyone delegated reaches `NoIssueProvider` unchanged.
export { ISSUE_API, IssueApi, NoIssueProvider } from "./src/issue/api.ts";
export type {
IssueDetails,
IssueInput,
IssueOperation,
IssueReadOptions,
IssueReference,
IssueUpsertOptions,
} from "./src/issue/api.ts";

// The nearest lexical `<IssueTracker>`, the accessor that reads it, and the
// tracker value itself.
export {
currentIssueTracker,
ISSUE_TRACKER_CONTEXT,
IssueTrackerContext,
} from "./src/issue/context.ts";
export type { IssueTrackerContextApi } from "./src/issue/context.ts";
export type { IssueTracker } from "./src/issue/tracker.ts";

// The Git host the pull-request and issue providers reconcile through, with
// the durable reconciliation a provider takes part in.
export { GIT_HOST_API, GitHost } from "./src/git-host/api.ts";
export type {
GitHostApi,
GitHostCall,
GitHostPhase,
GitHostPhaseDetails,
GitHostProvider,
GitHostRoutingRequest,
} from "./src/git-host/api.ts";
export { reconcileGitHostEffect, withGitHostProvider } from "./src/git-host/effect.ts";
export type {
CompleteGitHostEffectRequest,
GitHostCompletion,
GitHostObservation,
} from "./src/git-host/records.ts";
5 changes: 3 additions & 2 deletions packages/git/deno.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
"license": "MIT",
"exports": {
".": "./mod.ts",
"./deno": "./deno.ts",
"./credential-helper": "./credential-helper.ts"
"./api": "./api.ts",
"./credential-helper": "./credential-helper.ts",
"./deno": "./deno.ts"
}
}
50 changes: 14 additions & 36 deletions packages/git/mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,17 @@
* default export is the Plugin itself; everything beside it is the surface a
* provider adapter composes against.
*
* **The contextual Apis are not here.** `Git`, `RepositoryComposition`,
* `RepositoryContext`, `GitComposition`, `PullRequestAPI`, `IssueApi`,
* `IssueTrackerContext` and `GitHost` — with their interfaces, identities,
* direct operations and the types those interfaces are written in — publish
* from `@executablemd/git/api`. A seam a consumer can answer is a different
* kind of thing from a record it can read, and one route to each is what
* keeps that legible. What stays here is the Plugin and its profile
* predicate, the component registrations and definitions, the workflow
* installation, the durable effect identifiers, the errors, and the record
* parsers and their types.
*
* ```md
* <Repository name="site" url="https://github.com/octo/site.git">
* <Git.Switch branch="topic" />
Expand Down Expand Up @@ -79,21 +90,8 @@ export { gitDirectoryEntry } from "./src/composition/definitions.ts";
*/
export { declaresFor as gitPluginDeclaresFor } from "./src/plugin.ts";
export { workflowInstallation } from "./src/installation.ts";
export {
Git,
gitObjectFormat,
GitObjectError,
GitRepositoryError,
GitRevisionError,
readGitObject,
repositoryRoot,
revParse,
} from "./src/git.ts";
export type { GitApi, GitObjectFormat } from "./src/git.ts";
export { RepositoryComposition } from "./src/composition/api.ts";
export type { RepositoryCompositionApi } from "./src/composition/api.ts";
export { currentRepository, RepositoryContext } from "./src/composition/context.ts";
export type { RepositoryContextApi } from "./src/composition/context.ts";
export { GitObjectError, GitRepositoryError, GitRevisionError } from "./src/git.ts";
export type { GitObjectFormat } from "./src/git.ts";
export {
GitCompositionProviderError,
GitOperationError,
Expand Down Expand Up @@ -125,13 +123,7 @@ export type {
WorktreeCreationRequest,
WorktreeRecord,
} from "./src/composition/records.ts";
export {
NoPullRequestProvider,
PULL_REQUEST_API,
PullRequestAPI,
} from "./src/composition/pull-request-api.ts";
export type {
PullRequestApi,
PullRequestInput,
PullRequestReadOptions,
PullRequestUpsertOptions,
Expand All @@ -141,8 +133,6 @@ export {
pullRequestProviderName,
} from "./src/composition/pull-request-target.ts";
export type { PullRequestTarget } from "./src/composition/pull-request-target.ts";
export { GitComposition } from "./src/composition/git-api.ts";
export type { GitCompositionApi } from "./src/composition/git-api.ts";
export {
gitAddResultJson,
gitCommitResultJson,
Expand Down Expand Up @@ -241,7 +231,6 @@ export {
compositionDocumentation,
useCompositionComponents,
} from "./src/composition/installation.ts";
export { ISSUE_API, IssueApi, NoIssueProvider } from "./src/issue/api.ts";
export type {
IssueDetails,
IssueInput,
Expand All @@ -250,11 +239,6 @@ export type {
IssueReference,
IssueUpsertOptions,
} from "./src/issue/api.ts";
export {
ISSUE_TRACKER_CONTEXT,
IssueTrackerContext,
currentIssueTracker,
} from "./src/issue/context.ts";
export { ISSUE_EFFECT } from "./src/issue/effect-type.ts";
export {
IssueAmbiguousError,
Expand All @@ -272,9 +256,7 @@ export {
withinIssueCeiling,
} from "./src/issue/tracker.ts";
export type { IssueDestination, IssueTracker } from "./src/issue/tracker.ts";
export { GIT_HOST_API, GitHost } from "./src/git-host/api.ts";
export type {
GitHostApi,
GitHostCall,
GitHostPhase,
GitHostPhaseDetails,
Expand Down Expand Up @@ -307,11 +289,7 @@ export type {
GitHostObservation,
GitHostReconciliationRecord,
} from "./src/git-host/records.ts";
export {
GIT_HOST_EFFECT,
reconcileGitHostEffect,
withGitHostProvider,
} from "./src/git-host/effect.ts";
export { GIT_HOST_EFFECT } from "./src/git-host/effect.ts";
export {
filteredRepositoryIdentity,
parseRepositoryIdentity,
Expand Down
5 changes: 3 additions & 2 deletions packages/git/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@
"type": "module",
"exports": {
".": "./mod.ts",
"./deno": "./deno.ts",
"./credential-helper": "./credential-helper.ts"
"./api": "./api.ts",
"./credential-helper": "./credential-helper.ts",
"./deno": "./deno.ts"
},
"dependencies": {
"@effectionx/context-api": "0.6.0",
Expand Down
4 changes: 2 additions & 2 deletions packages/git/src/composition/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ export interface RepositoryCompositionApi {
* has no such thing at all — a workflow document names its repositories, and
* the component's own refusal is what says so.
*/
ambientRepository(): Operation<RepositorySelection | undefined>;
repository(): Operation<RepositorySelection | undefined>;
}

export const RepositoryComposition: Api<RepositoryCompositionApi> =
Expand All @@ -103,7 +103,7 @@ export const RepositoryComposition: Api<RepositoryCompositionApi> =
throw new RepositoryCompositionProviderError("<Worktree>");
},
// deno-lint-ignore require-yield
*ambientRepository(): Operation<RepositorySelection | undefined> {
*repository(): Operation<RepositorySelection | undefined> {
throw new RepositoryCompositionProviderError("an element written outside a <Repository>");
},
});
2 changes: 1 addition & 1 deletion packages/git/src/composition/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,5 +52,5 @@ export function* selectedRepository(): Operation<RepositorySelection | undefined
if (lexical !== undefined) {
return lexical;
}
return yield* RepositoryComposition.operations.ambientRepository();
return yield* RepositoryComposition.operations.repository();
}
2 changes: 1 addition & 1 deletion packages/git/src/deno/composition/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -308,7 +308,7 @@ export function useRepositoryComposition(
// `undefined` rather than a refusal: which component was written, and
// what it needed a repository for, is the component's own sentence.
// deno-lint-ignore require-yield
*ambientRepository(): Operation<RepositorySelection | undefined> {
*repository(): Operation<RepositorySelection | undefined> {
return undefined;
},
},
Expand Down
Loading
Loading