Contract framework with markdown-native contracts utilizing fenced YAML blocks

This commit is contained in:
2026-05-03 22:51:13 +02:00
parent 3cfda33bc9
commit e3e13ee45a
36 changed files with 2877 additions and 13 deletions

View File

@@ -9,6 +9,13 @@ import click
import yaml
from markitect_tool.core import parse_markdown_file
from markitect_tool.contract import (
ContractLoaderError,
check_markdown_file,
collect_metrics,
load_contract_file,
validate_contract,
)
from markitect_tool.schema import load_schema_file, validate_markdown_file, validate_schema
@@ -41,6 +48,23 @@ def parse(file: Path, output_format: str) -> None:
click.echo(json.dumps(data, indent=2, ensure_ascii=False))
@main.command()
@click.argument("file", type=click.Path(exists=True, dir_okay=False, path_type=Path))
@click.option(
"--format",
"output_format",
type=click.Choice(["json", "yaml", "text"], case_sensitive=False),
default="text",
show_default=True,
)
def metrics(file: Path, output_format: str) -> None:
"""Report practical size and complexity metrics for a Markdown file."""
document = parse_markdown_file(file)
data = collect_metrics(document).to_dict() | {"document_path": str(file)}
_emit_metrics(data, output_format)
@main.command()
@click.argument("file", type=click.Path(exists=True, dir_okay=False, path_type=Path))
@click.option(
@@ -88,6 +112,54 @@ def schema_validate(schema_file: Path, output_format: str) -> None:
raise click.exceptions.Exit(0 if result.valid else 1)
@main.group()
def contract() -> None:
"""Work with Markdown document contracts."""
@contract.command("validate")
@click.argument("contract_file", type=click.Path(exists=True, dir_okay=False, path_type=Path))
@click.option(
"--format",
"output_format",
type=click.Choice(["json", "yaml", "text"], case_sensitive=False),
default="text",
show_default=True,
)
def contract_validate(contract_file: Path, output_format: str) -> None:
"""Validate that a Markdown contract file is well formed."""
result = validate_contract(load_contract_file(contract_file))
_emit_diagnostic_result(result.to_dict(), output_format)
raise click.exceptions.Exit(0 if result.valid else 1)
@contract.command("check")
@click.argument("file", type=click.Path(exists=True, dir_okay=False, path_type=Path))
@click.option(
"--contract",
"contract_file",
required=True,
type=click.Path(exists=True, dir_okay=False, path_type=Path),
)
@click.option(
"--format",
"output_format",
type=click.Choice(["json", "yaml", "text"], case_sensitive=False),
default="text",
show_default=True,
)
def contract_check(file: Path, contract_file: Path, output_format: str) -> None:
"""Check a Markdown file against a Markdown document contract."""
try:
result = check_markdown_file(file, contract_file)
except ContractLoaderError as exc:
raise click.ClickException(str(exc)) from exc
_emit_diagnostic_result(result.to_dict(), output_format)
raise click.exceptions.Exit(0 if result.valid else 1)
def _emit_result(data: dict, output_format: str) -> None:
if output_format == "json":
click.echo(json.dumps(data, indent=2, ensure_ascii=False))
@@ -102,5 +174,45 @@ def _emit_result(data: dict, output_format: str) -> None:
click.echo(f"- {violation['path']}: {violation['message']}")
def _emit_diagnostic_result(data: dict, output_format: str) -> None:
if output_format == "json":
click.echo(json.dumps(data, indent=2, ensure_ascii=False))
elif output_format == "yaml":
click.echo(yaml.safe_dump(data, sort_keys=False))
else:
click.echo("valid" if data.get("valid") else "invalid")
for diagnostic in data.get("diagnostics", []):
click.echo(
f"- [{diagnostic['severity']}] {diagnostic['code']}: "
f"{diagnostic['message']}"
)
if diagnostic.get("source"):
source = diagnostic["source"]
suffix = f":{source['line']}" if source.get("line") else ""
click.echo(f" source: {source.get('path', '<document>')}{suffix}")
if diagnostic.get("guidance"):
click.echo(f" guidance: {diagnostic['guidance']}")
def _emit_metrics(data: dict, output_format: str) -> None:
if output_format == "json":
click.echo(json.dumps(data, indent=2, ensure_ascii=False))
elif output_format == "yaml":
click.echo(yaml.safe_dump(data, sort_keys=False))
else:
doc = data["document"]
click.echo("document")
for metric, value in doc.items():
click.echo(f"- {metric}: {value}")
sections = data.get("sections", [])
if sections:
click.echo("sections")
for section in sections:
click.echo(
f"- {section['heading']}: words={section['words']}, "
f"paragraphs={section['paragraphs']}, line={section['line']}"
)
if __name__ == "__main__":
main()