Skip to content

Execute Markdown workflows without Git #443

Description

@taras

Story

As a workflow author, I want xmd workflow start to execute the exact Markdown file I supplied, including an untracked file or one outside a Git repository, so starting and resuming a workflow does not require Git and always uses one immutable definition.

xmd workflow start ./prepare-release.md

The command snapshots the file's current bytes before the run becomes durable. The first execution and every resume use that retained snapshot even if the original file is edited, moved or deleted.

Current gap

workflow start currently locates a repository, resolves HEAD, and reads the document with repositoryRoot(), revParse() and readGitObject(). Consequently:

  • a file outside Git cannot start;
  • an untracked file cannot start;
  • a modified tracked file executes its older committed bytes; and
  • Workflow's definition identity, source retrieval, run record and journal binding all depend on Git object fields.

That dependency also prevents #822 from moving repository collaboration into @executablemd/git: Workflow still needs Git to load its own definition, while the Git Plugin needs Workflow's durable execution boundaries.

Definition v2

New starts and new candidate definitions for forks use this closed descriptor:

interface SourceBundleWorkflowDefinitionV2 {
  readonly version: 2;
  readonly kind: "source-bundle";
  readonly hashAlgorithm: "sha256";
  readonly bundleHash: string;
  readonly entrypoint: string;
  readonly sources: readonly SourceBundleEntryV2[];
  /** One exact canonical document target, without a leading `#`. */
  readonly targetPath?: string;
  readonly components?: readonly SourceBundleComponentV2[];
}

interface SourceBundleEntryV2 {
  readonly path: string;
  readonly sourceHash: string;
  readonly byteLength: number;
}

interface SourceBundleComponentV2 {
  readonly name: string;
  readonly path: string;
}

The descriptor admits exactly those members. bundleHash and every sourceHash are 64 lowercase hexadecimal digits. byteLength is a non-negative safe integer. sources is non-empty and sorted by the UTF-8 byte order of path; every path appears once. entrypoint equals exactly one source path.

components, when present, is non-empty and sorted by the UTF-8 byte order of name; every name appears once and every component path names a retained source. Absence means the definition declares no workflow components, while an empty array is refused. Component names retain the existing XMD component-name grammar and existing exclusions for structural, engine-owned and host-reserved names.

A root without an explicit workflow.components declaration produces one source entry. Existing explicit workflow components remain a supported closed dependency: their Markdown bytes produce additional source entries and the components mapping on the same v2 descriptor. The sources array can therefore carry later dependency closure without another definition-version or identity migration; adding, removing or changing a retained dependency changes the bundle identity.

The ordinary definition serializer emits members in the order shown above, with targetPath and components omitted when absent. Object-member order is presentation, not identity: a parser admits those exact members in any order, while storage and artifacts may apply their own canonical JSON key ordering. Unknown members, duplicate or non-canonically ordered source/component array entries, an empty optional collection, or a hash or length outside its grammar is refused.

Logical paths

A logical path is a portable identity inside the source bundle, not a host filesystem path. It:

  • is a non-empty NFC-normalized Unicode string containing only Unicode scalar values;
  • uses / separators and never begins or ends with /;
  • contains no NUL, C0 control, DEL, backslash or # character;
  • contains no empty, . or .. segment; and
  • is compared case-sensitively by its UTF-8 bytes, without locale or filesystem normalization.

The entrypoint must end in .md. For the current one-file common path, the host derives it from the supplied file's final path segment after NFC normalization. The containing directory, absolute path, invocation working directory and platform separators never enter the descriptor. Existing declared component paths are resolved relative to the source file before the run exists, then represented by canonical logical paths in the same bundle.

Different host paths containing the same exact bytes under the same logical entrypoint describe the same definition. Changing the logical entrypoint is an identity change because source positions and future relative references use it.

Content addressing

All lengths are unsigned big-endian integers. u32(n) is four bytes, u64(n) is eight bytes, utf8(value) is the exact UTF-8 encoding, and field(value) is u32(utf8(value).length) followed by those UTF-8 bytes. A hexadecimal hash contributes its decoded 32 bytes.

Each source hash is:

SHA-256(
  field("executablemd.workflow.source.v2") ||
  u64(source byte length) ||
  exact source bytes
)

The bundle hash is:

SHA-256(
  field("executablemd.workflow.bundle.v2") ||
  field(entrypoint) ||
  u32(source count) ||
  for each canonically ordered source:
    field(path) || decoded sourceHash || u64(byteLength) ||
  u32(component count) ||
  for each canonically ordered component:
    field(name) || field(path)
)

The target is deliberately outside the source-bundle hash: selecting a section does not change the bytes in the bundle. targetPath remains part of the complete workflow-definition identity and therefore still distinguishes two runs over the same bundle.

No property values, run ID, host path, Git provenance, retrieval metadata, timestamp or storage encoding enters either hash. Source bytes are hashed and stored without newline, BOM, Unicode or other content normalization.

Exact source retention

Definition v2 storage retains source content as BLOB bytes, never as a database text value or re-encoded JSON string. A newly created Git v1 run keeps live-database PRAGMA user_version = 1; a newly created source-bundle v2 run uses PRAGMA user_version = 2. Initialization selects the schema from the already parsed candidate definition and never upgrades an existing database.

The schema declarations are two immutable, closed inventories. Version 1 is the current OBJECTS map byte-for-byte. Version 2 keeps every version-1 table, index and trigger byte-for-byte except workflow_run, replaces that table with the definition below, and adds exactly the two definition-source tables below. In particular, definition_retrieval remains an optional replaceable-metadata table and every Workspace, journal, session and lifecycle object is unchanged.

CREATE TABLE workflow_run (
  id INTEGER PRIMARY KEY CHECK (id = 1),
  run_id TEXT NOT NULL,
  definition TEXT NOT NULL CHECK (
    json_valid(definition)
    AND json_extract(definition, '$.version') = 2
    AND json_extract(definition, '$.kind') = 'source-bundle'
  ),
  props TEXT NOT NULL CHECK (json_valid(props) AND json_type(props) = 'object'),
  status TEXT NOT NULL CHECK (
    status IN ('running', 'suspended', 'interrupted', 'completed', 'failed', 'cancelled')
  ),
  stop_reason_kind TEXT CHECK (
    stop_reason_kind IS NULL OR stop_reason_kind IN ('host', 'journal')
  ),
  stop_reason_code TEXT,
  stop_reason_event_id TEXT REFERENCES journal_events (event_id),
  created_at TEXT NOT NULL,
  updated_at TEXT NOT NULL,
  CHECK (
    (stop_reason_kind IS NULL AND stop_reason_code IS NULL AND stop_reason_event_id IS NULL)
    OR (stop_reason_kind = 'host' AND stop_reason_code IS NOT NULL AND stop_reason_event_id IS NULL)
    OR (stop_reason_kind = 'journal' AND stop_reason_code IS NULL AND stop_reason_event_id IS NOT NULL)
  )
) STRICT
CREATE TABLE workflow_definition_blob (
  source_hash TEXT PRIMARY KEY CHECK (
    length(source_hash) = 64
    AND source_hash NOT GLOB '*[^0-9a-f]*'
  ),
  byte_length INTEGER NOT NULL CHECK (
    byte_length >= 0 AND byte_length <= 9007199254740991
  ),
  content BLOB NOT NULL CHECK (length(content) = byte_length)
) STRICT, WITHOUT ROWID
CREATE TABLE workflow_definition_source (
  path TEXT PRIMARY KEY CHECK (length(path) > 0),
  source_hash TEXT NOT NULL REFERENCES workflow_definition_blob(source_hash) ON DELETE RESTRICT
) STRICT, WITHOUT ROWID

The descriptor parser, rather than SQLite collation, enforces the complete logical-path grammar and canonical order. The source table contains exactly one row per descriptor entry; multiple paths may reference one blob when their exact bytes are identical. Unreferenced blobs are corruption rather than tolerated garbage.

The stored manifest must equal the descriptor's complete sources array. Every referenced blob must exist, its byte length must equal both retained lengths, and recomputing its source hash must produce its key. Recomputing the bundle hash from the retained manifest and component mapping must produce bundleHash. An extra manifest row, an unreferenced blob, a missing entry, a length disagreement or a hash disagreement is corruption; no reader returns a partial bundle.

Before parsing as Markdown, the entrypoint and declared component sources must decode as well-formed UTF-8 under one strict decoder. Decoding performs no normalization. The BLOB remains authoritative and is what export and later resume preserve.

The public WorkflowRunStorage.create(CreateWorkflowRunRequest) initialization API remains v1-only, with its current exact { runId, definition: GitWorkflowDefinitionV1, base, props } request. Its parser refuses a v2 definition. lookup() recognizes both versions. This prevents a caller holding only a v2 descriptor from creating a database whose authoritative source bytes were never supplied.

V2 creation crosses only the existing trusted, non-contextual lifecycle transition. WorkflowRunCreation becomes this closed union:

interface GitWorkflowRunCreationV1 {
  readonly definition: GitWorkflowDefinitionV1;
  readonly base: string;
  readonly props: JsonObject;
  readonly retrieval?: Json;
}

interface SourceBundleSnapshotEntryV2 {
  readonly path: string;
  readonly bytes: Uint8Array;
}

interface SourceBundleWorkflowRunCreationV2 {
  readonly definition: SourceBundleWorkflowDefinitionV2;
  readonly sourceSnapshot: readonly SourceBundleSnapshotEntryV2[];
  readonly props: JsonObject;
  readonly retrieval?: Json;
}

type WorkflowRunCreation = GitWorkflowRunCreationV1 | SourceBundleWorkflowRunCreationV2;

sourceSnapshot is non-empty, admits no extra entry members, and has exactly the same paths in exactly the same order as definition.sources. Each bytes value is the candidate's exact byte sequence; its byte length and recomputed source hash must equal the corresponding descriptor entry. The transition takes an owned copy of every byte sequence before validation or persistence and retains only those copies, so later mutation of a caller-owned Uint8Array cannot change the run. A mismatch is an invalid creation and writes nothing.

The lifecycle begin, fork and private stageFork paths accept this union. Only their trusted transition implementation can turn the v2 member into storage: in its one initialization transaction it selects schema 2, writes the descriptor, manifest rows and de-duplicated BLOBs, establishes Workspace and lifecycle records, and begins the first execution. The lower-level public storage create() neither accepts nor partially stages v2.

The retained storage record is a closed discriminated union. The v1 member remains the current { runId, definition: GitWorkflowDefinitionV1, base, props, ... } shape. The v2 member is { runId, definition: SourceBundleWorkflowDefinitionV2, props, ... } and does not admit base or pinnedCommit; those are Git v1 fields, not empty or synthetic v2 values. All status, stop-reason and timestamp members represented by ... remain the current members in their current serialized order.

Recognition first validates the application ID, then dispatches only on user_version 1 or 2. Version 1 is accepted only when its schema inventory exactly equals the immutable v1 declaration. Version 2 is accepted only when its inventory exactly equals the immutable v2 declaration above. Version 0 with either declared inventory is corruption, any other version is unsupported, and a hybrid or extra object is corruption. Recognition never repairs, migrates or reinterprets a database. After structural recognition, a v2 reader parses the closed record and performs the full manifest/blob/hash verification before returning a run or attaching lifecycle state.

Version-1 live databases and run records keep their existing schema and exact meaning. Status and history expose the discriminated definition they retained; human output describes a v2 source bundle by its entrypoint and bundle hash instead of printing a fabricated Git base or commit.

Target semantics

The path passed to workflow start locates the input only. An optional authored selector is resolved against the exact candidate entrypoint bytes before storage is created. The descriptor retains only the one exact canonical document target produced by core, without #; a glob, alias or other request spelling is never identity.

Absent targetPath selects the whole entrypoint. Present targetPath must satisfy core's canonical target grammar and must resolve in the retained entrypoint. It participates in run-ID compatibility, the workflow journal binding, root-import admission and artifact verification. Resume uses the retained exact target and never resolves the caller's original selector again.

Start and persistence ordering

For start, the host performs these phases in order:

  1. Read the supplied file once as bytes. Derive its logical entrypoint, validate the path and strict UTF-8, parse the document, resolve the exact target, validate the declared component closure and props, and construct and verify the canonical v2 descriptor. None of this creates run storage or executes authored content.
  2. Acquire the executor lock for the chosen run ID. A compatible existing run is handled under the reuse rules below; its retained source is verified before another document-execution record is begun.
  3. For a new run, one storage transaction retains the v2 run record, complete source manifest and exact blobs, initial Workspace state, first document-execution record and running lifecycle state. The transaction commits all of them or none of them.
  4. Only after that commit may the CLI report workflow run:, import the root or execute authored content. The execution uses the committed snapshot, with its retained logical path and exact target.

Failure or cancellation in phases 1–3 leaves no recognized runnable run and no reported run ID. Repeating start with that ID can create it normally; an empty or private staging file is not a damaged run and is not discoverable by lookup or list. Once phase 3 commits, the run owns a complete definition snapshot. Interruption before the first authored effect is an ordinary interrupted execution that can resume from those retained bytes.

Fork uses the same v2 establishment for its candidate. Its already-required atomic admission retains the candidate descriptor and exact source store together with the inherited prefix, roots, lineage and first execution record. A failed compatibility preflight or failed admission leaves no discoverable destination run.

Run-ID reuse

A v2 start reusing an ID is compatible only when all of these agree:

  • definition version, kind and hashAlgorithm;
  • bundleHash, entrypoint, the complete canonical source manifest and component mapping;
  • exact presence and value of targetPath; and
  • normalized props.

The comparison checks the complete manifest even though the bundle hash commits to it; a hash is not used to excuse malformed or conflicting retained structure. The candidate's host path and optional provenance are ignored. Different bytes, logical paths, component mappings or target selection conflict as definition; different props conflict as props. A compatible reuse executes or replays the retained source, never the newly supplied file buffer.

A v1 run remains compatible only under its existing v1 comparison, including Git object format, object ID, repository-relative path, exact target, declared component records, base and props. V1 and v2 descriptors are never compatible with one another.

Durable and public WorkflowRun

The workflow journal record and the value returned by getWorkflowRun() are one closed union:

interface GitWorkflowRunV1 {
  readonly runId: string;
  readonly base: string;
  readonly pinnedCommit: string;
}

interface SourceBundleWorkflowRunV2 {
  readonly runId: string;
  readonly definitionVersion: 2;
  readonly bundleHash: string;
  readonly targetPath?: string;
}

type WorkflowRun = GitWorkflowRunV1 | SourceBundleWorkflowRunV2;

V1 preserves its exact three-member JSON representation. The ordinary v2 serializer emits runId, definitionVersion, bundleHash, then targetPath when present. Object-member order is not identity: parsing first recognizes the exact v1 member set or exact v2 member set in any order and then validates every value; it never admits another member or fills a synthetic base, pinnedCommit or target. A canonical-JSON container sorts these keys under its existing rule without changing the value.

The retained workflow effect description remains { type: "workflow_run", name: "workflow_run", base } for v1. Its v2 form is { type: "workflow_run", name: "workflow_run", definitionVersion: 2, bundleHash }. The journal record value is the complete union member above. getWorkflowRun() returns that frozen value, retainedWorkflowInstallation() accepts that union, and root document-execution records written by start or fork retain the matching union member. V2 admission cross-checks bundleHash and exact target presence/value against the complete storage descriptor before a document imports.

The existing public workflowInstallation({ base }) entrypoint remains a v1 Git convenience in this story, including its current Git lookup. #822 moves that adapter into the bundled Git Plugin and removes the final package-level Workflow-to-Git import. #443 changes the retained start/resume/fork/export path and the shared storage/journal contracts needed by that extraction; it does not claim that every exported Workflow module is already Git-free.

Legacy Git definitions

GitWorkflowDefinitionV1, its JSON shape, Git-object identity, run record, journal binding and compatible-reuse rules remain byte-for-byte and semantically unchanged. A v1 run is never rewritten as v2 merely because its source was retrieved successfully.

The retained workflow lifecycle parses and retains v1 identity but no longer invokes Git directly to obtain its source. A trusted host may supply this direct, non-contextual dependency:

type LegacyWorkflowSourceReader = (
  definition: GitWorkflowDefinitionV1,
  retrieval: Json | undefined,
) => Operation<Result<RetainedDefinitionSources>>;

The reader is captured by the workflow host before document code runs and is reachable through no Context, contextual API, component, Plugin installation result or authored value. It receives only the parsed v1 descriptor and its replaceable retrieval metadata. The Deno/compiled host initially supplies the existing Git-object reader through this seam; #822 moves that adapter into @executablemd/git without adding a Workflow-to-Git dependency.

Workflow validates the returned root identity, exact target, component set, paths, declared Git blob hashes and Markdown bytes against the v1 descriptor before lifecycle admission. A missing reader, unavailable checkout or object, malformed response, and response that disagrees with the descriptor are distinct failures. None substitutes HEAD, a working-tree file or an empty source, and none mutates the v1 run.

All source availability checks needed to start a v1 resume occur under the executor lock but before stale recovery, a new document-execution record, Workspace attachment, journal replay or root import. A failure therefore leaves the run's lifecycle and journal unchanged. A completed v1 replay, export or fork source uses the same host reader whenever its retained journal or artifact does not already carry an authenticated source closure.

Provenance

Git is optional provenance for v2, not definition identity or source retrieval. A host may retain a credential-free observation such as object format, commit and repository-relative path in the existing replaceable metadata boundary. It may be absent, replaced or become unreachable without changing compatibility or preventing resume.

The supplied absolute source path, checkout path and invocation working directory are never retained in the definition, run identity, journal binding or artifact. A provenance failure cannot fail a v2 start after the source bundle itself was established.

Failure contract

  • Invalid candidate: an unreadable input, invalid logical path, invalid UTF-8, malformed Markdown, unresolved target, invalid component declaration, invalid props, or a v2 lifecycle snapshot whose entries, lengths or hashes disagree with its descriptor refuses before run storage exists.
  • Incompatible reuse: a stored run under the requested ID has a different definition or props. The refusal names only the differing field, never source bytes, props or host paths.
  • Missing v2 source: the descriptor names a manifest entry or blob the live store does not contain. This is WorkflowDefinitionSourceMissingError, leaves the run unchanged and never consults the original file, provenance or legacy reader.
  • Corrupt v2 source: retained paths, lengths, hashes, bytes, component mappings or bundle hash disagree. This is WorkflowDefinitionCorruptError, returns no partial source and leaves the run unchanged.
  • Legacy reader unavailable: a v1 run needs source and its host installed no legacy reader. This is LegacyWorkflowSourceReaderUnavailableError and tells the operator to use a Git-capable XMD host; it creates no execution record.
  • Legacy retrieval unavailable: the installed reader cannot authenticate or read the retained commit and path. This is LegacyWorkflowSourceUnavailableError; it does not fall back to current HEAD or working-tree bytes.
  • Legacy response mismatch: the reader returns a source closure that does not describe the v1 definition. This is LegacyWorkflowSourceMismatchError; no returned bytes execute.
  • Persistence failure or cancellation: failure before the atomic creation commit publishes neither a runnable run nor workflow run:. Failure after the commit follows ordinary lifecycle settlement because the complete definition is already durable.

Stored-source errors do not quote source content, props, retrieval metadata or absolute paths. A malformed retained descriptor or database structure remains a storage-corruption refusal under the existing recognition rules rather than being reclassified as an unavailable external source.

Artifacts and portability

A v2 run remains exportable and inspectable without the original file or Git. It uses artifact format 2 while retaining the current physical SQLite container unchanged: the application ID, tables, indexes and PRAGMA user_version = 1 remain the exact container-schema-v1 declaration. Its header has artifact_version = 2 and container_version = 1; its canonical manifest is { "version": 2, "entries": [...] }; and artifact identity is computed with the domain prefix xmd-artifact\0v2\0. The manifest entry order, canonical-JSON rules and content-digest algorithm otherwise remain the format-1 rules.

Format 2 has this complete closed content-kind inventory:

artifact-frontier
workflow-run
document-execution
fork-lineage
journal-event
journal-record
workspace-root
workspace-root-manifest
dofs-manifest
dofs-manifest-bytes
dofs-blob
dofs-blob-bytes
workspace-repository
workspace-worktree
suspension-answer
agent-session
agent-session-portability
agent-session-bundle-bytes
definition-source-entry
definition-source-content

It admits no format-1 Git definition-source kind. Each descriptor source contributes exactly two entries whose natural identity is the canonical JSON string value of its logical path:

  • definition-source-entry uses canonical-json encoding and contains exactly the members { "path": path, "sourceHash": sourceHash, "byteLength": byteLength }. Canonical JSON writes those keys in its existing lexicographic order (byteLength, path, sourceHash); readers require the exact member set but do not treat object-member order as identity.
  • definition-source-content uses bytes encoding and contains the source's exact BLOB bytes.

The workflow-run entry carries only the v2 definition and v2 WorkflowRun; it admits no base or pinnedCommit. Semantic verification requires exactly one entry/content pair per descriptor path and no undeclared pair, checks both identities against the path, checks the declared and manifest lengths, recomputes every source hash, and recomputes the bundle hash before returning status or history.

Artifact format 1, its xmd-artifact\0v1\0 identity domain and its Git definition closure remain byte-for-byte unchanged and readable. A format-1 artifact admits only the v1 workflow definition; a format-2 artifact admits only the v2 source-bundle definition. The writer selects format 1 for a v1 live run and format 2 for a v2 live run. The reader selects the exact closed inventory, manifest parser, identity domain and semantic verifier from the header version and never interprets one version's closure as the other.

Exporting a v1 live run obtains its closure through the host-supplied legacy reader. Exporting v2 reads only the retained source store. The format-2 closure is sufficient for the separately tracked artifact-backed fork to copy the descriptor and exact bytes into a destination run without making the artifact path retrieval state.

specs/xmd-artifact-spec.md records the unchanged physical container, both artifact versions, both closed inventories, their identity domains and the two definition-closure variants.

Acceptance

  • A Markdown file outside a Git repository starts and executes its exact current bytes.
  • An untracked or modified file inside a repository executes its working-tree bytes rather than the version in HEAD.
  • A root without declared workflow components produces the exact v2 descriptor above with one logical entrypoint and one source; existing explicit component bundles remain closed over their exact bytes through additional canonical entries.
  • Moving, editing or deleting the original file after start does not affect resume, completed replay, export or compatible reuse.
  • Source and bundle hashes use the exact domain-separated encodings specified above, and retained BLOB bytes round-trip without normalization.
  • The snapshot is durably complete before root import or authored execution. A failure or cancellation before the creation commit leaves no recognized run and permits a clean retry of the ID.
  • Whole-document and exact-target runs remain distinct; resume never re-resolves a selector.
  • Compatible reuse executes the retained source and compares the complete v2 definition plus normalized props. V1, v2, changed source, changed logical paths, changed components and changed targets are incompatible as specified.
  • Missing or corrupt v2 source refuses without fallback or partial content and without advancing lifecycle state.
  • Existing v1 live runs and artifacts retain their exact identity and remain readable, resumable and exportable through a host-supplied legacy source reader; Workflow itself performs no Git operation.
  • A missing, unavailable or disagreeing legacy reader fails categorically before lifecycle mutation and never substitutes another source.
  • Optional Git provenance and every absolute host path remain outside authoritative identity and are unnecessary for v2 resume.
  • V2 status, history, fork and artifact surfaces preserve their existing behavior without synthetic Git fields.
  • The CLI retained-definition path establishes, executes, resumes and exports v2 without a Git provider; v1 source retrieval crosses only the host-supplied legacy-reader seam. The existing public workflowInstallation({ base }) v1 convenience remains explicitly assigned to Make Git and GitHub available in XMD runs and workflows #822.
  • A v1 live database keeps exact schema version 1. A v2 live database has exact schema version 2, the declared closed inventory and no base column. Neither is migrated or recognized as the other.
  • Artifact format 1 remains exact for v1; format 2 uses the unchanged physical container, its own manifest and identity domain, the complete closed inventory above and only a v2 source bundle.
  • Journal serialization, getWorkflowRun(), retained installation and fork roots use the exact v1/v2 WorkflowRun union above. Ordinary serializers use the declared presentation order; canonical-JSON storage sorts object keys; readers accept either order but no different member set.
  • Public WorkflowRunStorage.create() remains v1-only and refuses a v2 definition. Trusted begin, fork and stageFork accept the exact WorkflowRunCreation union and atomically retain owned copies of every v2 source byte.
  • architecture.md, specs/workflow-spec.md, the affected workflow sections of specs/executable-mdx-spec.md, specs/workflow-workspace-spec.md and specs/xmd-artifact-spec.md describe the v1/v2 identity, source retention, lifecycle ordering, failures and portability boundary.

Evidence

Focused definition tests cover the closed v2 schema, logical-path grammar, canonical UTF-8 ordering, component mapping, target presence, domain-separated hash vectors, byte lengths and malformed variants.

Lifecycle tests start from an outside-repository file, an untracked file and a modified tracked file; mutate or remove each source after creation; then resume and replay the retained bytes. Cancellation probes each phase before the atomic commit and proves the ID is absent and reusable. Another interrupts immediately after commit and proves the complete snapshot resumes.

Compatibility tests cover identical content reached through another host path, changed bytes, changed logical entrypoint, source order, component closure, target and props. Storage mutation tests remove and alter manifest rows and BLOBs and prove the exact missing/corrupt classifications with no lifecycle transition or fallback read.

Legacy fixtures created by the released v1 implementation resume, replay and export under a Git-capable host reader. Negative controls omit the reader, remove the retained Git object and return a mismatched closure; each leaves the run unchanged. An import-boundary test proves retained lifecycle and source-retrieval modules do not import Git or @executablemd/git; it permits only the existing workflowInstallation({ base }) v1 adapter that #822 removes.

Schema fixtures prove exact recognition of v1 and v2 inventories, refusal of version 0, unsupported versions, hybrids and extra objects, and absence of migration. Public-contract tests cover exact JSON member sets, ordinary serializer presentation and canonical-JSON ordering for both WorkflowRun variants through journal replay, getWorkflowRun(), retained installation and fork roots; reordered object members parse to the same value while reordered source/component arrays are refused.

Creation-boundary tests prove public storage creation still admits only v1, while trusted v2 begin, fork and private staging require a complete canonical sourceSnapshot, copy caller-owned buffers, reject missing, extra, reordered or mismatched entries before mutation, and commit the descriptor and exact BLOBs with the first execution.

Artifact export and inspection round trips cover v1 and v2 independently, including header/manifest version selection, both identity domains, the closed format-2 inventory and v2 exact-byte preservation after the original source disappears.

Dependencies and related work

Out of scope

  • Automatic discovery of undeclared dependency closure beyond the existing explicit workflow.components bundle.
  • Treating provenance as identity or using it to reconstruct missing v2 source.
  • In-place conversion of a v1 live run or artifact to v2.
  • Delivering the artifact-backed history fork; this story only keeps its authenticated source input representable.
  • Standard-input workflow definitions, URL definitions or package-specifier definitions.
  • Changing ordinary xmd run source behavior.
  • Moving or removing the public Git-backed workflowInstallation({ base }) convenience; Make Git and GitHub available in XMD runs and workflows #822 owns that extraction.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    documentsExecutable documents, authored workflows, and reader-facing document behaviorenhancementNew feature or request

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions