Skip to content

feat(cli): add mds watch subcommand with auto-recompile on save#93

Open
dean0x wants to merge 5 commits into
mainfrom
feature/57-mds-watch-command
Open

feat(cli): add mds watch subcommand with auto-recompile on save#93
dean0x wants to merge 5 commits into
mainfrom
feature/57-mds-watch-command

Conversation

@dean0x

@dean0x dean0x commented Jun 8, 2026

Copy link
Copy Markdown
Owner

Summary

Implements mds watch for milestone v0.3.0 (closes #57). Watches .mds files (or a directory) and auto-recompiles on save, enabling live-preview workflows without a build step on every change.

Changes

New command: mds watch

  • Single-file mode (mds watch template.mds): watches the entry file and all transitive @import dependencies. Editing any imported file triggers a recompile of the entry. Dep set is recomputed on every rebuild — never trusted from a stale prior state (applies ADR-016).
  • Directory mode (mds watch src/): recursive watch; compiles each changed .mds independently. On source deletion, removes the matching output file. Does not do reverse-dep tracking (documented limitation).
  • Full flag parity with mds build: -o, --out-dir, --vars, --set, --format (single-file only), global --quiet.
  • Watch-specific flags: --clear (clear terminal before rebuild, TTY-guarded), --debounce <ms> (default 100, coalesces rapid saves).
  • All status/warnings go to stderr; compiled content only to stdout when -o -.
  • Ctrl+C → "Stopped watching." → exit 0. Compile errors during watch print but never terminate the watcher.
  • Per-rebuild summary to stderr (unless quiet): Recompiled <out> (<n> deps) in <ms>ms.

Behavior-preserving refactor (AC-A7)

Build logic extracted from main.rs (722 lines) into build.rs with pub(crate) helpers. The refactor is verified green against the full existing test suite before any watch code was added.

New shared helper compile_and_write routes both markdown and messages modes through read_build_input and compile_with_deps, preserving the 10 MiB file-size cap on every code path (avoids PF-004: alternate output path silently bypassing a resource limit).

resolve_output_path_no_create added: pure path computation with no create_dir_all side effect, used by watch to compute deletion targets.

New dependencies

Crate Version MSRV Released Rationale
notify 8 1.77 2025-07-03 Cross-platform file watcher (kqueue/FSEvents/inotify)
ctrlc 3.5 1.69 2026-02-10 Ctrl+C handler (thread-safe, installs console handler on Windows)
libc 0.2 dev-dep, unix only SIGINT test via libc::kill

Both deps satisfy the project 30-day cooldown and MSRV 1.88 requirements.

notify-debouncer-full intentionally NOT added — debounce is hand-rolled per the minimal-deps policy.

Breaking Changes

None. The refactor is behavior-preserving; all existing mds build and mds check behavior is unchanged.

Reviewer Focus Areas

  1. PF-004 compliance (build.rs: compile_and_write, read_build_input): both markdown and messages modes go through the same read_build_input enforcing MAX_FILE_SIZE. The test T-U6 exercises the dep-return path; the resolve_output_path_no_create test verifies no mkdir side effect.

  2. ADR-016 compliance (watch.rs: run_watch_file loop): after each successful rebuild, files_of_interest and dirs_to_watch are recomputed from fresh dep output, not from the stale prior set.

  3. Loop bounds: drain_debounce terminates at a deadline = Instant::now() + duration (not while true). The collect_mds_files recursion is bounded by max_depth=64. Both outer watch loops use while let Ok(msg) = rx.recv() (terminates on sender drop or Interrupt).

  4. Path canonicalization (watch.rs): entry, vars file, and event paths are all canonicalized to handle macOS /tmp/private/tmp symlink differences. event_is_relevant tries both raw and canonical forms.

  5. Documented limitations (in Commands::Watch doc comment and README): no reverse-dep tracking in dir mode; entry parent-dir deletion loses the watch; stem collision in flat --out-dir.

Test plan

  • cargo test --workspace passes (912 tests, 0 regressions) — verified locally
  • 6 unit tests: dirs_to_watch, files_of_interest, event_is_relevant, collect_mds_files, output_path_for, compile_and_write dep return
  • 19 integration tests: initial compile, edit, transitive import tracking, dir startup, dir edit, new file pickup, deletion, vars reload, debounce coalescing, error recovery, Ctrl+C (unix), messages format, stdout separation, all invalid-combo rejections, startup error
  • cargo fmt --all --check passes
  • cargo clippy --workspace --all-targets -- -D warnings passes
  • Snyk code scan: 0 issues

dean0x and others added 5 commits June 9, 2026 00:40
… Implements mds watch for milestone v0.3.0 — watches .mds files and auto-recompiles on save, enabling live-preview workflows. Single-file mode tracks transitive @import deps (ADR-016: dep set recomputed on every rebuild). Directory mode compiles each changed .mds independently; on source deletion, removes the matching output file. Includes a behavior-preserving refactor: build logic extracted from main.rs into build.rs with pub(crate) helpers. New compile_and_write helper routes both markdown and messages modes through read_build_input and compile_with_deps, preserving the 10 MiB file-size cap on every code path (avoids PF-004). New deps: notify 8 (MSRV 1.77, stable 2025-07-03) and ctrlc 3.5 (MSRV 1.69, stable 2026-02-10) — both satisfy the 30-day cooldown and MSRV 1.88 requirements. 6 new unit tests (T-U1..T-U6) and 19 integration tests. All 912 workspace tests pass, 0 regressions. Co-Authored-By: Claude <noreply@anthropic.com>
…lish

Add 7 new integration tests to crates/mds-cli/tests/cli_watch.rs covering:
- AC-F3: import-removal resync: verifies both add AND remove directions of
  dynamic dep tracking, T-I4 completion
- AC-F7: dir mode vars-recompile-all: editing vars.json triggers rebuild of
  all .mds files in directory mode
- AC-A5: quiet mode keeps compile errors visible: -q suppresses status messages
  but errors still appear on stderr; watcher stays alive and recovers
- AC-F9: "Stopped watching." message on Ctrl+C: extends T-I16 to assert the
  message appears on stderr
- AC-P1: debounce coalesces burst: counts "Recompiled" lines to assert <= 2
  rebuilds from a 10-write burst in a 250ms window
- AC-F10: watch no-arg auto-detect: single .mds in cwd is compiled; multiple
  .mds files without explicit arg yields non-zero exit with descriptive error

Also includes Simplifier's canonicalize_vars_path extraction in watch.rs and
Scrutinizer's T-I10 --clear test rewrite with piped stderr and rebuild trigger.

Total mds-cli test count: 321 (was 314). All pass, 0 regressions.

Co-Authored-By: Claude <noreply@anthropic.com>
…rite in watch (#57)

macOS FSEvents delivers synthetic events for source files immediately after
watcher registration, causing the watch loop to fire 2-3 extra compile cycles
on startup even with no user edits. With -o - (stdout output) this writes the
compiled content 2-3x to stdout, corrupting downstream pipe consumers.

Fix: content-based output dedup in the watch loop.

- Add compile_to_content in build.rs: returns (content, deps) without
  writing. compile_and_write (used by build subcommand) is now a thin
  wrapper that always calls compile_to_content then write_output - build
  behavior is unchanged.
- After the initial compile, record a baseline by re-compiling (quiet, no
  write) and storing the content in last_written keyed by resolved output
  path (or "<stdout>" sentinel for -o -).
- In the watch loop: compile to content, compare with last_written, skip
  the write and "Recompiled" summary line when content is byte-identical.
  Dep set and watched dirs are still resynced regardless (ADR-016).
- Flush stdout after each write to stdout so pipe consumers receive data
  before a potential SIGKILL in tests or pipeline teardowns.

Regression tests added (QA-R1, QA-R2) in cli_watch.rs without -q so
stderr/stdout noise is directly observable:
- QA-R1: no edits at startup yields exactly 1 "Compiled to" and 0 "Recompiled"
- QA-R2: -o - with no edits writes compiled content EXACTLY ONCE to stdout

Both tests confirm: before fix stdout had 3x writes; after fix exactly 1.
All 921 workspace tests pass, 0 regressions.

Co-Authored-By: Claude <noreply@anthropic.com>
…build status line (#57)

Mirror the file-mode content-dedup approach in run_watch_dir: maintain a
HashMap<PathBuf, String> of last-written content keyed by resolved output
path. Populate the baseline after startup compile (but after watcher
registration) so synthetic FSEvents from macOS are recognised as no-ops.
In-loop rebuilds compile to content first, compare, and skip write + summary
when content is unchanged. Deletion path removes the entry from the dedup map.

Add an announce: bool parameter to write_output. In-loop rebuild writes
pass announce=false so only the "Recompiled" summary line is emitted.
Startup compiles and mds build continue to pass announce=true.

Add QA-R3: dir-mode no-spurious-startup-recompile integration test.
Add QA-R4: single-status-line-per-rebuild integration test.

All 923 workspace tests pass.

Co-Authored-By: Claude <noreply@anthropic.com>
… - features/mds-cli/KNOWLEDGE.md + index.json: knowledge base for the CLI front-end (build/check/watch subcommands, output-path/mds.json-config/ runtime-vars resolution, the notify+ctrlc watch architecture with debounce and content-dedup, and the stdout/stderr stream contract / exit-code mapping). - decisions.md / pitfalls.md: ADR-019 (.devflow ignore-by-default tracking), ADR-020 (don't reset the worktree while background maintenance writes .devflow), PF-005 (git reset --hard + checkout deletes worktree copies of files untracked on the destination) — recorded by background maintenance during this work.
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.

feat: mds watch command (file watcher with auto-recompile)

1 participant