Skip to content

Add contextual workflow runs with pinned commits #289

Description

@taras

Story

As a workflow author, I want each workflow run tied to one pinned starting commit and each executable element identified deterministically, so durable records and effects have explicit identities without extending execution lifetimes.

Scope

Add Expansion and getExpansion() to @executablemd/core. Introduce @executablemd/workflow with WorkflowRun, useWorkflow({ base }), getWorkflowRun(), and the contextual Git.revParse() capability.

getExpansion() replaces the public invocation() metadata reader without an alias. Private invocation lifecycle machinery remains internal.

WorkflowRun contains:

  • runId — an opaque stable identifier;
  • base — the supplied Git revision; and
  • pinnedCommit — the commit obtained by resolving the base once.

Git.revParse() has the semantics of:

git rev-parse --verify --end-of-options <revision>

The workflow package depends on @executablemd/core, @executablemd/durable-streams, and @executablemd/runtime. Core never imports workflow or Git.

Workflow-run lifetime

useWorkflow() installs ordinary middleware in the child scope that owns one document execution. Installing it alone creates no workflow run.

The first durable operation:

  1. allocates a cryptographically random run ID;
  2. resolves base^{commit} through Git.revParse();
  3. records one immutable WorkflowRun before root import.

getWorkflowRun() is available only inside the associated document execution. It throws:

  • before execute() starts;
  • when execute() has no useWorkflow() installation; and
  • after that document execution completes, even while the installer scope still exists.

Concurrent document executions remain isolated through Effection scope ownership.

Replay

Replay restores the recorded WorkflowRun without allocating another run ID or invoking Git. The supplied base must equal the recorded base. A completed journal must restore workflow metadata before the root-close shortcut, and a malformed record is refused rather than coerced.

Expansion identity

Expansion reports id, authored name, and opening-tag source position when known. Its ID derives from root document and structural expansion path, never from process-global state, time, randomness, scheduling order, or run ID.

Replay, retry attempts, and restoration of the same loop iteration preserve the ID. Distinct authored elements, loop iterations, projections, component expansions, and root documents receive distinct IDs.

Acceptance

  • Every descendant observes one frozen WorkflowRun by reference during a live document execution.
  • getWorkflowRun() obeys the exact before/during/after lifetime above.
  • Replay invokes neither run-ID allocation nor Git and refuses a changed base.
  • Git failures occur before root import and record no successful WorkflowRun.
  • Ordinary execute() and xmd run remain Git-independent without workflow middleware.
  • getExpansion() throws outside an executable element expansion and exposes no props, bindings, selected component, projected content, or live scope.
  • Expansion IDs satisfy every determinism and distinction claim above.
  • Workflow-wide durable identity combines run ID and expansion ID; the expansion ID does not embed the run ID.
  • Tests discriminate scope, derivation, replay, and Git-resolution boundaries.

Intentionally excluded

Dependencies

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions