This document is a small experiment in defining command-line options once and projecting them into several surfaces. The point is not to replace clap today. The point is to test a better source of truth.

The pressure is straightforward:

  • the current CLI parser is hand-authored Rust

  • the CLI reference is hand-authored prose

  • future Python tooling may want an argparse projection too

  • stale help text, defaults, examples, and rationale are exactly the kind of drift that literate source should reduce

This is a better fit for X-macro style reuse than classic GoF-style pattern generation. A command-line option is one concept with multiple projections: syntax, parser semantics, examples, rationale, and machine-readable facts.

That said, the cost is real and easy to underestimate. A projection system does not remove complexity; it moves part of it into the source-of-truth layer. That can create:

  • cognitive overhead for readers

  • unfamiliarity for contributors

  • tighter coupling between concerns that were previously independent

  • weak intermediate abstractions that are neither obvious nor general

  • extra policy decisions about when to use the meta-layer and when to write code directly

That last point matters more than it first appears. If every new option forces a fresh judgment call between "use the option spec" and "just write it directly", the system adds decision tax and reduces uniformity at the same time. That is a bad trade. A partial abstraction is often worse than either:

  • direct hand-written code and docs, or

  • a fully adopted projection system for a clearly bounded surface

So this experiment should be judged harshly. If it merely relocates drift into "is this option worth the macro?" discussions, it failed.

The experiment stays intentionally narrow:

  • one renderer script in Python 3.14+

  • one sample command spec for wb-tangle

  • four projections:

  • a Clap-like Rust snippet

  • an argparse snippet

  • an AsciiDoc table

  • a JSON facts file

If this proves useful, the same intermediate model can later project into the real clap code, MCP schemas, docs checks, and structured agent-facing facts.

Failure criteria

This experiment is only worth keeping if it reduces more pain than it creates. It should be considered a failure if one or more of these become true:

  • the intermediate declaration is harder to read than the hand-written clap or argparse code it replaces

  • contributors need project-specific lore before they can add or modify an option

  • the team keeps debating per-option whether the projection layer is "worth it"

  • the generated views are still routinely edited manually, defeating the point

  • the projections start chasing speculative future uses instead of solving current drift

The best outcome is not "macro all the things". The best outcome is to discover whether there is a narrow, high-drift surface where a shared declaration really is simpler than duplication.

Overview

PlantUML diagram

Files

The script is deliberately stdlib-only. That keeps the experiment cheap to run in CI, in a dev shell, or inside a container.

# <<@file scripts/option_spec/render.py>>=
"""Render a small CLI option-spec into several projections.

The schema is intentionally narrow. The goal is to validate the "define once,
project everywhere" shape before touching the real weaveback CLI.
"""

import argparse
import json
from dataclasses import dataclass
from pathlib import Path
import tomllib


type ValueKind = str


@dataclass(slots=True, frozen=True)
class OptionSpec:
    command: str
    id: str
    long: str
    short: str | None
    value_kind: ValueKind
    default: str | bool | int | None
    help_short: str
    rationale: str
    examples: tuple[str, ...]
    doc: str | None = None
    required: bool = False
    repeatable: bool = False

    def validate(self) -> None:
        if not self.id:
            raise ValueError("option id must not be empty")
        if not self.long:
            raise ValueError(f"option {self.id} must declare a long name")
        if self.value_kind not in {"bool", "string", "path", "int"}:
            raise ValueError(
                f"option {self.id} uses unsupported value_kind {self.value_kind!r}"
            )
        if self.short and len(self.short) != 1:
            raise ValueError(f"option {self.id} short name must be one character")
        if self.value_kind == "bool" and self.repeatable:
            raise ValueError(f"boolean flag {self.id} must not be repeatable")


@dataclass(slots=True, frozen=True)
class CommandSpec:
    name: str
    summary: str
    rationale: str
    examples: tuple[str, ...]
    options: tuple[OptionSpec, ...]

    def validate(self) -> None:
        if not self.name:
            raise ValueError("command name must not be empty")
        if not self.options:
            raise ValueError(f"command {self.name} must declare at least one option")
        seen: set[str] = set()
        for option in self.options:
            option.validate()
            if option.command != self.name:
                raise ValueError(
                    f"option {option.id} belongs to {option.command}, expected {self.name}"
                )
            if option.id in seen:
                raise ValueError(f"duplicate option id {option.id!r}")
            seen.add(option.id)


def load_spec(path: Path) -> CommandSpec:
    raw = tomllib.loads(path.read_text(encoding="utf-8"))
    command_raw = raw["command"]
    command_name = str(command_raw["name"])
    options = tuple(
        OptionSpec(
            command=command_name,
            id=str(item["id"]),
            long=str(item["long"]),
            short=str(item["short"]) if item.get("short") else None,
            value_kind=str(item["value_kind"]),
            default=item.get("default"),
            help_short=str(item["help_short"]),
            rationale=str(item["rationale"]),
            examples=tuple(str(example) for example in item.get("examples", [])),
            doc=str(item["doc"]) if item.get("doc") else None,
            required=bool(item.get("required", False)),
            repeatable=bool(item.get("repeatable", False)),
        )
        for item in raw.get("options", [])
    )
    command = CommandSpec(
        name=command_name,
        summary=str(command_raw["summary"]),
        rationale=str(command_raw["rationale"]),
        examples=tuple(str(example) for example in command_raw.get("examples", [])),
        options=options,
    )
    command.validate()
    return command


def rust_type_for(option: OptionSpec) -> str:
    if option.value_kind == "bool":
        return "bool"
    if option.value_kind == "path":
        return "std::path::PathBuf"
    if option.value_kind == "int":
        return "i64"
    return "String"


def default_literal(option: OptionSpec) -> str | None:
    if option.default is None:
        return None
    if isinstance(option.default, bool):
        return str(option.default).lower()
    return str(option.default)


def render_clap(command: CommandSpec) -> str:
    lines = [
        f"// Generated option projection for weaveback {command.name}",
        f"// {command.summary}",
        "",
    ]
    for option in command.options:
        lines.append(f"/// {option.help_short}")
        if option.doc:
            lines.append(f"/// {option.doc}")
        arg_parts = [f'long = "{option.long}"']
        if option.short:
            arg_parts.append(f"short = '{option.short}'")
        default = default_literal(option)
        if default is not None:
            arg_parts.append(f'default_value = "{default}"')
        if option.repeatable:
            arg_parts.append('value_name = "VALUE"')
        lines.append(f"#[arg({', '.join(arg_parts)})]")
        lines.append(f"{option.id}: {rust_type_for(option)},")
        lines.append("")
    return "\n".join(lines).rstrip() + "\n"


def render_argparse(command: CommandSpec) -> str:
    lines = [
        f'"""Generated argparse projection for weaveback {command.name}."""',
        "",
        "from argparse import ArgumentParser",
        "from pathlib import Path",
        "",
        "",
        "def build_parser() -> ArgumentParser:",
        f'    parser = ArgumentParser(prog="weaveback {command.name}")',
        f'    parser.description = "{command.summary}"',
    ]
    for option in command.options:
        flags = [f'"--{option.long}"']
        if option.short:
            flags.append(f'"-{option.short}"')
        kwargs: list[str] = []
        if option.value_kind == "bool":
            kwargs.append('action="store_true"')
        elif option.value_kind == "path":
            kwargs.append("type=Path")
        elif option.value_kind == "int":
            kwargs.append("type=int")
        if option.default is not None:
            kwargs.append(f"default={option.default!r}")
        kwargs.append(f"help={option.help_short!r}")
        lines.append(f"    parser.add_argument({', '.join(flags + kwargs)})")
    lines.append("    return parser")
    lines.append("")
    return "\n".join(lines)


def render_adoc(command: CommandSpec) -> str:
    lines = [
        f"== `{command.name}` option projection",
        "",
        command.summary,
        "",
        command.rationale,
        "",
        '[cols="2,1,3,4,3",options="header"]',
        "|===",
        "| Flag | Type | Default | Description | Why",
        "",
    ]
    for option in command.options:
        default = default_literal(option) or ""
        option_type = option.value_kind
        flag = f"`--{option.long}`"
        if option.short:
            flag += f" / `-{option.short}`"
        lines.append(
            f"| {flag} | `{option_type}` | `{default}` | {option.help_short} | {option.rationale}"
        )
    lines.extend(["|===", ""])
    if command.examples:
        lines.append("=== Examples")
        lines.append("")
        lines.append("[source,bash]")
        lines.append("----")
        lines.extend(command.examples)
        lines.append("----")
        lines.append("")
    return "\n".join(lines)


def render_facts(command: CommandSpec) -> str:
    payload = {
        "command": {
            "name": command.name,
            "summary": command.summary,
            "rationale": command.rationale,
            "examples": list(command.examples),
        },
        "options": [
            {
                "id": option.id,
                "long": option.long,
                "short": option.short,
                "value_kind": option.value_kind,
                "default": option.default,
                "help_short": option.help_short,
                "rationale": option.rationale,
                "examples": list(option.examples),
                "doc": option.doc,
                "required": option.required,
                "repeatable": option.repeatable,
            }
            for option in command.options
        ],
    }
    return json.dumps(payload, indent=2, sort_keys=True) + "\n"


def render_all(spec_path: Path, out_dir: Path) -> None:
    command = load_spec(spec_path)
    out_dir.mkdir(parents=True, exist_ok=True)
    stem = command.name.replace("-", "_")
    (out_dir / f"{stem}_clap.rs.inc").write_text(render_clap(command), encoding="utf-8")
    (out_dir / f"{stem}_argparse.py").write_text(
        render_argparse(command), encoding="utf-8"
    )
    (out_dir / f"{stem}_options.adoc").write_text(render_adoc(command), encoding="utf-8")
    (out_dir / f"{stem}_facts.json").write_text(render_facts(command), encoding="utf-8")


def main() -> None:
    parser = argparse.ArgumentParser()
    parser.add_argument("--spec", type=Path, required=True)
    parser.add_argument("--out", type=Path, required=True)
    args = parser.parse_args()
    render_all(args.spec, args.out)


if __name__ == "__main__":
    main()
# @

The sample spec keeps the experiment honest by covering two different option shapes:

  • --config, a typed value option with a default

  • --force-generated, a boolean flag with safety-oriented rationale

# <<@file scripts/option_spec/specs/tangle.toml>>=
[command]
name = "tangle"
summary = "Run all tangle passes from weaveback.toml or an alternate config file."
rationale = "This is the narrowest useful pilot because tangle already mixes parser semantics, recovery flags, and documentation drift."
examples = [
  "wb-tangle",
  "wb-tangle --config alt.toml",
  "wb-tangle --force-generated",
]

[[options]]
id = "config"
long = "config"
short = "c"
value_kind = "path"
default = "weaveback.toml"
help_short = "Path to the tangle config file."
rationale = "Lets the same binary operate on alternate roots and experimental pass graphs."
doc = "Reads the pass list and parser settings from a specific config file instead of the default weaveback.toml."
examples = ["wb-tangle --config docs-only.toml"]

[[options]]
id = "force_generated"
long = "force-generated"
value_kind = "bool"
default = false
help_short = "Overwrite generated files even if they differ from the stored baseline."
rationale = "Makes recovery explicit when literate source is authoritative and generated files have drifted locally."
doc = "This is an escape hatch for regeneration workflows, not the default safety path."
examples = ["wb-tangle --force-generated"]
# @

The test stays cheap too: invoke the script in a temporary directory and check for the expected projections.

# <<@file scripts/option_spec/tests/test_render.py>>=
import json
from pathlib import Path
import subprocess
import sys
from tempfile import TemporaryDirectory
import unittest


ROOT = Path(__file__).resolve().parents[3]
SCRIPT = ROOT / "scripts" / "option_spec" / "render.py"
SPEC = ROOT / "scripts" / "option_spec" / "specs" / "tangle.toml"


class RenderOptionSpecTest(unittest.TestCase):
    def test_render_all_projections(self) -> None:
        with TemporaryDirectory() as tmpdir:
            out_dir = Path(tmpdir)
            subprocess.run(
                [sys.executable, str(SCRIPT), "--spec", str(SPEC), "--out", str(out_dir)],
                check=True,
                cwd=ROOT,
            )

            clap = (out_dir / "tangle_clap.rs.inc").read_text(encoding="utf-8")
            argparse_py = (out_dir / "tangle_argparse.py").read_text(encoding="utf-8")
            adoc = (out_dir / "tangle_options.adoc").read_text(encoding="utf-8")
            facts = json.loads((out_dir / "tangle_facts.json").read_text(encoding="utf-8"))

            self.assertIn('long = "config"', clap)
            self.assertIn("force_generated: bool", clap)
            self.assertIn('parser.add_argument("--force-generated"', argparse_py)
            self.assertIn("| `--config` / `-c` | `path` | `weaveback.toml`", adoc)
            self.assertEqual(facts["command"]["name"], "tangle")
            self.assertEqual(facts["options"][1]["long"], "force-generated")


if __name__ == "__main__":
    unittest.main()
# @

This is not yet the real CLI source of truth. It is a proving ground for three questions:

  1. does a compact intermediate model stay readable?

  2. do the projections actually reduce drift?

  3. is the rationale field useful enough to justify the extra structure?

If the answers are yes, the next step is not "macro everything". The next step is to move one real command at a time behind the intermediate model and let facts from that model feed a future docs-check command.