This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
numd is a Nushell module for creating reproducible Markdown documents. It executes Nushell code blocks within markdown files and writes results back to the document.
# Run numd on a markdown file (updates file with execution results)
use numd; numd render README.md
# Preview mode (output to stdout, don't save)
use numd; numd render README.md --echo
# Dry run: return blocks that would execute (block_index, infostring, code), execute nothing
use numd; numd render README.md --dry-run
# Run tests (executes all example files and reports changes)
nu toolkit.nu test --json
# Clear outputs from a markdown file
use numd; numd clear-outputs path/to/file.md
# Strip markdown to get pure Nushell script
use numd; numd clear-outputs path/to/file.md --strip-markdown --echorender refuses to overwrite a git-tracked file that has uncommitted or staged changes ("... has uncommitted changes") — commit, stash, or pass --ignore-git-check.
The gate is on the save path only: --echo and clear-outputs are never blocked.
- mod.nu: Entry point exporting user-friendly commands (
render,clear-outputs, etc.) - plumbing.nu: Low-level pipeline commands for advanced usage/scripting
- commands.nu: Core implementation containing all command logic
- md-parser.nu:
parse-mdcommand parsing markdown into a table of semantic blocks (headers, paragraphs, code blocks, lists, blockquotes, frontmatter) - doc.nu:
doccommand rendering markdown docs for a module or command fromscopedata - parse-help.nu:
parse-helpcommand for formatting --help output - parse.nu: Frontmatter parsing utilities (
parse-frontmatter,to md-with-frontmatter)
Low-level composable commands (import via use numd/plumbing.nu):
use numd/plumbing.nu
# Parse markdown file into blocks table
plumbing parse-file file.md
# Strip output lines (# =>) from blocks
plumbing parse-file file.md | plumbing strip-outputs
# Execute code blocks and update with results
plumbing parse-file file.md | plumbing execute-blocks --save-intermed-script temp.nu
# Render blocks table back to markdown
plumbing parse-file file.md | plumbing strip-outputs | plumbing to-markdown
# Extract pure Nushell script (no markdown)
plumbing parse-file file.md | plumbing strip-outputs | plumbing to-numd-scriptThe high-level commands use these internally:
render=parse-file | execute-blocks | to-markdownclear-outputs=parse-file | strip-outputs | to-markdown
-
parse-markdown-to-blocks: Parses markdown into a table classifying each block by type (text,```nushell,```output-numd, and<!-- numd-gen: ... -->generate-regions) and action (execute,print-as-it-is,delete) -
decorate-original-code-blocks+generate-intermediate-script: Transforms executable code blocks into a temporary.nuscript with markers for output capture -
execute-intermediate-script: Runs the generated script in a new Nushell instance, capturing stdout -
extract-block-index+merge-markdown: Parses execution results using#code-block-marker-open-Nmarkers and merges them back into the original markdown structure
Blocks support fence options (e.g., ```nushell try, no-output):
no-run: Skip executionno-output: Execute but hide outputtry: Wrap in try-catchnew-instance: Execute in separate Nushell instanceseparate-block: Output results in separate code block instead of inline# =>run-once: Execute code block once, then set tono-run
- Code blocks are split by blank lines (double newlines) into command groups
- Each command group is executed separately via
split-by-blank-lines - Lines starting with
# =>contain output from previous command group - Plain
#comments are preserved;# =>output lines are regenerated on each run - Use
separate-blockfence option to output results in a separate code block instead of inline# =>
# Run all tests (unit + integration)
nu toolkit.nu test
# Run only unit tests (nutest-based, tests internal functions)
nu toolkit.nu test-unit
# Run only integration tests (executes example markdown files)
nu toolkit.nu test-integrationOutput mode is auto-detected. When stdout is a terminal you get the human view — only the non-passing tests (with the assertion message on failure), then a N passed, M failed summary.
When stdout is piped or redirected (agents, CI) you get machine-readable JSON instead.
This uses is-terminal --stdout, not $nu.is-interactive (which is false for any nu toolkit.nu ... script run and so cannot tell agent from human).
nu toolkit.nu test --json # force JSON even on a terminal
nu toolkit.nu test --pretty # force the human view even when piped
nu toolkit.nu test --all # human view: also list passing testsJSON rows use a flat schema — {type, name, status: 'passed'|'failed'|'changed', file, message}.
Note status (not nutest's PASS/FAIL result column); message holds the assertion text on failure, null otherwise.
The JSON channel always carries every row — the failures-only trimming applies to the human view only.
Unit tests use nutest framework.
Tests import internal functions via use ../numd/commands.nu * to test parsing and transformation logic directly.
The test-integration command:
- Runs all example files in
z_examples/through numd - Generates stripped
.nuversions inz_examples/99_strip_markdown/ - Runs
numd render README.mdto update README with latest outputs - Runs
git diff --quieton each touched file and reportspassed(no diff),changed(diff) orfailed(the run raised an error)
Example files serve as integration tests - read git diff to verify changes, and re-run with --update to stage the ones you meant to make.
Example outputs are machine-independent by construction: no host-dependent commands (whoami was removed for this reason), absolute paths are masked in the docs that display them, and test-integration pins TZ=UTC for every spawned intermediate script.
Two sources of legitimate diffs remain:
- Dynamic content:
git tagoutput in README.md (version changes over time) - Nushell version changes: Error message formatting, table rendering differences.
The README embeds the Nushell version the outputs were produced with (a
numd-genregion in the testing section), so a version bump labels its own churn.
So a run where every file is passed except the dynamic content files, which come back changed, is a passing run.
Any other changed file on an unchanged module is a bug, not noise.
Unit tests load nutest from the sibling repo via use ../nutest/nutest (see toolkit.nu).
Claude's --worktree puts worktrees under .claude/worktrees/<name>/, three levels below the repo root, so from inside a worktree ../nutest resolves to .claude/worktrees/nutest — which is empty.
nu toolkit.nu test-unit then fails at parse time with "module not found".
Fix it once with a single symlink at that spot, pointing back to the real sibling.
Every worktree resolves ../nutest to the same path, so one symlink serves all current and future worktrees:
ln -sfn ../../../nutest .claude/worktrees/nutest.claude/ is gitignored, so the symlink is never committed and never follows a worktree branch.
Integration tests (test-integration) don't need it — they run numd on the example files and have no nutest dependency.
This one symlink covers worktrees placed directly under .claude/worktrees/, which is the normal single-segment name (--worktree bugfix).
A name with a slash nests the worktree deeper and escapes the symlink, so keep worktree names flat.
By default, numd runs intermediate scripts with nu --no-config-file for reproducible output across different systems.
Use --use-host-config to load host's env, config, and plugin files when needed.
Use --eval to prepend Nushell code to the intermediate script:
# Inline code
numd render README.md --eval '$env.numd.table-width = 80'
# From config file
numd render README.md --eval (open --raw z_examples/numd_config_example1.nu)Example config files: z_examples/numd_config_example1.nu sets $env.config.footer_mode and a $env.config.table record; z_examples/numd_config_example2.nu also sets $env.numd.table-width, the width passed to table when rendering output (default 120).
- Do not squash commits when merging PRs - preserve individual commit history