Skip to content

graflo.cli.canonical

graflo canonical-check -- a canonical map against the manifest it maps.

A canonical map is authored against one schema long before it is merged against another, and until now the only thing that would check it was a full graflo merge: two manifests, an op, and a refusal naming one entry at a time. A map of hundreds of entries is not authored that way.

This verb is that check on its own. With one manifest it classifies every entry directly; with both it runs the merge preview, which puts each declaration through the rules merge uses without merging, so one bad entry does not hide the rest. --trim writes the map narrowed to what the manifest actually declares -- an authoring step with a diff, rather than something merge does silently at the far end of a pipeline.

Attributes

EXIT_DANGLING = 1 module-attribute

__all__ = ['canonical_check'] module-attribute

Classes

Functions:

canonical_check(map_path, left_path, right_path, scope, trim_path, as_json, exit_zero)

Check the canonical map at MAP_PATH against the manifest(s) it maps.

Source code in graflo/cli/canonical.py
@click.command("canonical-check")
@click.argument(
    "map_path", type=click.Path(exists=True, dir_okay=False, path_type=Path)
)
@click.option(
    "--left",
    "left_path",
    type=click.Path(exists=True, dir_okay=False, path_type=Path),
    default=None,
    help="The left manifest the map is checked against.",
)
@click.option(
    "--right",
    "right_path",
    type=click.Path(exists=True, dir_okay=False, path_type=Path),
    default=None,
    help="The right manifest the map is checked against.",
)
@click.option(
    "--scope",
    type=click.Choice(_SCOPES),
    default="left",
    show_default=True,
    help="Which side the map is scoped to, as on a merge op.",
)
@click.option(
    "--trim",
    "trim_path",
    type=click.Path(dir_okay=False, path_type=Path),
    default=None,
    help=(
        "Write the map narrowed to the entries that match, dropping the rest. "
        "Needs a single manifest to narrow against, so --scope must name one."
    ),
)
@click.option("--json", "as_json", is_flag=True, help="Emit the report as JSON.")
@click.option(
    "--exit-zero",
    is_flag=True,
    help="Report, but always exit 0.",
)
def canonical_check(
    map_path: Path,
    left_path: Path | None,
    right_path: Path | None,
    scope: Scope,
    trim_path: Path | None,
    as_json: bool,
    exit_zero: bool,
) -> None:
    """Check the canonical map at MAP_PATH against the manifest(s) it maps."""
    if left_path is None and right_path is None:
        raise click.UsageError("at least one of --left / --right is required")

    try:
        cm = CanonicalMap.model_validate(load_mapping(map_path))
    except ValueError as exc:
        raise _CanonicalSetupError(
            f"{map_path}: not a valid canonical map -- {type(exc).__name__}: {exc}"
        ) from exc

    try:
        left = load_manifest(left_path) if left_path is not None else None
        right = load_manifest(right_path) if right_path is not None else None
    except (ValueError, TypeError, KeyError) as exc:
        raise _CanonicalSetupError(
            f"could not read a manifest -- {type(exc).__name__}: {exc}"
        ) from exc

    entries: tuple[DanglingEntry, ...] = ()
    if left is not None and right is not None:
        found = _report_preview(left, right, cm, scope, as_json)
        dangling_found = bool(found)
    else:
        side = "right" if right is not None else "left"
        manifest = right if right is not None else left
        assert manifest is not None  # one of the two, checked above
        entries = dangling_entries(cm, manifest, side=side)
        _report_entries(entries, as_json=as_json)
        dangling_found = bool(entries)

    if trim_path is not None:
        _write_trimmed(cm, left, right, scope, trim_path)

    if exit_zero:
        return
    if dangling_found:
        raise SystemExit(EXIT_DANGLING)