chore: workplan MRKD-WP-0001 + improved CLAUDE.md; document next steps

- workplans/MRKD-WP-0001-foundation-level1.md: 8-task workplan for
  Foundation & LEVEL1 Core (T01 scaffolding through T08 e2e harness)
- CLAUDE.md: added Planned Architecture section (interface table, FR
  domain map, key concepts, round-trip data flow) and Development
  Commands stubs derived from FRS v0.2
- problems/next-steps-2026-03-14.md: implementation guide for next
  session — task order, dep list, state-hub task UUIDs, quality gates

No code implemented yet; workstream registered in State Hub.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-14 18:18:54 +01:00
parent d298e2989d
commit c6dfc9b172
12 changed files with 4465 additions and 1 deletions

View File

@@ -0,0 +1,3 @@
"""markidocx — Markdown ↔ DOCX round-trip editing system."""
__version__ = "0.1.0"

261
src/markidocx/cli.py Normal file
View File

@@ -0,0 +1,261 @@
"""CLI entry point for markidocx."""
from __future__ import annotations
import json
import sys
from pathlib import Path
from typing import Annotated, Optional
import typer
from rich.console import Console
from rich.table import Table
app = typer.Typer(
name="markidocx",
help="Markdown ↔ DOCX round-trip editing system.",
add_completion=False,
)
template_app = typer.Typer(help="Template family management.")
app.add_typer(template_app, name="template")
console = Console()
err_console = Console(stderr=True)
@app.command()
def validate(
manifest: Annotated[Path, typer.Argument(help="Path to manifest YAML file")],
json_output: Annotated[bool, typer.Option("--json", help="Machine-readable JSON output")] = False,
) -> None:
"""Validate a manifest file (FR-100)."""
from markidocx.manifest import ManifestError, load_manifest
try:
m = load_manifest(manifest)
if json_output:
typer.echo(json.dumps({"status": "ok", "project": m.project.name}))
else:
console.print(f"[green]✓[/green] Manifest valid: [bold]{m.project.name}[/bold]")
raise typer.Exit(0)
except ManifestError as exc:
if json_output:
typer.echo(json.dumps({"status": "error", "message": str(exc)}))
else:
err_console.print(f"[red]✗ Manifest error:[/red] {exc}")
raise typer.Exit(1)
@app.command()
def build(
manifest: Annotated[Path, typer.Argument(help="Path to manifest YAML file")],
json_output: Annotated[bool, typer.Option("--json", help="Machine-readable JSON output")] = False,
) -> None:
"""Build a DOCX from Markdown sources (FR-200)."""
from markidocx.builder import build_document
from markidocx.manifest import ManifestError, load_manifest
try:
m = load_manifest(manifest)
except ManifestError as exc:
if json_output:
typer.echo(json.dumps({"status": "error", "message": str(exc)}))
else:
err_console.print(f"[red]✗ Manifest error:[/red] {exc}")
raise typer.Exit(1)
result = build_document(m)
if json_output:
typer.echo(
json.dumps(
{
"status": "ok" if result.success else "error",
"output_path": str(result.output_path),
"family": result.family,
"feature_level": result.feature_level,
"warnings": result.warnings,
"errors": result.errors,
}
)
)
else:
if result.success:
console.print(f"[green]✓[/green] Built: [bold]{result.output_path}[/bold]")
for w in result.warnings:
console.print(f"[yellow]⚠[/yellow] {w}")
else:
for e in result.errors:
err_console.print(f"[red]✗[/red] {e}")
raise typer.Exit(0 if result.success else 1)
@app.command("import")
def import_docx(
manifest: Annotated[Path, typer.Argument(help="Path to manifest YAML file")],
docx: Annotated[Path, typer.Argument(help="Path to DOCX file to import")],
json_output: Annotated[bool, typer.Option("--json", help="Machine-readable JSON output")] = False,
) -> None:
"""Import a DOCX back to Markdown (FR-300)."""
from markidocx.importer import import_document
from markidocx.manifest import ManifestError, load_manifest
try:
m = load_manifest(manifest)
except ManifestError as exc:
if json_output:
typer.echo(json.dumps({"status": "error", "message": str(exc)}))
else:
err_console.print(f"[red]✗ Manifest error:[/red] {exc}")
raise typer.Exit(1)
result = import_document(m, docx)
if json_output:
typer.echo(
json.dumps(
{
"status": "ok" if result.success else "error",
"output_files": [str(p) for p in result.output_files],
"mapping_status": result.mapping_status,
"warnings": result.warnings,
}
)
)
else:
if result.success:
console.print(f"[green]✓[/green] Imported ({result.mapping_status})")
for f in result.output_files:
console.print(f"{f}")
for w in result.warnings:
console.print(f"[yellow]⚠[/yellow] {w}")
else:
err_console.print("[red]✗ Import failed[/red]")
raise typer.Exit(0 if result.success else 1)
@app.command()
def compare(
manifest: Annotated[Path, typer.Argument(help="Path to manifest YAML file")],
docx: Annotated[Path, typer.Argument(help="Path to DOCX file to compare against")],
json_output: Annotated[bool, typer.Option("--json", help="Machine-readable JSON output")] = False,
) -> None:
"""Compare original Markdown with re-imported DOCX (FR-700)."""
from markidocx.builder import build_document
from markidocx.differ import compare as do_compare
from markidocx.importer import import_document
from markidocx.manifest import ManifestError, load_manifest
try:
m = load_manifest(manifest)
except ManifestError as exc:
if json_output:
typer.echo(json.dumps({"status": "error", "message": str(exc)}))
else:
err_console.print(f"[red]✗ Manifest error:[/red] {exc}")
raise typer.Exit(2)
# Read original markdown
original_parts: list[str] = []
for src in m.sources:
p = manifest.parent / src.path
original_parts.append(p.read_text(encoding="utf-8"))
original_md = "\n\n".join(original_parts)
# Import the docx
result = import_document(m, docx)
if not result.success:
if json_output:
typer.echo(json.dumps({"status": "error", "message": "Import failed"}))
else:
err_console.print("[red]✗ Import failed — cannot compare[/red]")
raise typer.Exit(2)
reimported_parts: list[str] = []
for f in result.output_files:
reimported_parts.append(Path(f).read_text(encoding="utf-8"))
reimported_md = "\n\n".join(reimported_parts)
report = do_compare(original_md, reimported_md)
if json_output:
typer.echo(
json.dumps(
{
"has_drift": report.has_drift,
"preserved": report.preserved,
"degraded": report.degraded,
"broken": report.broken,
"unsupported": report.unsupported,
}
)
)
else:
if report.has_drift:
console.print("[yellow]⚠ Drift detected[/yellow]")
for item in report.degraded:
console.print(f" [yellow]degraded:[/yellow] {item}")
for item in report.broken:
console.print(f" [red]broken:[/red] {item}")
else:
console.print("[green]✓[/green] No drift detected")
if report.preserved:
console.print(f" preserved: {len(report.preserved)} elements")
raise typer.Exit(1 if report.has_drift else 0)
@template_app.command("list")
def template_list(
json_output: Annotated[bool, typer.Option("--json", help="Machine-readable JSON output")] = False,
) -> None:
"""List available template families (FR-603)."""
from markidocx.templates import FamilyRegistry
registry = FamilyRegistry()
families = registry.list_families()
if json_output:
typer.echo(
json.dumps(
[{"name": f.name, "description": f.description} for f in families]
)
)
else:
table = Table(title="Template Families")
table.add_column("Name", style="bold")
table.add_column("Description")
for f in families:
table.add_row(f.name, f.description)
console.print(table)
@template_app.command("register")
def template_register(
path: Annotated[Path, typer.Argument(help="Path to .docx template")],
name: Annotated[str, typer.Option("--name", help="Family name")] = "",
description: Annotated[str, typer.Option("--description", help="Family description")] = "",
json_output: Annotated[bool, typer.Option("--json", help="Machine-readable JSON output")] = False,
) -> None:
"""Register a custom template family (FR-605)."""
from markidocx.templates import FamilyRegistry
registry = FamilyRegistry()
if not name:
name = path.stem
try:
registry.register(path, name, description)
if json_output:
typer.echo(json.dumps({"status": "ok", "name": name}))
else:
console.print(f"[green]✓[/green] Registered template: [bold]{name}[/bold]")
except Exception as exc:
if json_output:
typer.echo(json.dumps({"status": "error", "message": str(exc)}))
else:
err_console.print(f"[red]✗[/red] {exc}")
raise typer.Exit(1)
if __name__ == "__main__":
app()