Skip to content

✨ Address a document's sections from the command line - #427

Merged
taras merged 2 commits into
mainfrom
agent/issue-412-cli-targets
Aug 10, 2026
Merged

✨ Address a document's sections from the command line#427
taras merged 2 commits into
mainfrom
agent/issue-412-cli-targets

Conversation

@taras

@taras taras commented Aug 10, 2026

Copy link
Copy Markdown
Owner

Why

Issue #412 makes a root document's sections individually addressable. PR #421 (merged as f57ae74) built the core: the target catalog, selector resolution, projection, and durable identity. Nothing on the command line could reach it.

This is the second layer: the two CLI consumers. It does not implement workflow-definition version 2 or workflow start/resume — that is PR C, which depends on PR A rather than on this.

What changes

Before:

  • xmd run <path> ran the whole document. A path was a filesystem path, byte for byte.
  • There was no way to ask a document what it addresses.

After:

  • xmd targets <document.md> prints one full document reference per addressable section.
  • xmd run README.md#Release/Publish and xmd README.md#Release/* run exactly one section.
  • A file argument to xmd run is a document reference: the first raw # starts a target selector.
$ xmd targets smoke-test/document-targets.md
smoke-test/document-targets.md#Alpha
smoke-test/document-targets.md#Beta

$ xmd run 'smoke-test/document-targets.md#Alpha' --raw
# Document targets
…
ALPHA_RAN

A failed selection reports full references rather than bare fragments:

$ xmd run 'doc.md#Delta'
"Delta" matches no document target.
Available targets:
  doc.md#Alpha
  doc.md#Beta

xmd test is untouched: its path grammar is unchanged and it gains no target selection.

How it works

argv → fileSource(path) → inspectDocument() → exact target replaces the selector
     → readsValue() rereads → execute({ …root, target: exact })

The CLI consumes PR A's API and adds no heading scanner, selector matcher, projection routine, or error type of its own.

The selector is replaced by its answer before anything executes. The run preparation path already inspects the root to discover its props; it now builds that root with fileSource() and, when inspection resolves a target, retains { …root, target: info.target } — the exact canonical target, never the caller's glob. Execution reads the file again from that exact root.

That replacement is the load-bearing part. If a wildcard resolves to Alpha during inspection and the file is then replaced so the same wildcard would name Beta, the run fails on the absent Alpha. It never silently runs Beta.

readsValue() used to turn every inspection failure into "not a value root" so execution could report it. It now rethrows a recognized DocumentTargetError, so a replacement the value-mode inspection can see leaves runDocument() before the provider installer. Every other inspection failure still returns false.

That is the earlier of two refusals, not the only one. runDocument() reads the document three times — preparation, value-mode inspection, then execute() — and a replacement that first appears on the third read is refused by execute(), after the provider installer has run. Installing a provider is not using one: neither refusal starts or attaches a service, and neither expands anything. Closing that third-read interval would need a prepared-root/execution boundary, which is deliberately out of scope here.

Review guide

Start with: packages/cli/tests/targets-cli.test.ts

Then review:

  1. packages/cli/src/cli.tsexactRoot() and its call site in preparePropsPhase()
  2. readsValue() — the narrowed failure behavior
  3. targetFailureReport() — the one formatter, used by preparation and by the final run failure
  4. listTargets() / targetsGrammarError() — the inspection-only command
  5. Specifications and the architecture construct inventory

Look carefully at:

  • CT12, the inspection/execution replacement seam. It installs a stateful API.Fs middleware around the exported runXmd() operation rather than racing a real file, and asserts the exact-Alpha diagnostic, no service installation, and no read of what the replacement's section would have read.

What must stay true

  • The exact canonical target is what executes; a glob is never authority — enforced by exactRoot() replacing the selector before execute(), checked by CT12. Mutating exactRoot to keep the glob reds CT12 alone.
  • A refused target never starts a service, whichever read discovered it — checked by CT12 (serviceInstalled === false, refused before the installer) and CT12a (serviceInstalled === true, serviceStarted === false, refused after it). xmd targets never invokes the installer at all.
  • A refused target performs no authored effect — checked by both cases asserting the replacement section's <File> read never happened; the harness installs the host Files provider and points the contextual cwd at the fixture, so that read would succeed if it were attempted.
  • xmd targets performs inspection only — enforced by the command never entering runDocument(), checked by CT4 (an unresolvable component, an executable block, and an authored write, none of which run).
  • xmd test path grammar is unchanged — checked by DT30, which runs literal we#ird.test.md and pct%25.test.md and shows the percent-encoded spelling names no file.

How to verify it

  • CT6a proves a raw % is escape syntax: pct%25zz.md lists and runs, pct%zz.md is refused as an invalid reference, and DT30 shows xmd test still reads that literal filename.
  • CT1–CT3, CT13/CT13a/CT13b prove the catalog's content, source order, retained duplicates, byte-empty empty catalog, and the canonical encoding of headings containing /, *, #, %, spaces, and Unicode — listed and runnable through those references. They fail if the CLI re-encoded a canonical fragment or printed the core's bare fragments.
  • CT4 proves discovery runs nothing. It fails if xmd targets reached expansion.
  • CT5/CT5a/CT5b/CT5c/CT5d prove every rejected invocation — missing argument, any fragment including an empty one, a second argument, -e, props, journal/verbose/raw/component-dir/secret-detection/pattern, agent flags, an unknown option, and -- — exits 1 with empty stdout and leaves no journal behind. They are split across several cases because each row is a subprocess and one case has to fit the shortest per-test budget of the three runtimes.
  • CT7–CT9 prove both run forms and *, embedded *, and ** execute one target and exclude both earlier and later siblings.
  • CT10/CT10a/CT10b/CT11 prove zero, invalid, empty-catalog, and ambiguous selections fail before expansion and print full references, with duplicates preserved in the matched list.
  • CT12/CT12a prove a wildcard cannot select one target at inspection and another at execution — CT12 when an inspection sees the replacement, CT12a when only execute()'s own read does. CT12a pins the boundary the contract now states: provider installed, service never started, nothing expanded.
  • CT16 proves a target failure outranks an invalid props schema and performs no authored effect, while a resolvable target lets the schema failure be reported.
  • PC21–PC23, VR7–VR9, IE22–IE23, CH6–CH9, DT30 cover targeted props and property help, targeted value and <Output> roots, inline exclusivity, help discoverability, and the unchanged xmd test grammar.

Manually, against the compiled binary:

deno task build
./dist/xmd targets smoke-test/document-targets.md
./dist/xmd run 'smoke-test/document-targets.md#Alpha' --raw

Scope

Included

  • xmd targets <document-reference>, inspection only.
  • Document-reference grammar for both xmd run forms.
  • Exact-target replacement before execution.
  • Full-reference target diagnostics in both failure locations (preparation, and the final run attempt).
  • Help and usage text for the new command and grammar.
  • Specification and architecture updates for the shipped surface.
  • A smoke-test/document-targets.md fixture and one step in the existing smoke CI job.

Intentionally unchanged

  • xmd test — no target selection, no reference grammar.
  • Inline xmd run -e — untargeted.
  • Untargeted xmd run <path> for ordinary paths.
  • API.Files, its adapters, provider installation, and every runtime adapter (deno.ts, node.ts, bun.ts, compiled.ts).
  • Core target parsing, projection, and replay code.
  • No --target option, no multi-match fan-out, no workflow definition or storage.

New abstractions

  • exactRoot() exists because the requested root and the executed root are different values, and the difference is the invariant this PR ships.

  • targetFailureReport() exists because both failure locations must render one diagnostic identically; it keeps the core's own first line and only re-renders the target lists as references.

  • readReference() / inspectCatalog() exist to hand fileSource()'s throw and inspectDocument()'s throw back as Result at the two call sites that report rather than raise.

  • Each new abstraction has multiple concrete uses or a clear justification.

  • No speculative functionality is included.

Risks and limitations

  • One intentional compatibility break. A xmd run or xmd targets path containing a raw #, or any literal %, no longer means that literal filename. % begins escape syntax wherever it appears, so pct%zz.md is refused as a malformed reference rather than read literally; it is written pct%25zz.md. Help and both specifications teach %23 and %25, and xmd test is deliberately exempt.
  • Issue Run document sections as hierarchical workflow targets #412 remains incomplete until PR C supplies the version-2 workflow definition carrying the exact target.

Scope confirmation

  • Every changed file supports the purpose described above.
  • Unrelated cleanup and formatting changes are excluded.
  • Generated or mechanical changes are clearly identified.
  • The description matches the final diff and test results.

Base and verification

Every command run locally, and its result:

Command Result
deno task lint 0 errors (pre-existing warnings only)
deno task check no errors
deno task check:jsr Success Dry run complete
git diff --check clean
pnpm exec tsc --project tsconfig.node.json --noEmit exit 0
deno task test (full Deno suite, at 541fc03) 463 passed (3225 steps), 0 failed, 6m5s
All six CLI suites under Node 122 pass, 0 fail
All six CLI suites under Bun 122 pass, 0 fail
deno test scripts/tests/ci-workflow.test.ts 1 passed (9 steps)
deno task build then ./dist/xmd targets smoke-test/document-targets.md catalog matched byte for byte
./dist/xmd run 'smoke-test/document-targets.md#Alpha' --raw ALPHA_RAN present, BETA_RAN absent

The full Deno suite was run rather than affected-file selection, because this change crosses subprocess dispatch, dynamic CLI parsing, and a compiled-binary smoke boundary.

Mutation probes show the replacement cases are discriminating:

  • exactRoot() returning the requested root unchanged — CT12 and CT12a both fail with status 0: the run silently succeeds executing the replacement's section, and the recorder shows its <File> read did happen. That is precisely the defect the invariant exists to prevent.
  • readsValue() swallowing the target failure again — CT12 fails on serviceInstalled, which is the early-refusal boundary.

CI owns the exhaustive Node and Bun suites, site, smoke, review, the filesystem-contract rows, and the aggregate green check. composability skips off main by repository policy.

`xmd targets <document.md>` prints one full document reference per
addressable section, by inspection alone. A file argument to `xmd run`
becomes a document reference, so `xmd run README.md#Release/Publish` and
`xmd README.md#Release/*` run exactly one section of a document.

The selector is replaced by its answer before anything executes. Run
preparation already inspects the root to discover its props; it now
retains the exact canonical target that inspection resolved and asks
execution for that, never for the glob. A file replaced between the two
reads therefore fails on the target the run chose rather than silently
running whatever the glob would name now, and `readsValue()` raises a
recognized target failure so that refusal lands before the service
installer and before `execute()`.

Diagnostics keep the core's own first line and render its target lists as
full references, because a reference is what a caller can act on.

A filename holding `#` or a literal `%HH` sequence is now written `%23`
and `%25HH` for `xmd run`. `xmd test` keeps its own path grammar and
gains no target selection.
@github-actions

github-actions Bot commented Aug 10, 2026

Copy link
Copy Markdown

PR #427: ✨ Address a document's sections from the command line

13 files, +1311 / -31

Scope

🔴 PR has 1342 lines changed. Split into focused PRs.

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

🟡 Changes span 7 directories.

Structural

Oxlint structural signals:

  • no-empty-function ×1: packages/cli/src/cli.ts

Slop

Oxlint slop signals:

  • no-console ×3: packages/cli/src/cli.ts

Static Analysis

Oxlint: 6 diagnostics across 1 file (4 rules)
Density: 0.005 violations/added-line

no-console (3): packages/cli/src/cli.ts
no-empty-function (1): packages/cli/src/cli.ts
no-unsafe-type-assertion (1): packages/cli/src/cli.ts
no-floating-promises (1): packages/cli/src/cli.ts

Correctness

No extraneous code patterns detected.

Two documented claims were stronger than the code.

The run reads its document three times — preparation, the value-mode
inspection, then execution — so a replacement that first appears on the
third read is refused by `execute()`, after the host's provider installer
has run. The specification said every such refusal precedes the
installer. It does not, and the guarantee worth stating is the one that
holds either way: installing a provider is not using one, so a refused
run starts no service and expands nothing, whichever read caught it.
CT12 keeps the early boundary and CT12a pins the later one, asserting
that the installed provider's start operation is never called.

`fileSource()` reads a raw `%` as the start of escape syntax wherever it
appears, so every literal percent in a reference must be written `%25` —
not only one that begins a valid `%HH` sequence. `pct%zz.md` is refused
as a malformed reference rather than read as that filename. The help
text already said this correctly; the specifications and the shared
`fileSource()` docstring did not.

The rejection cases are split three invocations to a case. Each row is a
subprocess, and Bun's fixed 5s per-test budget leaves no room for six of
them on a loaded runner.
@taras
taras marked this pull request as ready for review August 10, 2026 17:22
@taras
taras merged commit b324b97 into main Aug 10, 2026
16 checks passed
@taras
taras deleted the agent/issue-412-cli-targets branch August 10, 2026 17:22
Sign up for free to 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