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
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,9 @@ jobs:
- name: A REPL entry's issues across the compiled binary
run: deno test --allow-all --frozen scripts/tests/repl-issues-compiled.test.ts

- name: A REPL entry's publications across the compiled binary
run: deno test --allow-all --frozen scripts/tests/repl-push-compiled.test.ts

# Each package's `components.md` is a packaged asset on the same footing,
# and the long-form documentation is the only thing that reports whether
# one shipped — a binary missing them still lists every component and
Expand Down
122 changes: 111 additions & 11 deletions architecture.md

Large diffs are not rendered by default.

12 changes: 1 addition & 11 deletions packages/cli/src/compiled-repl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,11 @@ import { homedir } from "node:os";
import { sep } from "node:path";
import process from "node:process";
import type { Operation } from "effection";
import type { HelperAssembly } from "@executablemd/git/credential-helper";
import { installReplHost, platformDataRoot, replHostFacts } from "./repl-assembly.ts";
import { XMD_VERSION } from "./cli.ts";
import { useCompiledReplTerminal } from "./compiled-repl-terminal.ts";
import { useDenoReplRepositories } from "./deno-repositories.ts";

export function* useCompiledRepl(helper: HelperAssembly): Operation<void> {
export function* useCompiledRepl(): Operation<void> {
yield* installReplHost({
dataRoot: () => platformDataRoot(process.platform, homedir(), process.env, sep),
// What this process is, stated here because this is where it is known.
Expand All @@ -39,13 +37,5 @@ export function* useCompiledRepl(helper: HelperAssembly): Operation<void> {
createExclusive: (path) => open(path, "wx").then((handle) => handle.close()),
appendRecord: (path, record) => appendFile(path, record),
});
// What an entry's execution initializes: the recording of the File and Dir
// work it does, and of the managed Repository and Worktree selections that
// say where it does it. Registered here, in the runtime scope the selected
// REPL host owns, because only a runtime-named module may name the native
// adapter and this runtime's own credential-helper assembly — and registering
// it acquires nothing, so a session that only reads history discovers no
// checkout, creates no managed root and takes no lease.
yield* useDenoReplRepositories(helper);
yield* useCompiledReplTerminal();
}
7 changes: 4 additions & 3 deletions packages/cli/src/compiled.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,9 +99,10 @@ if (isCredentialHelperMode(process.argv.slice(2))) {
() => useDenoWorkflowHost(HELPER),
useMachineSessions(),
// What this host selects for a REPL command, and what it installs for
// one. The selection is the issue capability an entry runs with; Node
// and Bun supply neither half.
{ select: selectReplEntryHost, install: () => useCompiledRepl(HELPER) },
// one. The selection is the capability an entry runs with — its
// checkouts, its local Git, its publications and its issues; Node and Bun
// supply neither half.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// supply neither half.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// supply neither half.

{ select: () => selectReplEntryHost(HELPER), install: () => useCompiledRepl() },
);
});
}
12 changes: 1 addition & 11 deletions packages/cli/src/deno-repl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,11 @@ import { homedir } from "node:os";
import { sep } from "node:path";
import process from "node:process";
import type { Operation } from "effection";
import type { HelperAssembly } from "@executablemd/git/credential-helper";
import { installReplHost, platformDataRoot, replHostFacts } from "./repl-assembly.ts";
import { XMD_VERSION } from "./cli.ts";
import { useDenoReplTerminal } from "./deno-repl-terminal.ts";
import { useDenoReplRepositories } from "./deno-repositories.ts";

export function* useDenoRepl(helper: HelperAssembly): Operation<void> {
export function* useDenoRepl(): Operation<void> {
yield* installReplHost({
dataRoot: () => platformDataRoot(process.platform, homedir(), process.env, sep),
// What this process is, stated here because this is where it is known.
Expand All @@ -39,13 +37,5 @@ export function* useDenoRepl(helper: HelperAssembly): Operation<void> {
createExclusive: (path) => open(path, "wx").then((handle) => handle.close()),
appendRecord: (path, record) => appendFile(path, record),
});
// What an entry's execution initializes: the recording of the File and Dir
// work it does, and of the managed Repository and Worktree selections that
// say where it does it. Registered here, in the runtime scope the selected
// REPL host owns, because only a runtime-named module may name the native
// adapter and this runtime's own credential-helper assembly — and registering
// it acquires nothing, so a session that only reads history discovers no
// checkout, creates no managed root and takes no lease.
yield* useDenoReplRepositories(helper);
yield* useDenoReplTerminal();
}
71 changes: 23 additions & 48 deletions packages/cli/src/deno-repositories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,7 @@ import type { Plugin, PluginInstallRequest, PluginInstallation } from "@executab
import type { Operation } from "effection";
import { cwd } from "@executablemd/runtime";
import { gitPlugin } from "@executablemd/git";
import {
replIssueInstallation,
useReplRepositoryRecording,
useRunComposition,
} from "@executablemd/git/deno";
import { replRepositoryInstallation, useRunComposition } from "@executablemd/git/deno";
import type { HelperAssembly } from "@executablemd/git/credential-helper";
import { DEFAULT_REPOSITORY_ROOT } from "./run-repositories.ts";
import { ReplEntryHost } from "./repl-entry-installation.ts";
Expand Down Expand Up @@ -109,20 +105,29 @@ export function denoRunRepositories(
}

/**
* The issue capability a REPL entry runs with on this host.
* What a REPL entry runs with on this host: its checkouts, its local Git, its
* publications and its issues.
*
* Inert, and selected by the runtime entrypoint beside the bundled Plugin:
* naming the native GitHub adapter is a runtime-named module's business, and
* this is the value the shared command path carries without importing one. It
* reads no configuration and holds no credential — what an entry's issue work
* is allowed to reach is read by the invoked operation that needs it.
* naming the native adapter and this runtime's own credential-helper assembly
* is a runtime-named module's business, and this is the value the shared
* command path carries without importing one. It reads no configuration and
* holds no credential — what an entry's work is allowed to reach is read by the
* invoked operation that needs it.
*
* The owner is supplied per entry by the session that derived it, which is why
* this is a factory rather than an installation: one value per host, one
* installation per entry.
* capability per entry. One factory, because an entry initializes its
* repository state once; two registrations of it would be two providers.
*/
export const denoReplIssues: ReplEntryInstallation = (owner: string) =>
replIssueInstallation(owner);
export function denoReplEntry(helper: HelperAssembly, root: string): ReplEntryInstallation {
// Copied here, at selection, rather than held as the caller's object. What an
// entry initializes with has to be what this host selected: a caller that
// kept a reference and changed a field afterwards would be configuring every
// later entry from outside the selection the command already made.
const configured = snapshot(helper);
return (owner: string) => replRepositoryInstallation(owner, { helper: configured, root });
}

/**
* Select that value for the REPL command that is about to assemble itself.
Expand All @@ -132,8 +137,11 @@ export const denoReplIssues: ReplEntryInstallation = (owner: string) =>
* this adapter. Setting it establishes nothing: the value is inert code, and
* what an entry does with it happens inside that entry.
*/
export function* selectReplEntryHost(): Operation<void> {
yield* ReplEntryHost.set(denoReplIssues);
export function* selectReplEntryHost(
helper: HelperAssembly,
root: string = DEFAULT_REPOSITORY_ROOT,
): Operation<void> {
yield* ReplEntryHost.set(denoReplEntry(helper, root));
}

/**
Expand All @@ -156,36 +164,3 @@ function snapshot(helper: HelperAssembly): HelperAssembly {
: { launcherEnvironment: Object.freeze({ ...helper.launcherEnvironment }) }),
});
}

/**
* What this registration gives a REPL entry's execution: recording, selection
* and local Git.
*
* Registered by the selected REPL host rather than by the bundled Plugin,
* because an entry is not an ordinary run: it retains the file work it does, the
* managed checkouts it selects to do that work in, and the staging, branch
* switches and commits it performs in them — and it installs no publication and
* no external worker. The issue capability an entry runs with is selected
* separately, above, because it belongs to no checkout. Each executing entry initializes its own, in the scope
* below its Agent attachment, so a passive view — a settled history, a frozen
* position — initializes nothing, creates no managed root, reads no commit
* identity and takes no lease.
*
* The root and the helper are copied here, at registration, rather than held as
* the caller's object. What an entry initializes with has to be what this host
* selected: a caller that kept a reference and changed a field afterwards would
* be configuring every later entry from outside the selection the command
* already made.
*/
export function* useDenoReplRepositories(
helper: HelperAssembly,
root: string = DEFAULT_REPOSITORY_ROOT,
): Operation<void> {
const configured = snapshot(helper);
yield* Execution.around({
*init(_args, next): Operation<void> {
yield* useReplRepositoryRecording({ helper: configured, root });
yield* next();
},
});
}
7 changes: 4 additions & 3 deletions packages/cli/src/deno.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,9 +118,10 @@ if (isCredentialHelperMode(process.argv.slice(2))) {
() => useDenoWorkflowHost(HELPER),
useMachineSessions(),
// What this host selects for a REPL command, and what it installs for
// one. The selection is the issue capability an entry runs with; Node
// and Bun supply neither half.
{ select: selectReplEntryHost, install: () => useDenoRepl(HELPER) },
// one. The selection is the capability an entry runs with — its
// checkouts, its local Git, its publications and its issues; Node and Bun
// supply neither half.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// supply neither half.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// supply neither half.

{ select: () => selectReplEntryHost(HELPER), install: () => useDenoRepl() },
);
});
}
29 changes: 19 additions & 10 deletions packages/cli/src/repl-entry-installation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,33 +8,42 @@
* per *entry*, for the exact execution and entry key the entry belongs to.
*
* What crosses is a factory, and nothing else. It holds code and this
* deployment's configuration; it holds no owner, no site, no provider, no
* credential, no Git session and no identity, so nothing read from here is
* authority over permission, durable identity or reconciliation. The owner is
* deployment's configuration — the managed root and the credential-helper
* assembly; it holds no owner, no site, no provider, no credential, no Git
* session and no identity, so nothing read from here is authority over
* permission, durable identity or reconciliation. The owner is
* derived by the session for the entry it is about to run, and the identity
* inside the installation is minted by canonical core.
*
* Deno and the compiled binary select a value; Node and Bun leave the default
* absence, which is what makes `<Issue>` on those runtimes reach its ordinary
* missing-provider refusal rather than a half-configured host.
* absence, which is what makes `<Issue>` and `<Git.Push />` on those runtimes
* reach their ordinary missing-provider refusals rather than a half-configured
* host.
*
* It is CLI-private. No package member exports it and no selector names it:
* this is the host saying what an entry runs with.
*/

import { createContext } from "effection";
import type { Context } from "effection";
import type { Context, Operation } from "effection";
import type { ExecutionInstallation } from "@executablemd/core/host";

/**
* What one entry's capability is built from: the owner of that entry's retained
* work.
*
* Called inside the entry task, once, for the entry about to execute. Calling
* it performs nothing — what it answers with is a declaration, an admission and
* an installation callback the execution runs in its own scope.
* Called inside the entry task, once, for the entry about to execute. What it
* answers with is declarations, admissions and an installation callback the
* execution runs in its own scope.
*
* It is an operation because an entry's capability may have to *register*
* something in that task before the execution starts — the repository
* capability registers the `Execution.init` its own state is created by. It
* still acquires nothing: no directory, no lease, no `git`, no credential and
* no initialization of its own happens here, and the registration belongs to
* the entry task rather than to a scope that closes when this returns.
*/
export type ReplEntryInstallation = (owner: string) => ExecutionInstallation;
export type ReplEntryInstallation = (owner: string) => Operation<ExecutionInstallation>;

/** The factory this host runs REPL entries with, or absence. */
export const ReplEntryHost: Context<ReplEntryInstallation | undefined> = createContext<
Expand Down
9 changes: 8 additions & 1 deletion packages/cli/src/repl/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1324,8 +1324,15 @@ function* start(
// what a host attaches is copied by value before anything installs:
// an entry that got one afterwards would be running under a capability
// the execution never admitted.
//
// Yielded in this task, after the Agent attachment above and before the
// execution below, so what the capability registers for the execution —
// the initialization its own private state is created by — belongs to
// the entry rather than to a scope that has already closed.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// the entry rather than to a scope that has already closed.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// the entry rather than to a scope that has already closed.

const owned =
entryInstallation === undefined ? [] : [entryInstallation(entryOwner(execution.id, key))];
entryInstallation === undefined
? []
: [yield* entryInstallation(entryOwner(execution.id, key))];
// This entry's own selection, offered in the scope this execution runs
// in. Every entry installs it again: core reads the final contextual
// answer once per execution, and a chain assembled in a scope that has
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ await main(function* (args) {
yield* useHostFiles();
// The assembly this host selects, stated here as an entrypoint states it.
// Inert: this fixture selects no repository, so nothing ever reaches it.
yield* useDenoRepl({ runtime: "source", platform: "unix", execPath: process.execPath });
yield* useDenoRepl();

const ran = yield* runReplProgram({
profile: {
Expand Down
67 changes: 48 additions & 19 deletions packages/cli/tests/repl-issues.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,8 @@ import { assembleReplProfile } from "../src/repl-profile.ts";
import type { ReplExecutionProfile } from "../src/repl-profile.ts";
import { resolveAgentStack } from "../src/agent-stack.ts";
import { NO_PLUGINS } from "../src/plugin-host.ts";
import {
denoReplIssues,
selectReplEntryHost,
useDenoReplRepositories,
} from "../src/deno-repositories.ts";
import { selectReplEntryHost } from "../src/deno-repositories.ts";
import type { HelperAssembly } from "../../git/src/deno/composition/credential-helper.ts";
import { ReplHost } from "../src/repl/host.ts";
import { openReplSession, submitReplEntry } from "../src/repl/session.ts";
import type { ReplSession } from "../src/repl/session.ts";
Expand All @@ -76,6 +73,29 @@ import type { ReplExecution, RetainedStream } from "../src/repl/journal.ts";
/** The tracker every row files in, as a document writes it. */
const TRACKER = "https://github.com/octo/project";

/** The credential-helper assembly these rows' host selects. Never invoked. */
const SUITE_HELPER: HelperAssembly = Object.freeze({
runtime: "source",
platform: "unix",
execPath: "/nonexistent/xmd",
});

/**
* The repository recording an entry's execution initializes.
*
* Registered on the same seam the per-entry factory registers it on, because
* what these rows are about is an entry's issue work beside its checkouts
* rather than which host assembled either.
*/
function useReplRepositories(root: string): Operation<void> {
return Execution.around({
*init(_args, next): Operation<void> {
yield* useReplRepositoryRecording({ helper: SUITE_HELPER, root });
yield* next();
},
});
}

/**
* What a command contributes to an entry's execution.
*
Expand Down Expand Up @@ -175,7 +195,8 @@ function entryHost(
seen: Counted,
options: GitHubIssuesOptions = {},
): ReplEntryInstallation {
return (owner: string) => {
// deno-lint-ignore require-yield
return function* (owner: string): Operation<ExecutionInstallation> {
seen.owners.push(owner);
return replIssueInstallation(owner, {
access: gitHubSource(gitHubAccessFor(fixture.server.url, { credential: "valid" })),
Expand Down Expand Up @@ -242,10 +263,7 @@ function submit(
yield* useHostFiles();
yield* useWorkingDirectory(options.cwd ?? fixture.checkout);
if (options.repositories !== undefined) {
yield* useDenoReplRepositories(
{ runtime: "source", platform: "unix", execPath: "/nonexistent/xmd" },
options.repositories,
);
yield* useReplRepositories(options.repositories);
}
const installations = [...(yield* replInstallations()), ...(options.installations ?? [])];
const session = opened(
Expand Down Expand Up @@ -275,10 +293,7 @@ function reopen(fixture: Fixture, run: ReplExecution, options: RunOptions = {}):
yield* useHostFiles();
yield* useWorkingDirectory(options.cwd ?? fixture.checkout);
if (options.repositories !== undefined) {
yield* useDenoReplRepositories(
{ runtime: "source", platform: "unix", execPath: "/nonexistent/xmd" },
options.repositories,
);
yield* useReplRepositories(options.repositories);
}
const installations = [...(yield* replInstallations()), ...(options.installations ?? [])];
const session = opened(
Expand Down Expand Up @@ -1450,10 +1465,14 @@ describe("how a REPL entry gets its issues", () => {
if (!stack.ok) {
throw stack.error;
}
yield* selectReplEntryHost();
yield* selectReplEntryHost(SUITE_HELPER, planWriterRoot);
return yield* assembleReplProfile(stack.value, NO_PLUGINS, { planWriterRoot });
});
expect(selected.entryInstallation).toBe(denoReplIssues);
// Behavioural rather than by identity: the production selector builds the
// factory from this host's own configuration, so what the profile carries
// is a closure over it rather than a module-level value to compare against.
const factory = selected.entryInstallation;
expect(typeof factory).toBe("function");

// And without it: the same assembly, in a scope nothing selected in, leaves
// the member absent rather than reaching for a default.
Expand All @@ -1466,9 +1485,19 @@ describe("how a REPL entry gets its issues", () => {
});
expect(absent.entryInstallation).toBe(undefined);

// Selecting reaches nothing: no issue host exists yet, because an
// installation is built per entry and this command has run none.
expect(typeof selected.entryInstallation).toBe("function");
// 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.
const built = yield* scoped(function* (): Operation<ExecutionInstallation> {
if (factory === undefined) {
throw new Error("the production selector selected no entry capability");
}
return yield* factory('["executablemd.repl.issue-owner",1,"execution-1","1"]');
});
expect([...(built.components ?? [])].map((component) => component.name).sort()).toEqual([
"Git.Push",
"Issue",
]);
});

it("E2: shadows the inherited default, and refuses where no host selected one", function* (): Operation<void> {
Expand Down
Loading
Loading