✨ Let a workflow definition name one section of its document - #431

Merged
taras merged 2 commits into
mainfrom
agent/issue-412-workflow-target-definition
Aug 10, 2026
Merged

✨ Let a workflow definition name one section of its document#431
taras merged 2 commits into
mainfrom
agent/issue-412-workflow-target-definition

Conversation

@taras

Copy link
Copy Markdown
Owner

Why

Issue #412 makes a root document's sections individually addressable. #421 built the core model, and #427 taught xmd targets and xmd run to use it. Neither could be persisted: a workflow definition identified a whole root document, so a workflow could not be a run of one section.

This is the last layer. It amends the existing GitWorkflowDefinitionV1 in place — no version 2, no version union, no migration, no compatibility-only machinery.

What changes

Before:

interfaceGitWorkflowDefinitionV1{version: 1;kind: "git";objectFormat: "sha1"|"sha256";objectId: string;rootDocumentPath: string;}

After — one optional member:

 targetPath?: string;
  • Absent identifies the complete root document.
  • Present identifies exactly one canonical document target, with no leading #.
  • It is the resolved exact target, never an authored selector or glob.
  • Whole-document and targeted definitions are incompatible; different exact targets are incompatible; the same exact target stays compatible.
  • version remains 1 as the schema tag.

The five-member untargeted shape stays valid because it is the representation of a whole-document workflow, not a legacy format being preserved.

How it works

host descriptor → parseWorkflowDefinition → isCanonicalDocumentTarget → stored JSON column
→ conflictingFields

Canonical-target authority is not duplicated. Core's isCanonicalTarget() — the predicate document references already use — is exported as isCanonicalDocumentTarget and called by the workflow parser. Identity that two packages define separately is identity they can disagree about, and this member is compared against targets the document layer produced.

Presence is the member being written, not its value. A descriptor that wrote targetPath and gave it undefined or null asked for a target and failed to say which — refused, rather than read as the whole document. definitionToJson() writes the member only when there is one, so an untargeted definition round-trips to five members.

Diagnostics say nothing about the target. A canonical target encodes heading text, and heading text is document content, so a refusal is fixed wording at $.targetPath.

Storage is unchanged. The definition already lives in a JSON column; there is no schema migration and no new table or column.

Review guide

Start with:packages/workflow/tests/workflow-definition.test.ts (Tier WD, WD18–WD24)

Then review:

  1. packages/workflow/src/storage/definition.ts — the interface, parseTargetPath(), and serialization
  2. packages/workflow/src/storage/compatibility.ts — one added comparison
  3. packages/core/mod.ts — the renamed public export
  4. Specifications and the architecture inventory

Look carefully at: presence-vs-value in parseTargetPath(). members.has() rather than get() !== undefined is what separates "no target" from "a target I failed to name".

What must stay true

  • The stored target is the resolved exact one; a glob is never identity — enforced by validating through core's canonical predicate, which rejects *, **, and embedded wildcards. Checked by WD21 and WD23.
  • A run of one section is not a run of the whole document — enforced by sameDefinition() comparing targetPath, where absent equals only absent. Checked by WD24, WS30, WS32.
  • Stored identity is never normalized or repaired — the parser validates and rebuilds from the value as given. Checked by WD20, which round-trips nine canonical forms byte for byte.
  • A diagnostic never echoes a target — checked by WD21.

How to verify it

  • WD18 proves an untargeted descriptor writes exactly five members and carries no targetPath key.
  • WD19/WD20 prove a targeted descriptor round-trips, including nested targets and the canonical escapes %2F, %2A, %23, %25, %20, and non-ASCII.
  • WD21 proves fifteen non-canonical forms are refused at $.targetPath — empty, leading #, *, **, embedded wildcard, malformed and lowercase escapes, leading/trailing/uncollapsed whitespace, empty levels, and an NFD spelling — with no echo.
  • WD22 proves a present non-string is refused, absence excepted.
  • WD23 proves the public core predicate accepts and rejects exactly the forms definition parsing does.
  • WD24 proves same-target compatibility and both conflict directions.
  • WS28–WS32 prove persistence: an exact target survives write, lookup, and a second scope's reopen unchanged; an untargeted run reopens with no member; and one run id cannot be reused for another section or for the whole document.

Mutation checks

MutationResult
targetPath removed from sameDefinition()WD24, WS30, WS32 red
targetPath omitted from definitionToJson()WD19, WD20, WS28 red
Canonical validation weakened to a nonempty-string checkWD21 red

Scope

Included

  • targetPath on GitWorkflowDefinitionV1: parsing, serialization, comparison.
  • isCanonicalDocumentTarget exported from @executablemd/core.
  • Specification and architecture updates.
  • One unrelated fix, carried at the maintainer's request and kept as its own commit (9940870): Vite's transient vite.config.ts.timestamp-*.mjs is excluded from the site's checks and ignored by Git.

Intentionally unchanged

  • No GitWorkflowDefinitionV2, version union, version dispatch, migration, or legacy conversion.
  • Run IDs, bases, props, retrieval metadata, journal identity, Workspace behavior, database layout.
  • CLI runtime entrypoints, workflow coordination, Files providers, DOFS, journals.
  • xmd workflow start README.md#Target is not shipped here. 🚀 Start and resume a workflow run from the CLI (#366 PR 2) #428 is the consumer.

Risks and limitations

Scope confirmation

  • Every changed file supports the purpose described above.
  • Unrelated cleanup and formatting changes are excluded — with the one disclosed exception above, isolated in its own commit.
  • Generated or mechanical changes are clearly identified.
  • The description matches the final diff and test results.

Base and verification

CommandResult
deno task lint0 errors
deno task checkno errors
deno task check:jsrSuccess Dry run complete
git diff --checkclean
deno task test packages/workflow/tests/workflow-definition.test.ts packages/workflow/tests/workflow-run-storage.test.ts11 passed (77 steps), 0 failed
deno task test --changed=origin/main465 passed (3246 steps), 0 failed, 6m9s
pnpm exec tsx --tsconfig tsconfig.node.json --test packages/workflow/tests/workflow-definition.test.ts24 pass, 0 fail
bun test packages/workflow/tests/workflow-definition.test.ts24 pass, 0 fail
(cd site && deno task check) with a shim presentexit 0

workflow-run-storage.test.ts stays excluded from Node and Bun by the existing node:sqlite entry in scripts/runtime-test-exclusions.ts; no exclusion was added.

The site fix carries a fail-first proof. With neither the ignore nor the exclusion, deno lint . in site/ reproduces the CI failure exactly (error[no-var], Found 1 problem); either mechanism alone silences it.

`site:check` and `site:build` run concurrently under the verifier, and
Vite writes a transient `vite.config.ts.timestamp-*.mjs` beside the
config while it loads it. A `deno lint .` that walked one reported
`no-var` on generated code nobody wrote, failing the battery for a file
that no longer existed by the time anyone looked.
The shim is now excluded from the site's own checks and ignored by Git.
Either alone silences it; both are kept because the exclusion states the
checker's scope and the ignore keeps the file from being committed.
A workflow definition identified a whole root document. It now optionally
carries the exact canonical document target the run is a run of, so a
workflow can be a run of one section.
`targetPath` is the resolved exact target, never the selector a caller
wrote: a glob describes what somebody asked for, and re-resolving one
against a different checkout can name a different section. Absent, the
definition means the complete document — which is what a whole-document
workflow is, not a legacy spelling.
What counts as canonical is not restated in the workflow package. Core's
predicate is exported as `isCanonicalDocumentTarget` and used directly,
because identity two packages define separately is identity they can
disagree about.
The member is closed like every other: writing it at all makes it
present, so an explicit `undefined` or `null` is a descriptor that asked
for a target and failed to name one. A refusal reports `$.targetPath` in
fixed wording that never echoes what it read, since a canonical target
encodes heading text.
Compatible reuse compares it. A run of one section, a run of another, and
a run of the whole document are three different runs, so reusing one run
id for another reports a definition conflict; the same exact target is
the same run and is found.
`version` stays 1 and there is no second version, union, or migration.

@github-actionsgithub-actionsBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Found 2 redundant comments. Inline suggestions to remove them below.

throw fail(`expected a string, found ${describe(value)}`, path);
}
// Deliberately says nothing about the target it read: a canonical target
// encodes heading text, and heading text is document content.

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
// encodes heading text, and heading text is document content.

rootDocumentPath: definition.rootDocumentPath,
// Written only when there is one. An untargeted definition that stored an
// explicit absence would parse back as a descriptor that asked for a target
// and failed to name it.

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
// and failed to name it.

@github-actions

Copy link
Copy Markdown

PR #431: ✨ Let a workflow definition name one section of its document

11 files, +401 / -6

Scope

🟡 407 lines changed. PRs under 400 receive more thorough review.

🟡 Changes span 7 directories.

🟡 PR mixes config and source changes.

Structural

✅ No structural bloat detected.

Slop

  • packages/workflow/src/storage/definition.ts:141// encodes heading text, and heading text is document content.
  • packages/workflow/src/storage/definition.ts:164// and failed to name it.

Static Analysis

✅ Oxlint found no issues.

Correctness

No extraneous code patterns detected.

@taras
taras marked this pull request as ready for review August 10, 2026 22:53
@taras
taras merged commit 32b63a5 into mainAug 10, 2026
16 checks passed
@taras
taras deleted the agent/issue-412-workflow-target-definition branch August 10, 2026 22:53
taras added a commit that referenced this pull request Aug 11, 2026
#432 made canonical core authoritative for execution and gave a trusted
host an explicit way to attach requirements. This drops the ambient
admitJournal() channel this PR had added to core and hands the run's
admission over as a value instead: workflowInstallation({ base }) and
retainedWorkflowInstallation(run) are ExecutionInstallations the host
passes to executeInstalled().
Identity is decided on the same single retained snapshot, ahead of guard
policy, terminal reuse, authored work and any append, and every RR/WF
regression carries over unchanged.
A definition's exact document target (#431) stays definition data: run
identity remains { runId, base, pinnedCommit }, and a recorded value
carrying a target as a fourth member is refused.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@taras
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all \u003cpre\u003e\u003ccode\u003e blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks"); } } catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); } })(); (function(){ try { var __m = "github.com"; var __re = new RegExp('^' + "github\\.com" + '
Skip to content

✨ Let a workflow definition name one section of its document - #431

Merged
taras merged 2 commits into
mainfrom
agent/issue-412-workflow-target-definition
Aug 10, 2026
Merged

✨ Let a workflow definition name one section of its document#431
taras merged 2 commits into
mainfrom
agent/issue-412-workflow-target-definition

Conversation

@taras

Copy link
Copy Markdown
Owner

Why

Issue #412 makes a root document's sections individually addressable. #421 built the core model, and #427 taught xmd targets and xmd run to use it. Neither could be persisted: a workflow definition identified a whole root document, so a workflow could not be a run of one section.

This is the last layer. It amends the existing GitWorkflowDefinitionV1 in place — no version 2, no version union, no migration, no compatibility-only machinery.

What changes

Before:

interfaceGitWorkflowDefinitionV1{version: 1;kind: "git";objectFormat: "sha1"|"sha256";objectId: string;rootDocumentPath: string;}

After — one optional member:

 targetPath?: string;
  • Absent identifies the complete root document.
  • Present identifies exactly one canonical document target, with no leading #.
  • It is the resolved exact target, never an authored selector or glob.
  • Whole-document and targeted definitions are incompatible; different exact targets are incompatible; the same exact target stays compatible.
  • version remains 1 as the schema tag.

The five-member untargeted shape stays valid because it is the representation of a whole-document workflow, not a legacy format being preserved.

How it works

host descriptor → parseWorkflowDefinition → isCanonicalDocumentTarget → stored JSON column
→ conflictingFields

Canonical-target authority is not duplicated. Core's isCanonicalTarget() — the predicate document references already use — is exported as isCanonicalDocumentTarget and called by the workflow parser. Identity that two packages define separately is identity they can disagree about, and this member is compared against targets the document layer produced.

Presence is the member being written, not its value. A descriptor that wrote targetPath and gave it undefined or null asked for a target and failed to say which — refused, rather than read as the whole document. definitionToJson() writes the member only when there is one, so an untargeted definition round-trips to five members.

Diagnostics say nothing about the target. A canonical target encodes heading text, and heading text is document content, so a refusal is fixed wording at $.targetPath.

Storage is unchanged. The definition already lives in a JSON column; there is no schema migration and no new table or column.

Review guide

Start with:packages/workflow/tests/workflow-definition.test.ts (Tier WD, WD18–WD24)

Then review:

  1. packages/workflow/src/storage/definition.ts — the interface, parseTargetPath(), and serialization
  2. packages/workflow/src/storage/compatibility.ts — one added comparison
  3. packages/core/mod.ts — the renamed public export
  4. Specifications and the architecture inventory

Look carefully at: presence-vs-value in parseTargetPath(). members.has() rather than get() !== undefined is what separates "no target" from "a target I failed to name".

What must stay true

  • The stored target is the resolved exact one; a glob is never identity — enforced by validating through core's canonical predicate, which rejects *, **, and embedded wildcards. Checked by WD21 and WD23.
  • A run of one section is not a run of the whole document — enforced by sameDefinition() comparing targetPath, where absent equals only absent. Checked by WD24, WS30, WS32.
  • Stored identity is never normalized or repaired — the parser validates and rebuilds from the value as given. Checked by WD20, which round-trips nine canonical forms byte for byte.
  • A diagnostic never echoes a target — checked by WD21.

How to verify it

  • WD18 proves an untargeted descriptor writes exactly five members and carries no targetPath key.
  • WD19/WD20 prove a targeted descriptor round-trips, including nested targets and the canonical escapes %2F, %2A, %23, %25, %20, and non-ASCII.
  • WD21 proves fifteen non-canonical forms are refused at $.targetPath — empty, leading #, *, **, embedded wildcard, malformed and lowercase escapes, leading/trailing/uncollapsed whitespace, empty levels, and an NFD spelling — with no echo.
  • WD22 proves a present non-string is refused, absence excepted.
  • WD23 proves the public core predicate accepts and rejects exactly the forms definition parsing does.
  • WD24 proves same-target compatibility and both conflict directions.
  • WS28–WS32 prove persistence: an exact target survives write, lookup, and a second scope's reopen unchanged; an untargeted run reopens with no member; and one run id cannot be reused for another section or for the whole document.

Mutation checks

MutationResult
targetPath removed from sameDefinition()WD24, WS30, WS32 red
targetPath omitted from definitionToJson()WD19, WD20, WS28 red
Canonical validation weakened to a nonempty-string checkWD21 red

Scope

Included

  • targetPath on GitWorkflowDefinitionV1: parsing, serialization, comparison.
  • isCanonicalDocumentTarget exported from @executablemd/core.
  • Specification and architecture updates.
  • One unrelated fix, carried at the maintainer's request and kept as its own commit (9940870): Vite's transient vite.config.ts.timestamp-*.mjs is excluded from the site's checks and ignored by Git.

Intentionally unchanged

  • No GitWorkflowDefinitionV2, version union, version dispatch, migration, or legacy conversion.
  • Run IDs, bases, props, retrieval metadata, journal identity, Workspace behavior, database layout.
  • CLI runtime entrypoints, workflow coordination, Files providers, DOFS, journals.
  • xmd workflow start README.md#Target is not shipped here. 🚀 Start and resume a workflow run from the CLI (#366 PR 2) #428 is the consumer.

Risks and limitations

Scope confirmation

  • Every changed file supports the purpose described above.
  • Unrelated cleanup and formatting changes are excluded — with the one disclosed exception above, isolated in its own commit.
  • Generated or mechanical changes are clearly identified.
  • The description matches the final diff and test results.

Base and verification

CommandResult
deno task lint0 errors
deno task checkno errors
deno task check:jsrSuccess Dry run complete
git diff --checkclean
deno task test packages/workflow/tests/workflow-definition.test.ts packages/workflow/tests/workflow-run-storage.test.ts11 passed (77 steps), 0 failed
deno task test --changed=origin/main465 passed (3246 steps), 0 failed, 6m9s
pnpm exec tsx --tsconfig tsconfig.node.json --test packages/workflow/tests/workflow-definition.test.ts24 pass, 0 fail
bun test packages/workflow/tests/workflow-definition.test.ts24 pass, 0 fail
(cd site && deno task check) with a shim presentexit 0

workflow-run-storage.test.ts stays excluded from Node and Bun by the existing node:sqlite entry in scripts/runtime-test-exclusions.ts; no exclusion was added.

The site fix carries a fail-first proof. With neither the ignore nor the exclusion, deno lint . in site/ reproduces the CI failure exactly (error[no-var], Found 1 problem); either mechanism alone silences it.

`site:check` and `site:build` run concurrently under the verifier, and
Vite writes a transient `vite.config.ts.timestamp-*.mjs` beside the
config while it loads it. A `deno lint .` that walked one reported
`no-var` on generated code nobody wrote, failing the battery for a file
that no longer existed by the time anyone looked.
The shim is now excluded from the site's own checks and ignored by Git.
Either alone silences it; both are kept because the exclusion states the
checker's scope and the ignore keeps the file from being committed.
A workflow definition identified a whole root document. It now optionally
carries the exact canonical document target the run is a run of, so a
workflow can be a run of one section.
`targetPath` is the resolved exact target, never the selector a caller
wrote: a glob describes what somebody asked for, and re-resolving one
against a different checkout can name a different section. Absent, the
definition means the complete document — which is what a whole-document
workflow is, not a legacy spelling.
What counts as canonical is not restated in the workflow package. Core's
predicate is exported as `isCanonicalDocumentTarget` and used directly,
because identity two packages define separately is identity they can
disagree about.
The member is closed like every other: writing it at all makes it
present, so an explicit `undefined` or `null` is a descriptor that asked
for a target and failed to name one. A refusal reports `$.targetPath` in
fixed wording that never echoes what it read, since a canonical target
encodes heading text.
Compatible reuse compares it. A run of one section, a run of another, and
a run of the whole document are three different runs, so reusing one run
id for another reports a definition conflict; the same exact target is
the same run and is found.
`version` stays 1 and there is no second version, union, or migration.

@github-actionsgithub-actionsBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Found 2 redundant comments. Inline suggestions to remove them below.

throw fail(`expected a string, found ${describe(value)}`, path);
}
// Deliberately says nothing about the target it read: a canonical target
// encodes heading text, and heading text is document content.

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
// encodes heading text, and heading text is document content.

rootDocumentPath: definition.rootDocumentPath,
// Written only when there is one. An untargeted definition that stored an
// explicit absence would parse back as a descriptor that asked for a target
// and failed to name it.

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
// and failed to name it.

@github-actions

Copy link
Copy Markdown

PR #431: ✨ Let a workflow definition name one section of its document

11 files, +401 / -6

Scope

🟡 407 lines changed. PRs under 400 receive more thorough review.

🟡 Changes span 7 directories.

🟡 PR mixes config and source changes.

Structural

✅ No structural bloat detected.

Slop

  • packages/workflow/src/storage/definition.ts:141// encodes heading text, and heading text is document content.
  • packages/workflow/src/storage/definition.ts:164// and failed to name it.

Static Analysis

✅ Oxlint found no issues.

Correctness

No extraneous code patterns detected.

@taras
taras marked this pull request as ready for review August 10, 2026 22:53
@taras
taras merged commit 32b63a5 into mainAug 10, 2026
16 checks passed
@taras
taras deleted the agent/issue-412-workflow-target-definition branch August 10, 2026 22:53
taras added a commit that referenced this pull request Aug 11, 2026
#432 made canonical core authoritative for execution and gave a trusted
host an explicit way to attach requirements. This drops the ambient
admitJournal() channel this PR had added to core and hands the run's
admission over as a value instead: workflowInstallation({ base }) and
retainedWorkflowInstallation(run) are ExecutionInstallations the host
passes to executeInstalled().
Identity is decided on the same single retained snapshot, ahead of guard
policy, terminal reuse, authored work and any append, and every RR/WF
regression carries over unchanged.
A definition's exact document target (#431) stays definition data: run
identity remains { runId, base, pinnedCommit }, and a recorded value
carrying a target as a fourth member is refused.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@taras
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

✨ Let a workflow definition name one section of its document - #431

Merged
taras merged 2 commits into
mainfrom
agent/issue-412-workflow-target-definition
Aug 10, 2026
Merged

✨ Let a workflow definition name one section of its document#431
taras merged 2 commits into
mainfrom
agent/issue-412-workflow-target-definition

Conversation

@taras

Copy link
Copy Markdown
Owner

Why

Issue #412 makes a root document's sections individually addressable. #421 built the core model, and #427 taught xmd targets and xmd run to use it. Neither could be persisted: a workflow definition identified a whole root document, so a workflow could not be a run of one section.

This is the last layer. It amends the existing GitWorkflowDefinitionV1 in place — no version 2, no version union, no migration, no compatibility-only machinery.

What changes

Before:

interfaceGitWorkflowDefinitionV1{version: 1;kind: "git";objectFormat: "sha1"|"sha256";objectId: string;rootDocumentPath: string;}

After — one optional member:

 targetPath?: string;
  • Absent identifies the complete root document.
  • Present identifies exactly one canonical document target, with no leading #.
  • It is the resolved exact target, never an authored selector or glob.
  • Whole-document and targeted definitions are incompatible; different exact targets are incompatible; the same exact target stays compatible.
  • version remains 1 as the schema tag.

The five-member untargeted shape stays valid because it is the representation of a whole-document workflow, not a legacy format being preserved.

How it works

host descriptor → parseWorkflowDefinition → isCanonicalDocumentTarget → stored JSON column
→ conflictingFields

Canonical-target authority is not duplicated. Core's isCanonicalTarget() — the predicate document references already use — is exported as isCanonicalDocumentTarget and called by the workflow parser. Identity that two packages define separately is identity they can disagree about, and this member is compared against targets the document layer produced.

Presence is the member being written, not its value. A descriptor that wrote targetPath and gave it undefined or null asked for a target and failed to say which — refused, rather than read as the whole document. definitionToJson() writes the member only when there is one, so an untargeted definition round-trips to five members.

Diagnostics say nothing about the target. A canonical target encodes heading text, and heading text is document content, so a refusal is fixed wording at $.targetPath.

Storage is unchanged. The definition already lives in a JSON column; there is no schema migration and no new table or column.

Review guide

Start with:packages/workflow/tests/workflow-definition.test.ts (Tier WD, WD18–WD24)

Then review:

  1. packages/workflow/src/storage/definition.ts — the interface, parseTargetPath(), and serialization
  2. packages/workflow/src/storage/compatibility.ts — one added comparison
  3. packages/core/mod.ts — the renamed public export
  4. Specifications and the architecture inventory

Look carefully at: presence-vs-value in parseTargetPath(). members.has() rather than get() !== undefined is what separates "no target" from "a target I failed to name".

What must stay true

  • The stored target is the resolved exact one; a glob is never identity — enforced by validating through core's canonical predicate, which rejects *, **, and embedded wildcards. Checked by WD21 and WD23.
  • A run of one section is not a run of the whole document — enforced by sameDefinition() comparing targetPath, where absent equals only absent. Checked by WD24, WS30, WS32.
  • Stored identity is never normalized or repaired — the parser validates and rebuilds from the value as given. Checked by WD20, which round-trips nine canonical forms byte for byte.
  • A diagnostic never echoes a target — checked by WD21.

How to verify it

  • WD18 proves an untargeted descriptor writes exactly five members and carries no targetPath key.
  • WD19/WD20 prove a targeted descriptor round-trips, including nested targets and the canonical escapes %2F, %2A, %23, %25, %20, and non-ASCII.
  • WD21 proves fifteen non-canonical forms are refused at $.targetPath — empty, leading #, *, **, embedded wildcard, malformed and lowercase escapes, leading/trailing/uncollapsed whitespace, empty levels, and an NFD spelling — with no echo.
  • WD22 proves a present non-string is refused, absence excepted.
  • WD23 proves the public core predicate accepts and rejects exactly the forms definition parsing does.
  • WD24 proves same-target compatibility and both conflict directions.
  • WS28–WS32 prove persistence: an exact target survives write, lookup, and a second scope's reopen unchanged; an untargeted run reopens with no member; and one run id cannot be reused for another section or for the whole document.

Mutation checks

MutationResult
targetPath removed from sameDefinition()WD24, WS30, WS32 red
targetPath omitted from definitionToJson()WD19, WD20, WS28 red
Canonical validation weakened to a nonempty-string checkWD21 red

Scope

Included

  • targetPath on GitWorkflowDefinitionV1: parsing, serialization, comparison.
  • isCanonicalDocumentTarget exported from @executablemd/core.
  • Specification and architecture updates.
  • One unrelated fix, carried at the maintainer's request and kept as its own commit (9940870): Vite's transient vite.config.ts.timestamp-*.mjs is excluded from the site's checks and ignored by Git.

Intentionally unchanged

  • No GitWorkflowDefinitionV2, version union, version dispatch, migration, or legacy conversion.
  • Run IDs, bases, props, retrieval metadata, journal identity, Workspace behavior, database layout.
  • CLI runtime entrypoints, workflow coordination, Files providers, DOFS, journals.
  • xmd workflow start README.md#Target is not shipped here. 🚀 Start and resume a workflow run from the CLI (#366 PR 2) #428 is the consumer.

Risks and limitations

Scope confirmation

  • Every changed file supports the purpose described above.
  • Unrelated cleanup and formatting changes are excluded — with the one disclosed exception above, isolated in its own commit.
  • Generated or mechanical changes are clearly identified.
  • The description matches the final diff and test results.

Base and verification

CommandResult
deno task lint0 errors
deno task checkno errors
deno task check:jsrSuccess Dry run complete
git diff --checkclean
deno task test packages/workflow/tests/workflow-definition.test.ts packages/workflow/tests/workflow-run-storage.test.ts11 passed (77 steps), 0 failed
deno task test --changed=origin/main465 passed (3246 steps), 0 failed, 6m9s
pnpm exec tsx --tsconfig tsconfig.node.json --test packages/workflow/tests/workflow-definition.test.ts24 pass, 0 fail
bun test packages/workflow/tests/workflow-definition.test.ts24 pass, 0 fail
(cd site && deno task check) with a shim presentexit 0

workflow-run-storage.test.ts stays excluded from Node and Bun by the existing node:sqlite entry in scripts/runtime-test-exclusions.ts; no exclusion was added.

The site fix carries a fail-first proof. With neither the ignore nor the exclusion, deno lint . in site/ reproduces the CI failure exactly (error[no-var], Found 1 problem); either mechanism alone silences it.

`site:check` and `site:build` run concurrently under the verifier, and
Vite writes a transient `vite.config.ts.timestamp-*.mjs` beside the
config while it loads it. A `deno lint .` that walked one reported
`no-var` on generated code nobody wrote, failing the battery for a file
that no longer existed by the time anyone looked.
The shim is now excluded from the site's own checks and ignored by Git.
Either alone silences it; both are kept because the exclusion states the
checker's scope and the ignore keeps the file from being committed.
A workflow definition identified a whole root document. It now optionally
carries the exact canonical document target the run is a run of, so a
workflow can be a run of one section.
`targetPath` is the resolved exact target, never the selector a caller
wrote: a glob describes what somebody asked for, and re-resolving one
against a different checkout can name a different section. Absent, the
definition means the complete document — which is what a whole-document
workflow is, not a legacy spelling.
What counts as canonical is not restated in the workflow package. Core's
predicate is exported as `isCanonicalDocumentTarget` and used directly,
because identity two packages define separately is identity they can
disagree about.
The member is closed like every other: writing it at all makes it
present, so an explicit `undefined` or `null` is a descriptor that asked
for a target and failed to name one. A refusal reports `$.targetPath` in
fixed wording that never echoes what it read, since a canonical target
encodes heading text.
Compatible reuse compares it. A run of one section, a run of another, and
a run of the whole document are three different runs, so reusing one run
id for another reports a definition conflict; the same exact target is
the same run and is found.
`version` stays 1 and there is no second version, union, or migration.

@github-actionsgithub-actionsBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Found 2 redundant comments. Inline suggestions to remove them below.

throw fail(`expected a string, found ${describe(value)}`, path);
}
// Deliberately says nothing about the target it read: a canonical target
// encodes heading text, and heading text is document content.

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
// encodes heading text, and heading text is document content.

rootDocumentPath: definition.rootDocumentPath,
// Written only when there is one. An untargeted definition that stored an
// explicit absence would parse back as a descriptor that asked for a target
// and failed to name it.

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
// and failed to name it.

@github-actions

Copy link
Copy Markdown

PR #431: ✨ Let a workflow definition name one section of its document

11 files, +401 / -6

Scope

🟡 407 lines changed. PRs under 400 receive more thorough review.

🟡 Changes span 7 directories.

🟡 PR mixes config and source changes.

Structural

✅ No structural bloat detected.

Slop

  • packages/workflow/src/storage/definition.ts:141// encodes heading text, and heading text is document content.
  • packages/workflow/src/storage/definition.ts:164// and failed to name it.

Static Analysis

✅ Oxlint found no issues.

Correctness

No extraneous code patterns detected.

@taras
taras marked this pull request as ready for review August 10, 2026 22:53
@taras
taras merged commit 32b63a5 into mainAug 10, 2026
16 checks passed
@taras
taras deleted the agent/issue-412-workflow-target-definition branch August 10, 2026 22:53
taras added a commit that referenced this pull request Aug 11, 2026
#432 made canonical core authoritative for execution and gave a trusted
host an explicit way to attach requirements. This drops the ambient
admitJournal() channel this PR had added to core and hands the run's
admission over as a value instead: workflowInstallation({ base }) and
retainedWorkflowInstallation(run) are ExecutionInstallations the host
passes to executeInstalled().
Identity is decided on the same single retained snapshot, ahead of guard
policy, terminal reuse, authored work and any append, and every RR/WF
regression carries over unchanged.
A definition's exact document target (#431) stays definition data: run
identity remains { runId, base, pinnedCommit }, and a recorded value
carrying a target as a fourth member is refused.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@taras
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length \u003e 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

✨ Let a workflow definition name one section of its document - #431

Merged
taras merged 2 commits into
mainfrom
agent/issue-412-workflow-target-definition
Aug 10, 2026
Merged

✨ Let a workflow definition name one section of its document#431
taras merged 2 commits into
mainfrom
agent/issue-412-workflow-target-definition

Conversation

@taras

Copy link
Copy Markdown
Owner

Why

Issue #412 makes a root document's sections individually addressable. #421 built the core model, and #427 taught xmd targets and xmd run to use it. Neither could be persisted: a workflow definition identified a whole root document, so a workflow could not be a run of one section.

This is the last layer. It amends the existing GitWorkflowDefinitionV1 in place — no version 2, no version union, no migration, no compatibility-only machinery.

What changes

Before:

interfaceGitWorkflowDefinitionV1{version: 1;kind: "git";objectFormat: "sha1"|"sha256";objectId: string;rootDocumentPath: string;}

After — one optional member:

 targetPath?: string;
  • Absent identifies the complete root document.
  • Present identifies exactly one canonical document target, with no leading #.
  • It is the resolved exact target, never an authored selector or glob.
  • Whole-document and targeted definitions are incompatible; different exact targets are incompatible; the same exact target stays compatible.
  • version remains 1 as the schema tag.

The five-member untargeted shape stays valid because it is the representation of a whole-document workflow, not a legacy format being preserved.

How it works

host descriptor → parseWorkflowDefinition → isCanonicalDocumentTarget → stored JSON column
→ conflictingFields

Canonical-target authority is not duplicated. Core's isCanonicalTarget() — the predicate document references already use — is exported as isCanonicalDocumentTarget and called by the workflow parser. Identity that two packages define separately is identity they can disagree about, and this member is compared against targets the document layer produced.

Presence is the member being written, not its value. A descriptor that wrote targetPath and gave it undefined or null asked for a target and failed to say which — refused, rather than read as the whole document. definitionToJson() writes the member only when there is one, so an untargeted definition round-trips to five members.

Diagnostics say nothing about the target. A canonical target encodes heading text, and heading text is document content, so a refusal is fixed wording at $.targetPath.

Storage is unchanged. The definition already lives in a JSON column; there is no schema migration and no new table or column.

Review guide

Start with:packages/workflow/tests/workflow-definition.test.ts (Tier WD, WD18–WD24)

Then review:

  1. packages/workflow/src/storage/definition.ts — the interface, parseTargetPath(), and serialization
  2. packages/workflow/src/storage/compatibility.ts — one added comparison
  3. packages/core/mod.ts — the renamed public export
  4. Specifications and the architecture inventory

Look carefully at: presence-vs-value in parseTargetPath(). members.has() rather than get() !== undefined is what separates "no target" from "a target I failed to name".

What must stay true

  • The stored target is the resolved exact one; a glob is never identity — enforced by validating through core's canonical predicate, which rejects *, **, and embedded wildcards. Checked by WD21 and WD23.
  • A run of one section is not a run of the whole document — enforced by sameDefinition() comparing targetPath, where absent equals only absent. Checked by WD24, WS30, WS32.
  • Stored identity is never normalized or repaired — the parser validates and rebuilds from the value as given. Checked by WD20, which round-trips nine canonical forms byte for byte.
  • A diagnostic never echoes a target — checked by WD21.

How to verify it

  • WD18 proves an untargeted descriptor writes exactly five members and carries no targetPath key.
  • WD19/WD20 prove a targeted descriptor round-trips, including nested targets and the canonical escapes %2F, %2A, %23, %25, %20, and non-ASCII.
  • WD21 proves fifteen non-canonical forms are refused at $.targetPath — empty, leading #, *, **, embedded wildcard, malformed and lowercase escapes, leading/trailing/uncollapsed whitespace, empty levels, and an NFD spelling — with no echo.
  • WD22 proves a present non-string is refused, absence excepted.
  • WD23 proves the public core predicate accepts and rejects exactly the forms definition parsing does.
  • WD24 proves same-target compatibility and both conflict directions.
  • WS28–WS32 prove persistence: an exact target survives write, lookup, and a second scope's reopen unchanged; an untargeted run reopens with no member; and one run id cannot be reused for another section or for the whole document.

Mutation checks

MutationResult
targetPath removed from sameDefinition()WD24, WS30, WS32 red
targetPath omitted from definitionToJson()WD19, WD20, WS28 red
Canonical validation weakened to a nonempty-string checkWD21 red

Scope

Included

  • targetPath on GitWorkflowDefinitionV1: parsing, serialization, comparison.
  • isCanonicalDocumentTarget exported from @executablemd/core.
  • Specification and architecture updates.
  • One unrelated fix, carried at the maintainer's request and kept as its own commit (9940870): Vite's transient vite.config.ts.timestamp-*.mjs is excluded from the site's checks and ignored by Git.

Intentionally unchanged

  • No GitWorkflowDefinitionV2, version union, version dispatch, migration, or legacy conversion.
  • Run IDs, bases, props, retrieval metadata, journal identity, Workspace behavior, database layout.
  • CLI runtime entrypoints, workflow coordination, Files providers, DOFS, journals.
  • xmd workflow start README.md#Target is not shipped here. 🚀 Start and resume a workflow run from the CLI (#366 PR 2) #428 is the consumer.

Risks and limitations

Scope confirmation

  • Every changed file supports the purpose described above.
  • Unrelated cleanup and formatting changes are excluded — with the one disclosed exception above, isolated in its own commit.
  • Generated or mechanical changes are clearly identified.
  • The description matches the final diff and test results.

Base and verification

CommandResult
deno task lint0 errors
deno task checkno errors
deno task check:jsrSuccess Dry run complete
git diff --checkclean
deno task test packages/workflow/tests/workflow-definition.test.ts packages/workflow/tests/workflow-run-storage.test.ts11 passed (77 steps), 0 failed
deno task test --changed=origin/main465 passed (3246 steps), 0 failed, 6m9s
pnpm exec tsx --tsconfig tsconfig.node.json --test packages/workflow/tests/workflow-definition.test.ts24 pass, 0 fail
bun test packages/workflow/tests/workflow-definition.test.ts24 pass, 0 fail
(cd site && deno task check) with a shim presentexit 0

workflow-run-storage.test.ts stays excluded from Node and Bun by the existing node:sqlite entry in scripts/runtime-test-exclusions.ts; no exclusion was added.

The site fix carries a fail-first proof. With neither the ignore nor the exclusion, deno lint . in site/ reproduces the CI failure exactly (error[no-var], Found 1 problem); either mechanism alone silences it.

`site:check` and `site:build` run concurrently under the verifier, and
Vite writes a transient `vite.config.ts.timestamp-*.mjs` beside the
config while it loads it. A `deno lint .` that walked one reported
`no-var` on generated code nobody wrote, failing the battery for a file
that no longer existed by the time anyone looked.
The shim is now excluded from the site's own checks and ignored by Git.
Either alone silences it; both are kept because the exclusion states the
checker's scope and the ignore keeps the file from being committed.
A workflow definition identified a whole root document. It now optionally
carries the exact canonical document target the run is a run of, so a
workflow can be a run of one section.
`targetPath` is the resolved exact target, never the selector a caller
wrote: a glob describes what somebody asked for, and re-resolving one
against a different checkout can name a different section. Absent, the
definition means the complete document — which is what a whole-document
workflow is, not a legacy spelling.
What counts as canonical is not restated in the workflow package. Core's
predicate is exported as `isCanonicalDocumentTarget` and used directly,
because identity two packages define separately is identity they can
disagree about.
The member is closed like every other: writing it at all makes it
present, so an explicit `undefined` or `null` is a descriptor that asked
for a target and failed to name one. A refusal reports `$.targetPath` in
fixed wording that never echoes what it read, since a canonical target
encodes heading text.
Compatible reuse compares it. A run of one section, a run of another, and
a run of the whole document are three different runs, so reusing one run
id for another reports a definition conflict; the same exact target is
the same run and is found.
`version` stays 1 and there is no second version, union, or migration.

@github-actionsgithub-actionsBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Found 2 redundant comments. Inline suggestions to remove them below.

throw fail(`expected a string, found ${describe(value)}`, path);
}
// Deliberately says nothing about the target it read: a canonical target
// encodes heading text, and heading text is document content.

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
// encodes heading text, and heading text is document content.

rootDocumentPath: definition.rootDocumentPath,
// Written only when there is one. An untargeted definition that stored an
// explicit absence would parse back as a descriptor that asked for a target
// and failed to name it.

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
// and failed to name it.

@github-actions

Copy link
Copy Markdown

PR #431: ✨ Let a workflow definition name one section of its document

11 files, +401 / -6

Scope

🟡 407 lines changed. PRs under 400 receive more thorough review.

🟡 Changes span 7 directories.

🟡 PR mixes config and source changes.

Structural

✅ No structural bloat detected.

Slop

  • packages/workflow/src/storage/definition.ts:141// encodes heading text, and heading text is document content.
  • packages/workflow/src/storage/definition.ts:164// and failed to name it.

Static Analysis

✅ Oxlint found no issues.

Correctness

No extraneous code patterns detected.

@taras
taras marked this pull request as ready for review August 10, 2026 22:53
@taras
taras merged commit 32b63a5 into mainAug 10, 2026
16 checks passed
@taras
taras deleted the agent/issue-412-workflow-target-definition branch August 10, 2026 22:53
taras added a commit that referenced this pull request Aug 11, 2026
#432 made canonical core authoritative for execution and gave a trusted
host an explicit way to attach requirements. This drops the ambient
admitJournal() channel this PR had added to core and hands the run's
admission over as a value instead: workflowInstallation({ base }) and
retainedWorkflowInstallation(run) are ExecutionInstallations the host
passes to executeInstalled().
Identity is decided on the same single retained snapshot, ahead of guard
policy, terminal reuse, authored work and any append, and every RR/WF
regression carries over unchanged.
A definition's exact document target (#431) stays definition data: run
identity remains { runId, base, pinnedCommit }, and a recorded value
carrying a target as a fourth member is refused.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@taras
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

✨ Let a workflow definition name one section of its document - #431

Merged
taras merged 2 commits into
mainfrom
agent/issue-412-workflow-target-definition
Aug 10, 2026
Merged

✨ Let a workflow definition name one section of its document#431
taras merged 2 commits into
mainfrom
agent/issue-412-workflow-target-definition

Conversation

@taras

Copy link
Copy Markdown
Owner

Why

Issue #412 makes a root document's sections individually addressable. #421 built the core model, and #427 taught xmd targets and xmd run to use it. Neither could be persisted: a workflow definition identified a whole root document, so a workflow could not be a run of one section.

This is the last layer. It amends the existing GitWorkflowDefinitionV1 in place — no version 2, no version union, no migration, no compatibility-only machinery.

What changes

Before:

interfaceGitWorkflowDefinitionV1{version: 1;kind: "git";objectFormat: "sha1"|"sha256";objectId: string;rootDocumentPath: string;}

After — one optional member:

 targetPath?: string;
  • Absent identifies the complete root document.
  • Present identifies exactly one canonical document target, with no leading #.
  • It is the resolved exact target, never an authored selector or glob.
  • Whole-document and targeted definitions are incompatible; different exact targets are incompatible; the same exact target stays compatible.
  • version remains 1 as the schema tag.

The five-member untargeted shape stays valid because it is the representation of a whole-document workflow, not a legacy format being preserved.

How it works

host descriptor → parseWorkflowDefinition → isCanonicalDocumentTarget → stored JSON column
→ conflictingFields

Canonical-target authority is not duplicated. Core's isCanonicalTarget() — the predicate document references already use — is exported as isCanonicalDocumentTarget and called by the workflow parser. Identity that two packages define separately is identity they can disagree about, and this member is compared against targets the document layer produced.

Presence is the member being written, not its value. A descriptor that wrote targetPath and gave it undefined or null asked for a target and failed to say which — refused, rather than read as the whole document. definitionToJson() writes the member only when there is one, so an untargeted definition round-trips to five members.

Diagnostics say nothing about the target. A canonical target encodes heading text, and heading text is document content, so a refusal is fixed wording at $.targetPath.

Storage is unchanged. The definition already lives in a JSON column; there is no schema migration and no new table or column.

Review guide

Start with:packages/workflow/tests/workflow-definition.test.ts (Tier WD, WD18–WD24)

Then review:

  1. packages/workflow/src/storage/definition.ts — the interface, parseTargetPath(), and serialization
  2. packages/workflow/src/storage/compatibility.ts — one added comparison
  3. packages/core/mod.ts — the renamed public export
  4. Specifications and the architecture inventory

Look carefully at: presence-vs-value in parseTargetPath(). members.has() rather than get() !== undefined is what separates "no target" from "a target I failed to name".

What must stay true

  • The stored target is the resolved exact one; a glob is never identity — enforced by validating through core's canonical predicate, which rejects *, **, and embedded wildcards. Checked by WD21 and WD23.
  • A run of one section is not a run of the whole document — enforced by sameDefinition() comparing targetPath, where absent equals only absent. Checked by WD24, WS30, WS32.
  • Stored identity is never normalized or repaired — the parser validates and rebuilds from the value as given. Checked by WD20, which round-trips nine canonical forms byte for byte.
  • A diagnostic never echoes a target — checked by WD21.

How to verify it

  • WD18 proves an untargeted descriptor writes exactly five members and carries no targetPath key.
  • WD19/WD20 prove a targeted descriptor round-trips, including nested targets and the canonical escapes %2F, %2A, %23, %25, %20, and non-ASCII.
  • WD21 proves fifteen non-canonical forms are refused at $.targetPath — empty, leading #, *, **, embedded wildcard, malformed and lowercase escapes, leading/trailing/uncollapsed whitespace, empty levels, and an NFD spelling — with no echo.
  • WD22 proves a present non-string is refused, absence excepted.
  • WD23 proves the public core predicate accepts and rejects exactly the forms definition parsing does.
  • WD24 proves same-target compatibility and both conflict directions.
  • WS28–WS32 prove persistence: an exact target survives write, lookup, and a second scope's reopen unchanged; an untargeted run reopens with no member; and one run id cannot be reused for another section or for the whole document.

Mutation checks

MutationResult
targetPath removed from sameDefinition()WD24, WS30, WS32 red
targetPath omitted from definitionToJson()WD19, WD20, WS28 red
Canonical validation weakened to a nonempty-string checkWD21 red

Scope

Included

  • targetPath on GitWorkflowDefinitionV1: parsing, serialization, comparison.
  • isCanonicalDocumentTarget exported from @executablemd/core.
  • Specification and architecture updates.
  • One unrelated fix, carried at the maintainer's request and kept as its own commit (9940870): Vite's transient vite.config.ts.timestamp-*.mjs is excluded from the site's checks and ignored by Git.

Intentionally unchanged

  • No GitWorkflowDefinitionV2, version union, version dispatch, migration, or legacy conversion.
  • Run IDs, bases, props, retrieval metadata, journal identity, Workspace behavior, database layout.
  • CLI runtime entrypoints, workflow coordination, Files providers, DOFS, journals.
  • xmd workflow start README.md#Target is not shipped here. 🚀 Start and resume a workflow run from the CLI (#366 PR 2) #428 is the consumer.

Risks and limitations

Scope confirmation

  • Every changed file supports the purpose described above.
  • Unrelated cleanup and formatting changes are excluded — with the one disclosed exception above, isolated in its own commit.
  • Generated or mechanical changes are clearly identified.
  • The description matches the final diff and test results.

Base and verification

CommandResult
deno task lint0 errors
deno task checkno errors
deno task check:jsrSuccess Dry run complete
git diff --checkclean
deno task test packages/workflow/tests/workflow-definition.test.ts packages/workflow/tests/workflow-run-storage.test.ts11 passed (77 steps), 0 failed
deno task test --changed=origin/main465 passed (3246 steps), 0 failed, 6m9s
pnpm exec tsx --tsconfig tsconfig.node.json --test packages/workflow/tests/workflow-definition.test.ts24 pass, 0 fail
bun test packages/workflow/tests/workflow-definition.test.ts24 pass, 0 fail
(cd site && deno task check) with a shim presentexit 0

workflow-run-storage.test.ts stays excluded from Node and Bun by the existing node:sqlite entry in scripts/runtime-test-exclusions.ts; no exclusion was added.

The site fix carries a fail-first proof. With neither the ignore nor the exclusion, deno lint . in site/ reproduces the CI failure exactly (error[no-var], Found 1 problem); either mechanism alone silences it.

`site:check` and `site:build` run concurrently under the verifier, and
Vite writes a transient `vite.config.ts.timestamp-*.mjs` beside the
config while it loads it. A `deno lint .` that walked one reported
`no-var` on generated code nobody wrote, failing the battery for a file
that no longer existed by the time anyone looked.
The shim is now excluded from the site's own checks and ignored by Git.
Either alone silences it; both are kept because the exclusion states the
checker's scope and the ignore keeps the file from being committed.
A workflow definition identified a whole root document. It now optionally
carries the exact canonical document target the run is a run of, so a
workflow can be a run of one section.
`targetPath` is the resolved exact target, never the selector a caller
wrote: a glob describes what somebody asked for, and re-resolving one
against a different checkout can name a different section. Absent, the
definition means the complete document — which is what a whole-document
workflow is, not a legacy spelling.
What counts as canonical is not restated in the workflow package. Core's
predicate is exported as `isCanonicalDocumentTarget` and used directly,
because identity two packages define separately is identity they can
disagree about.
The member is closed like every other: writing it at all makes it
present, so an explicit `undefined` or `null` is a descriptor that asked
for a target and failed to name one. A refusal reports `$.targetPath` in
fixed wording that never echoes what it read, since a canonical target
encodes heading text.
Compatible reuse compares it. A run of one section, a run of another, and
a run of the whole document are three different runs, so reusing one run
id for another reports a definition conflict; the same exact target is
the same run and is found.
`version` stays 1 and there is no second version, union, or migration.

@github-actionsgithub-actionsBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Found 2 redundant comments. Inline suggestions to remove them below.

throw fail(`expected a string, found ${describe(value)}`, path);
}
// Deliberately says nothing about the target it read: a canonical target
// encodes heading text, and heading text is document content.

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
// encodes heading text, and heading text is document content.

rootDocumentPath: definition.rootDocumentPath,
// Written only when there is one. An untargeted definition that stored an
// explicit absence would parse back as a descriptor that asked for a target
// and failed to name it.

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
// and failed to name it.

@github-actions

Copy link
Copy Markdown

PR #431: ✨ Let a workflow definition name one section of its document

11 files, +401 / -6

Scope

🟡 407 lines changed. PRs under 400 receive more thorough review.

🟡 Changes span 7 directories.

🟡 PR mixes config and source changes.

Structural

✅ No structural bloat detected.

Slop

  • packages/workflow/src/storage/definition.ts:141// encodes heading text, and heading text is document content.
  • packages/workflow/src/storage/definition.ts:164// and failed to name it.

Static Analysis

✅ Oxlint found no issues.

Correctness

No extraneous code patterns detected.

@taras
taras marked this pull request as ready for review August 10, 2026 22:53
@taras
taras merged commit 32b63a5 into mainAug 10, 2026
16 checks passed
@taras
taras deleted the agent/issue-412-workflow-target-definition branch August 10, 2026 22:53
taras added a commit that referenced this pull request Aug 11, 2026
#432 made canonical core authoritative for execution and gave a trusted
host an explicit way to attach requirements. This drops the ambient
admitJournal() channel this PR had added to core and hands the run's
admission over as a value instead: workflowInstallation({ base }) and
retainedWorkflowInstallation(run) are ExecutionInstallations the host
passes to executeInstalled().
Identity is decided on the same single retained snapshot, ahead of guard
policy, terminal reuse, authored work and any append, and every RR/WF
regression carries over unchanged.
A definition's exact document target (#431) stays definition data: run
identity remains { runId, base, pinnedCommit }, and a recorded value
carrying a target as a fourth member is refused.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@taras
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

✨ Let a workflow definition name one section of its document - #431

Merged
taras merged 2 commits into
mainfrom
agent/issue-412-workflow-target-definition
Aug 10, 2026
Merged

✨ Let a workflow definition name one section of its document#431
taras merged 2 commits into
mainfrom
agent/issue-412-workflow-target-definition

Conversation

@taras

Copy link
Copy Markdown
Owner

Why

Issue #412 makes a root document's sections individually addressable. #421 built the core model, and #427 taught xmd targets and xmd run to use it. Neither could be persisted: a workflow definition identified a whole root document, so a workflow could not be a run of one section.

This is the last layer. It amends the existing GitWorkflowDefinitionV1 in place — no version 2, no version union, no migration, no compatibility-only machinery.

What changes

Before:

interfaceGitWorkflowDefinitionV1{version: 1;kind: "git";objectFormat: "sha1"|"sha256";objectId: string;rootDocumentPath: string;}

After — one optional member:

 targetPath?: string;
  • Absent identifies the complete root document.
  • Present identifies exactly one canonical document target, with no leading #.
  • It is the resolved exact target, never an authored selector or glob.
  • Whole-document and targeted definitions are incompatible; different exact targets are incompatible; the same exact target stays compatible.
  • version remains 1 as the schema tag.

The five-member untargeted shape stays valid because it is the representation of a whole-document workflow, not a legacy format being preserved.

How it works

host descriptor → parseWorkflowDefinition → isCanonicalDocumentTarget → stored JSON column
→ conflictingFields

Canonical-target authority is not duplicated. Core's isCanonicalTarget() — the predicate document references already use — is exported as isCanonicalDocumentTarget and called by the workflow parser. Identity that two packages define separately is identity they can disagree about, and this member is compared against targets the document layer produced.

Presence is the member being written, not its value. A descriptor that wrote targetPath and gave it undefined or null asked for a target and failed to say which — refused, rather than read as the whole document. definitionToJson() writes the member only when there is one, so an untargeted definition round-trips to five members.

Diagnostics say nothing about the target. A canonical target encodes heading text, and heading text is document content, so a refusal is fixed wording at $.targetPath.

Storage is unchanged. The definition already lives in a JSON column; there is no schema migration and no new table or column.

Review guide

Start with:packages/workflow/tests/workflow-definition.test.ts (Tier WD, WD18–WD24)

Then review:

  1. packages/workflow/src/storage/definition.ts — the interface, parseTargetPath(), and serialization
  2. packages/workflow/src/storage/compatibility.ts — one added comparison
  3. packages/core/mod.ts — the renamed public export
  4. Specifications and the architecture inventory

Look carefully at: presence-vs-value in parseTargetPath(). members.has() rather than get() !== undefined is what separates "no target" from "a target I failed to name".

What must stay true

  • The stored target is the resolved exact one; a glob is never identity — enforced by validating through core's canonical predicate, which rejects *, **, and embedded wildcards. Checked by WD21 and WD23.
  • A run of one section is not a run of the whole document — enforced by sameDefinition() comparing targetPath, where absent equals only absent. Checked by WD24, WS30, WS32.
  • Stored identity is never normalized or repaired — the parser validates and rebuilds from the value as given. Checked by WD20, which round-trips nine canonical forms byte for byte.
  • A diagnostic never echoes a target — checked by WD21.

How to verify it

  • WD18 proves an untargeted descriptor writes exactly five members and carries no targetPath key.
  • WD19/WD20 prove a targeted descriptor round-trips, including nested targets and the canonical escapes %2F, %2A, %23, %25, %20, and non-ASCII.
  • WD21 proves fifteen non-canonical forms are refused at $.targetPath — empty, leading #, *, **, embedded wildcard, malformed and lowercase escapes, leading/trailing/uncollapsed whitespace, empty levels, and an NFD spelling — with no echo.
  • WD22 proves a present non-string is refused, absence excepted.
  • WD23 proves the public core predicate accepts and rejects exactly the forms definition parsing does.
  • WD24 proves same-target compatibility and both conflict directions.
  • WS28–WS32 prove persistence: an exact target survives write, lookup, and a second scope's reopen unchanged; an untargeted run reopens with no member; and one run id cannot be reused for another section or for the whole document.

Mutation checks

MutationResult
targetPath removed from sameDefinition()WD24, WS30, WS32 red
targetPath omitted from definitionToJson()WD19, WD20, WS28 red
Canonical validation weakened to a nonempty-string checkWD21 red

Scope

Included

  • targetPath on GitWorkflowDefinitionV1: parsing, serialization, comparison.
  • isCanonicalDocumentTarget exported from @executablemd/core.
  • Specification and architecture updates.
  • One unrelated fix, carried at the maintainer's request and kept as its own commit (9940870): Vite's transient vite.config.ts.timestamp-*.mjs is excluded from the site's checks and ignored by Git.

Intentionally unchanged

  • No GitWorkflowDefinitionV2, version union, version dispatch, migration, or legacy conversion.
  • Run IDs, bases, props, retrieval metadata, journal identity, Workspace behavior, database layout.
  • CLI runtime entrypoints, workflow coordination, Files providers, DOFS, journals.
  • xmd workflow start README.md#Target is not shipped here. 🚀 Start and resume a workflow run from the CLI (#366 PR 2) #428 is the consumer.

Risks and limitations

Scope confirmation

  • Every changed file supports the purpose described above.
  • Unrelated cleanup and formatting changes are excluded — with the one disclosed exception above, isolated in its own commit.
  • Generated or mechanical changes are clearly identified.
  • The description matches the final diff and test results.

Base and verification

CommandResult
deno task lint0 errors
deno task checkno errors
deno task check:jsrSuccess Dry run complete
git diff --checkclean
deno task test packages/workflow/tests/workflow-definition.test.ts packages/workflow/tests/workflow-run-storage.test.ts11 passed (77 steps), 0 failed
deno task test --changed=origin/main465 passed (3246 steps), 0 failed, 6m9s
pnpm exec tsx --tsconfig tsconfig.node.json --test packages/workflow/tests/workflow-definition.test.ts24 pass, 0 fail
bun test packages/workflow/tests/workflow-definition.test.ts24 pass, 0 fail
(cd site && deno task check) with a shim presentexit 0

workflow-run-storage.test.ts stays excluded from Node and Bun by the existing node:sqlite entry in scripts/runtime-test-exclusions.ts; no exclusion was added.

The site fix carries a fail-first proof. With neither the ignore nor the exclusion, deno lint . in site/ reproduces the CI failure exactly (error[no-var], Found 1 problem); either mechanism alone silences it.

`site:check` and `site:build` run concurrently under the verifier, and
Vite writes a transient `vite.config.ts.timestamp-*.mjs` beside the
config while it loads it. A `deno lint .` that walked one reported
`no-var` on generated code nobody wrote, failing the battery for a file
that no longer existed by the time anyone looked.
The shim is now excluded from the site's own checks and ignored by Git.
Either alone silences it; both are kept because the exclusion states the
checker's scope and the ignore keeps the file from being committed.
A workflow definition identified a whole root document. It now optionally
carries the exact canonical document target the run is a run of, so a
workflow can be a run of one section.
`targetPath` is the resolved exact target, never the selector a caller
wrote: a glob describes what somebody asked for, and re-resolving one
against a different checkout can name a different section. Absent, the
definition means the complete document — which is what a whole-document
workflow is, not a legacy spelling.
What counts as canonical is not restated in the workflow package. Core's
predicate is exported as `isCanonicalDocumentTarget` and used directly,
because identity two packages define separately is identity they can
disagree about.
The member is closed like every other: writing it at all makes it
present, so an explicit `undefined` or `null` is a descriptor that asked
for a target and failed to name one. A refusal reports `$.targetPath` in
fixed wording that never echoes what it read, since a canonical target
encodes heading text.
Compatible reuse compares it. A run of one section, a run of another, and
a run of the whole document are three different runs, so reusing one run
id for another reports a definition conflict; the same exact target is
the same run and is found.
`version` stays 1 and there is no second version, union, or migration.

@github-actionsgithub-actionsBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Found 2 redundant comments. Inline suggestions to remove them below.

throw fail(`expected a string, found ${describe(value)}`, path);
}
// Deliberately says nothing about the target it read: a canonical target
// encodes heading text, and heading text is document content.

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
// encodes heading text, and heading text is document content.

rootDocumentPath: definition.rootDocumentPath,
// Written only when there is one. An untargeted definition that stored an
// explicit absence would parse back as a descriptor that asked for a target
// and failed to name it.

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
// and failed to name it.

@github-actions

Copy link
Copy Markdown

PR #431: ✨ Let a workflow definition name one section of its document

11 files, +401 / -6

Scope

🟡 407 lines changed. PRs under 400 receive more thorough review.

🟡 Changes span 7 directories.

🟡 PR mixes config and source changes.

Structural

✅ No structural bloat detected.

Slop

  • packages/workflow/src/storage/definition.ts:141// encodes heading text, and heading text is document content.
  • packages/workflow/src/storage/definition.ts:164// and failed to name it.

Static Analysis

✅ Oxlint found no issues.

Correctness

No extraneous code patterns detected.

@taras
taras marked this pull request as ready for review August 10, 2026 22:53
@taras
taras merged commit 32b63a5 into mainAug 10, 2026
16 checks passed
@taras
taras deleted the agent/issue-412-workflow-target-definition branch August 10, 2026 22:53
taras added a commit that referenced this pull request Aug 11, 2026
#432 made canonical core authoritative for execution and gave a trusted
host an explicit way to attach requirements. This drops the ambient
admitJournal() channel this PR had added to core and hands the run's
admission over as a value instead: workflowInstallation({ base }) and
retainedWorkflowInstallation(run) are ExecutionInstallations the host
passes to executeInstalled().
Identity is decided on the same single retained snapshot, ahead of guard
policy, terminal reuse, authored work and any append, and every RR/WF
regression carries over unchanged.
A definition's exact document target (#431) stays definition data: run
identity remains { runId, base, pinnedCommit }, and a recorded value
carrying a target as a fourth member is refused.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@taras
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

✨ Let a workflow definition name one section of its document - #431

Merged
taras merged 2 commits into
mainfrom
agent/issue-412-workflow-target-definition
Aug 10, 2026
Merged

✨ Let a workflow definition name one section of its document#431
taras merged 2 commits into
mainfrom
agent/issue-412-workflow-target-definition

Conversation

@taras

Copy link
Copy Markdown
Owner

Why

Issue #412 makes a root document's sections individually addressable. #421 built the core model, and #427 taught xmd targets and xmd run to use it. Neither could be persisted: a workflow definition identified a whole root document, so a workflow could not be a run of one section.

This is the last layer. It amends the existing GitWorkflowDefinitionV1 in place — no version 2, no version union, no migration, no compatibility-only machinery.

What changes

Before:

interfaceGitWorkflowDefinitionV1{version: 1;kind: "git";objectFormat: "sha1"|"sha256";objectId: string;rootDocumentPath: string;}

After — one optional member:

 targetPath?: string;
  • Absent identifies the complete root document.
  • Present identifies exactly one canonical document target, with no leading #.
  • It is the resolved exact target, never an authored selector or glob.
  • Whole-document and targeted definitions are incompatible; different exact targets are incompatible; the same exact target stays compatible.
  • version remains 1 as the schema tag.

The five-member untargeted shape stays valid because it is the representation of a whole-document workflow, not a legacy format being preserved.

How it works

host descriptor → parseWorkflowDefinition → isCanonicalDocumentTarget → stored JSON column
→ conflictingFields

Canonical-target authority is not duplicated. Core's isCanonicalTarget() — the predicate document references already use — is exported as isCanonicalDocumentTarget and called by the workflow parser. Identity that two packages define separately is identity they can disagree about, and this member is compared against targets the document layer produced.

Presence is the member being written, not its value. A descriptor that wrote targetPath and gave it undefined or null asked for a target and failed to say which — refused, rather than read as the whole document. definitionToJson() writes the member only when there is one, so an untargeted definition round-trips to five members.

Diagnostics say nothing about the target. A canonical target encodes heading text, and heading text is document content, so a refusal is fixed wording at $.targetPath.

Storage is unchanged. The definition already lives in a JSON column; there is no schema migration and no new table or column.

Review guide

Start with:packages/workflow/tests/workflow-definition.test.ts (Tier WD, WD18–WD24)

Then review:

  1. packages/workflow/src/storage/definition.ts — the interface, parseTargetPath(), and serialization
  2. packages/workflow/src/storage/compatibility.ts — one added comparison
  3. packages/core/mod.ts — the renamed public export
  4. Specifications and the architecture inventory

Look carefully at: presence-vs-value in parseTargetPath(). members.has() rather than get() !== undefined is what separates "no target" from "a target I failed to name".

What must stay true

  • The stored target is the resolved exact one; a glob is never identity — enforced by validating through core's canonical predicate, which rejects *, **, and embedded wildcards. Checked by WD21 and WD23.
  • A run of one section is not a run of the whole document — enforced by sameDefinition() comparing targetPath, where absent equals only absent. Checked by WD24, WS30, WS32.
  • Stored identity is never normalized or repaired — the parser validates and rebuilds from the value as given. Checked by WD20, which round-trips nine canonical forms byte for byte.
  • A diagnostic never echoes a target — checked by WD21.

How to verify it

  • WD18 proves an untargeted descriptor writes exactly five members and carries no targetPath key.
  • WD19/WD20 prove a targeted descriptor round-trips, including nested targets and the canonical escapes %2F, %2A, %23, %25, %20, and non-ASCII.
  • WD21 proves fifteen non-canonical forms are refused at $.targetPath — empty, leading #, *, **, embedded wildcard, malformed and lowercase escapes, leading/trailing/uncollapsed whitespace, empty levels, and an NFD spelling — with no echo.
  • WD22 proves a present non-string is refused, absence excepted.
  • WD23 proves the public core predicate accepts and rejects exactly the forms definition parsing does.
  • WD24 proves same-target compatibility and both conflict directions.
  • WS28–WS32 prove persistence: an exact target survives write, lookup, and a second scope's reopen unchanged; an untargeted run reopens with no member; and one run id cannot be reused for another section or for the whole document.

Mutation checks

MutationResult
targetPath removed from sameDefinition()WD24, WS30, WS32 red
targetPath omitted from definitionToJson()WD19, WD20, WS28 red
Canonical validation weakened to a nonempty-string checkWD21 red

Scope

Included

  • targetPath on GitWorkflowDefinitionV1: parsing, serialization, comparison.
  • isCanonicalDocumentTarget exported from @executablemd/core.
  • Specification and architecture updates.
  • One unrelated fix, carried at the maintainer's request and kept as its own commit (9940870): Vite's transient vite.config.ts.timestamp-*.mjs is excluded from the site's checks and ignored by Git.

Intentionally unchanged

  • No GitWorkflowDefinitionV2, version union, version dispatch, migration, or legacy conversion.
  • Run IDs, bases, props, retrieval metadata, journal identity, Workspace behavior, database layout.
  • CLI runtime entrypoints, workflow coordination, Files providers, DOFS, journals.
  • xmd workflow start README.md#Target is not shipped here. 🚀 Start and resume a workflow run from the CLI (#366 PR 2) #428 is the consumer.

Risks and limitations

Scope confirmation

  • Every changed file supports the purpose described above.
  • Unrelated cleanup and formatting changes are excluded — with the one disclosed exception above, isolated in its own commit.
  • Generated or mechanical changes are clearly identified.
  • The description matches the final diff and test results.

Base and verification

CommandResult
deno task lint0 errors
deno task checkno errors
deno task check:jsrSuccess Dry run complete
git diff --checkclean
deno task test packages/workflow/tests/workflow-definition.test.ts packages/workflow/tests/workflow-run-storage.test.ts11 passed (77 steps), 0 failed
deno task test --changed=origin/main465 passed (3246 steps), 0 failed, 6m9s
pnpm exec tsx --tsconfig tsconfig.node.json --test packages/workflow/tests/workflow-definition.test.ts24 pass, 0 fail
bun test packages/workflow/tests/workflow-definition.test.ts24 pass, 0 fail
(cd site && deno task check) with a shim presentexit 0

workflow-run-storage.test.ts stays excluded from Node and Bun by the existing node:sqlite entry in scripts/runtime-test-exclusions.ts; no exclusion was added.

The site fix carries a fail-first proof. With neither the ignore nor the exclusion, deno lint . in site/ reproduces the CI failure exactly (error[no-var], Found 1 problem); either mechanism alone silences it.

`site:check` and `site:build` run concurrently under the verifier, and
Vite writes a transient `vite.config.ts.timestamp-*.mjs` beside the
config while it loads it. A `deno lint .` that walked one reported
`no-var` on generated code nobody wrote, failing the battery for a file
that no longer existed by the time anyone looked.
The shim is now excluded from the site's own checks and ignored by Git.
Either alone silences it; both are kept because the exclusion states the
checker's scope and the ignore keeps the file from being committed.
A workflow definition identified a whole root document. It now optionally
carries the exact canonical document target the run is a run of, so a
workflow can be a run of one section.
`targetPath` is the resolved exact target, never the selector a caller
wrote: a glob describes what somebody asked for, and re-resolving one
against a different checkout can name a different section. Absent, the
definition means the complete document — which is what a whole-document
workflow is, not a legacy spelling.
What counts as canonical is not restated in the workflow package. Core's
predicate is exported as `isCanonicalDocumentTarget` and used directly,
because identity two packages define separately is identity they can
disagree about.
The member is closed like every other: writing it at all makes it
present, so an explicit `undefined` or `null` is a descriptor that asked
for a target and failed to name one. A refusal reports `$.targetPath` in
fixed wording that never echoes what it read, since a canonical target
encodes heading text.
Compatible reuse compares it. A run of one section, a run of another, and
a run of the whole document are three different runs, so reusing one run
id for another reports a definition conflict; the same exact target is
the same run and is found.
`version` stays 1 and there is no second version, union, or migration.

@github-actionsgithub-actionsBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Found 2 redundant comments. Inline suggestions to remove them below.

throw fail(`expected a string, found ${describe(value)}`, path);
}
// Deliberately says nothing about the target it read: a canonical target
// encodes heading text, and heading text is document content.

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
// encodes heading text, and heading text is document content.

rootDocumentPath: definition.rootDocumentPath,
// Written only when there is one. An untargeted definition that stored an
// explicit absence would parse back as a descriptor that asked for a target
// and failed to name it.

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
// and failed to name it.

@github-actions

Copy link
Copy Markdown

PR #431: ✨ Let a workflow definition name one section of its document

11 files, +401 / -6

Scope

🟡 407 lines changed. PRs under 400 receive more thorough review.

🟡 Changes span 7 directories.

🟡 PR mixes config and source changes.

Structural

✅ No structural bloat detected.

Slop

  • packages/workflow/src/storage/definition.ts:141// encodes heading text, and heading text is document content.
  • packages/workflow/src/storage/definition.ts:164// and failed to name it.

Static Analysis

✅ Oxlint found no issues.

Correctness

No extraneous code patterns detected.

@taras
taras marked this pull request as ready for review August 10, 2026 22:53
@taras
taras merged commit 32b63a5 into mainAug 10, 2026
16 checks passed
@taras
taras deleted the agent/issue-412-workflow-target-definition branch August 10, 2026 22:53
taras added a commit that referenced this pull request Aug 11, 2026
#432 made canonical core authoritative for execution and gave a trusted
host an explicit way to attach requirements. This drops the ambient
admitJournal() channel this PR had added to core and hands the run's
admission over as a value instead: workflowInstallation({ base }) and
retainedWorkflowInstallation(run) are ExecutionInstallations the host
passes to executeInstalled().
Identity is decided on the same single retained snapshot, ahead of guard
policy, terminal reuse, authored work and any append, and every RR/WF
regression carries over unchanged.
A definition's exact document target (#431) stays definition data: run
identity remains { runId, base, pinnedCommit }, and a recorded value
carrying a target as a fourth member is refused.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@taras
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

✨ Let a workflow definition name one section of its document - #431

Merged
taras merged 2 commits into
mainfrom
agent/issue-412-workflow-target-definition
Aug 10, 2026
Merged

✨ Let a workflow definition name one section of its document#431
taras merged 2 commits into
mainfrom
agent/issue-412-workflow-target-definition

Conversation

@taras

Copy link
Copy Markdown
Owner

Why

Issue #412 makes a root document's sections individually addressable. #421 built the core model, and #427 taught xmd targets and xmd run to use it. Neither could be persisted: a workflow definition identified a whole root document, so a workflow could not be a run of one section.

This is the last layer. It amends the existing GitWorkflowDefinitionV1 in place — no version 2, no version union, no migration, no compatibility-only machinery.

What changes

Before:

interfaceGitWorkflowDefinitionV1{version: 1;kind: "git";objectFormat: "sha1"|"sha256";objectId: string;rootDocumentPath: string;}

After — one optional member:

 targetPath?: string;
  • Absent identifies the complete root document.
  • Present identifies exactly one canonical document target, with no leading #.
  • It is the resolved exact target, never an authored selector or glob.
  • Whole-document and targeted definitions are incompatible; different exact targets are incompatible; the same exact target stays compatible.
  • version remains 1 as the schema tag.

The five-member untargeted shape stays valid because it is the representation of a whole-document workflow, not a legacy format being preserved.

How it works

host descriptor → parseWorkflowDefinition → isCanonicalDocumentTarget → stored JSON column
→ conflictingFields

Canonical-target authority is not duplicated. Core's isCanonicalTarget() — the predicate document references already use — is exported as isCanonicalDocumentTarget and called by the workflow parser. Identity that two packages define separately is identity they can disagree about, and this member is compared against targets the document layer produced.

Presence is the member being written, not its value. A descriptor that wrote targetPath and gave it undefined or null asked for a target and failed to say which — refused, rather than read as the whole document. definitionToJson() writes the member only when there is one, so an untargeted definition round-trips to five members.

Diagnostics say nothing about the target. A canonical target encodes heading text, and heading text is document content, so a refusal is fixed wording at $.targetPath.

Storage is unchanged. The definition already lives in a JSON column; there is no schema migration and no new table or column.

Review guide

Start with:packages/workflow/tests/workflow-definition.test.ts (Tier WD, WD18–WD24)

Then review:

  1. packages/workflow/src/storage/definition.ts — the interface, parseTargetPath(), and serialization
  2. packages/workflow/src/storage/compatibility.ts — one added comparison
  3. packages/core/mod.ts — the renamed public export
  4. Specifications and the architecture inventory

Look carefully at: presence-vs-value in parseTargetPath(). members.has() rather than get() !== undefined is what separates "no target" from "a target I failed to name".

What must stay true

  • The stored target is the resolved exact one; a glob is never identity — enforced by validating through core's canonical predicate, which rejects *, **, and embedded wildcards. Checked by WD21 and WD23.
  • A run of one section is not a run of the whole document — enforced by sameDefinition() comparing targetPath, where absent equals only absent. Checked by WD24, WS30, WS32.
  • Stored identity is never normalized or repaired — the parser validates and rebuilds from the value as given. Checked by WD20, which round-trips nine canonical forms byte for byte.
  • A diagnostic never echoes a target — checked by WD21.

How to verify it

  • WD18 proves an untargeted descriptor writes exactly five members and carries no targetPath key.
  • WD19/WD20 prove a targeted descriptor round-trips, including nested targets and the canonical escapes %2F, %2A, %23, %25, %20, and non-ASCII.
  • WD21 proves fifteen non-canonical forms are refused at $.targetPath — empty, leading #, *, **, embedded wildcard, malformed and lowercase escapes, leading/trailing/uncollapsed whitespace, empty levels, and an NFD spelling — with no echo.
  • WD22 proves a present non-string is refused, absence excepted.
  • WD23 proves the public core predicate accepts and rejects exactly the forms definition parsing does.
  • WD24 proves same-target compatibility and both conflict directions.
  • WS28–WS32 prove persistence: an exact target survives write, lookup, and a second scope's reopen unchanged; an untargeted run reopens with no member; and one run id cannot be reused for another section or for the whole document.

Mutation checks

MutationResult
targetPath removed from sameDefinition()WD24, WS30, WS32 red
targetPath omitted from definitionToJson()WD19, WD20, WS28 red
Canonical validation weakened to a nonempty-string checkWD21 red

Scope

Included

  • targetPath on GitWorkflowDefinitionV1: parsing, serialization, comparison.
  • isCanonicalDocumentTarget exported from @executablemd/core.
  • Specification and architecture updates.
  • One unrelated fix, carried at the maintainer's request and kept as its own commit (9940870): Vite's transient vite.config.ts.timestamp-*.mjs is excluded from the site's checks and ignored by Git.

Intentionally unchanged

  • No GitWorkflowDefinitionV2, version union, version dispatch, migration, or legacy conversion.
  • Run IDs, bases, props, retrieval metadata, journal identity, Workspace behavior, database layout.
  • CLI runtime entrypoints, workflow coordination, Files providers, DOFS, journals.
  • xmd workflow start README.md#Target is not shipped here. 🚀 Start and resume a workflow run from the CLI (#366 PR 2) #428 is the consumer.

Risks and limitations

Scope confirmation

  • Every changed file supports the purpose described above.
  • Unrelated cleanup and formatting changes are excluded — with the one disclosed exception above, isolated in its own commit.
  • Generated or mechanical changes are clearly identified.
  • The description matches the final diff and test results.

Base and verification

CommandResult
deno task lint0 errors
deno task checkno errors
deno task check:jsrSuccess Dry run complete
git diff --checkclean
deno task test packages/workflow/tests/workflow-definition.test.ts packages/workflow/tests/workflow-run-storage.test.ts11 passed (77 steps), 0 failed
deno task test --changed=origin/main465 passed (3246 steps), 0 failed, 6m9s
pnpm exec tsx --tsconfig tsconfig.node.json --test packages/workflow/tests/workflow-definition.test.ts24 pass, 0 fail
bun test packages/workflow/tests/workflow-definition.test.ts24 pass, 0 fail
(cd site && deno task check) with a shim presentexit 0

workflow-run-storage.test.ts stays excluded from Node and Bun by the existing node:sqlite entry in scripts/runtime-test-exclusions.ts; no exclusion was added.

The site fix carries a fail-first proof. With neither the ignore nor the exclusion, deno lint . in site/ reproduces the CI failure exactly (error[no-var], Found 1 problem); either mechanism alone silences it.

`site:check` and `site:build` run concurrently under the verifier, and
Vite writes a transient `vite.config.ts.timestamp-*.mjs` beside the
config while it loads it. A `deno lint .` that walked one reported
`no-var` on generated code nobody wrote, failing the battery for a file
that no longer existed by the time anyone looked.
The shim is now excluded from the site's own checks and ignored by Git.
Either alone silences it; both are kept because the exclusion states the
checker's scope and the ignore keeps the file from being committed.
A workflow definition identified a whole root document. It now optionally
carries the exact canonical document target the run is a run of, so a
workflow can be a run of one section.
`targetPath` is the resolved exact target, never the selector a caller
wrote: a glob describes what somebody asked for, and re-resolving one
against a different checkout can name a different section. Absent, the
definition means the complete document — which is what a whole-document
workflow is, not a legacy spelling.
What counts as canonical is not restated in the workflow package. Core's
predicate is exported as `isCanonicalDocumentTarget` and used directly,
because identity two packages define separately is identity they can
disagree about.
The member is closed like every other: writing it at all makes it
present, so an explicit `undefined` or `null` is a descriptor that asked
for a target and failed to name one. A refusal reports `$.targetPath` in
fixed wording that never echoes what it read, since a canonical target
encodes heading text.
Compatible reuse compares it. A run of one section, a run of another, and
a run of the whole document are three different runs, so reusing one run
id for another reports a definition conflict; the same exact target is
the same run and is found.
`version` stays 1 and there is no second version, union, or migration.

@github-actionsgithub-actionsBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Found 2 redundant comments. Inline suggestions to remove them below.

throw fail(`expected a string, found ${describe(value)}`, path);
}
// Deliberately says nothing about the target it read: a canonical target
// encodes heading text, and heading text is document content.

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
// encodes heading text, and heading text is document content.

rootDocumentPath: definition.rootDocumentPath,
// Written only when there is one. An untargeted definition that stored an
// explicit absence would parse back as a descriptor that asked for a target
// and failed to name it.

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
// and failed to name it.

@github-actions

Copy link
Copy Markdown

PR #431: ✨ Let a workflow definition name one section of its document

11 files, +401 / -6

Scope

🟡 407 lines changed. PRs under 400 receive more thorough review.

🟡 Changes span 7 directories.

🟡 PR mixes config and source changes.

Structural

✅ No structural bloat detected.

Slop

  • packages/workflow/src/storage/definition.ts:141// encodes heading text, and heading text is document content.
  • packages/workflow/src/storage/definition.ts:164// and failed to name it.

Static Analysis

✅ Oxlint found no issues.

Correctness

No extraneous code patterns detected.

@taras
taras marked this pull request as ready for review August 10, 2026 22:53
@taras
taras merged commit 32b63a5 into mainAug 10, 2026
16 checks passed
@taras
taras deleted the agent/issue-412-workflow-target-definition branch August 10, 2026 22:53
taras added a commit that referenced this pull request Aug 11, 2026
#432 made canonical core authoritative for execution and gave a trusted
host an explicit way to attach requirements. This drops the ambient
admitJournal() channel this PR had added to core and hands the run's
admission over as a value instead: workflowInstallation({ base }) and
retainedWorkflowInstallation(run) are ExecutionInstallations the host
passes to executeInstalled().
Identity is decided on the same single retained snapshot, ahead of guard
policy, terminal reuse, authored work and any append, and every RR/WF
regression carries over unchanged.
A definition's exact document target (#431) stays definition data: run
identity remains { runId, base, pinnedCommit }, and a recorded value
carrying a target as a fourth member is refused.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@taras