info-tech-canon/src/info_tech_canon/cli.py
tegwick e1a6314131
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 3s
Check import manifests by hash and name together (INFO-WP-0028 T01-T03)
import-review takes any partner manifest and returns, per concept, whether the
name resolves in the ownership index and to which artifact, and per entry
whether the pinned SHA-256 matches the blob at the declared source commit. Both
run in one pass so neither can be recorded without the other, which is the
failure this workplan exists to prevent. It exits non-zero on a finding, reads
JSON or YAML, needs no partner checkout, and carries its own limit: resolution
proves a name exists and names one owner, nothing more.

Accepted manifests are registered under infospace/interfaces/manifests/ as
provenance-preserving copies owned by the partner, with the partner revision and
retrieval date recorded. Editing a copy to make a check pass is forbidden in the
file itself. Validation re-resolves them and reports drift as
federation_import_drift, a warning naming the partner rather than an error,
because a stale partner pin is not this repository's file to fix.

The review kit gains an extension-boundary-review template requiring hash count,
resolution count and conflict count as three separate lines, and an operating
rule saying one is never evidence of another. Both boundary files carry the
standing-check result.

Verified live: security-canon resolves 11 of 11, interface-canon 23 of 25 with
the two known Interface and Endpoint pins. make check passes with 58 tests,
clean validation and those two warnings.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 3588@bnt-lap001
Assistant-Session: 24b80f66-e5a7-4e61-99fe-2d422e6d17da
2026-09-20 23:43:34 +02:00

307 lines
10 KiB
Python

from __future__ import annotations
import argparse
import json
import sys
from collections.abc import Callable
from pathlib import Path
from typing import Any
from .api import serve
from .service import (
CanonServiceError,
alignment_template,
artifact_graph,
generate_agent_briefs,
generate_indexes,
generate_tree,
inspect_canon,
list_artifacts,
list_models,
list_standards,
list_views,
profile_graph,
profile_inspect,
profile_validate,
read_view,
review_capability_record,
review_kit,
validate_canon,
write_validation_report,
)
Command = Callable[[argparse.Namespace], dict[str, Any]]
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(prog="info-tech-canon")
parser.add_argument(
"--root",
default="",
help="Infospace root. Defaults to ./infospace from the repository root.",
)
sub = parser.add_subparsers(dest="command", required=True)
inspect = sub.add_parser("inspect", help="Inspect the canon infospace")
inspect.set_defaults(handler=_inspect)
artifacts = sub.add_parser("artifacts", help="List canon artifacts")
artifacts.add_argument("--kind", default="")
artifacts.set_defaults(handler=_artifacts)
models = sub.add_parser("models", help="List canon model artifacts")
models.set_defaults(handler=_models)
standards = sub.add_parser("standards", help="List canon standard artifacts")
standards.set_defaults(handler=_standards)
review_kit_cmd = sub.add_parser(
"review-kit",
help="Read the consumer repository alignment review kit",
)
review_kit_cmd.set_defaults(handler=_review_kit)
alignment_template_cmd = sub.add_parser(
"alignment-template",
help="Read the consumer alignment workplan template",
)
alignment_template_cmd.set_defaults(handler=_alignment_template)
validate = sub.add_parser("validate", help="Validate the canon infospace")
validate.add_argument(
"--write",
default="",
help="Write the JSON validation payload to this path.",
)
validate.set_defaults(handler=_validate)
index = sub.add_parser("index", help="Refresh generated indexes and views")
index.set_defaults(handler=_index)
tree = sub.add_parser("tree", help="Refresh the generated infospace tree")
tree.set_defaults(handler=_tree)
agent_briefs = sub.add_parser("agent-briefs", help="Refresh generated agent briefs")
agent_briefs.set_defaults(handler=_agent_briefs)
views = sub.add_parser("views", help="List or read generated views")
views.add_argument("name", nargs="?", default="")
views.set_defaults(handler=_views)
graph = sub.add_parser("graph", help="Export the canon artifact graph")
graph.add_argument("--format", choices=["json", "mermaid"], default="json")
graph.set_defaults(handler=_graph)
profile = sub.add_parser("profile", help="Inspect canon profiles")
profile_sub = profile.add_subparsers(dest="profile_command", required=True)
profile_inspect_cmd = profile_sub.add_parser("inspect", help="Inspect a profile")
profile_inspect_cmd.add_argument("profile")
profile_inspect_cmd.set_defaults(handler=_profile_inspect)
profile_validate_cmd = profile_sub.add_parser("validate", help="Validate a profile")
profile_validate_cmd.add_argument("profile")
profile_validate_cmd.set_defaults(handler=_profile_validate)
profile_graph_cmd = profile_sub.add_parser("graph", help="Export a profile graph")
profile_graph_cmd.add_argument("profile")
profile_graph_cmd.add_argument("--format", choices=["json", "mermaid"], default="json")
profile_graph_cmd.set_defaults(handler=_profile_graph)
capability_review = sub.add_parser(
"capability-review",
help="Review a consumer capability record against the live catalog",
)
capability_review.add_argument("record")
capability_review.set_defaults(handler=_capability_review)
emission = sub.add_parser("emission-review", help="Validate a source-owned YAML/JSON cadence declaration")
emission.add_argument("record")
emission.set_defaults(handler=_emission_review)
coverage = sub.add_parser("validation-coverage", help="List implemented checks and explicit limits")
coverage.set_defaults(handler=_coverage)
freshness = sub.add_parser("check-generated", help="Check projections without changing source files")
freshness.set_defaults(handler=_check_generated)
inventory = sub.add_parser("scope-inventory", help="Derive scope counts from the artifact registry")
inventory.set_defaults(handler=_scope_inventory)
concepts = sub.add_parser(
"concept-coverage",
help="Measure declared concepts against candidates defined in artifact prose")
concepts.add_argument("--artifact", help="Limit the report to one artifact id")
concepts.set_defaults(handler=_concept_coverage)
imports_cmd = sub.add_parser(
"import-review",
help="Resolve a partner import manifest: pinned hashes and concept names")
imports_cmd.add_argument("manifest")
imports_cmd.set_defaults(handler=_import_review)
bundle = sub.add_parser("export-emission-contract", help="Export a content-addressed contract tar")
bundle.add_argument("destination")
bundle.set_defaults(handler=_export_emission)
benchmark = sub.add_parser("benchmark-reads", help="Measure ten uncached inspect calls")
benchmark.set_defaults(handler=_benchmark_reads)
api = sub.add_parser("api", help="Run the read-only local API")
api.add_argument("--host", default="127.0.0.1")
api.add_argument("--port", type=int, default=8765)
api.set_defaults(handler=_api)
return parser
def main(argv: list[str] | None = None) -> int:
parser = build_parser()
args = parser.parse_args(argv)
handler: Command = args.handler
try:
result = handler(args)
except CanonServiceError as exc:
_print_json(exc.to_dict())
return 2
except Exception as exc:
_print_json(
{
"ok": False,
"error": {
"code": "unhandled_error",
"message": str(exc),
"details": {},
},
}
)
return 1
if result:
_print_json(result)
return 0 if result.get("ok", False) else 1
def _root(args: argparse.Namespace) -> Path | None:
return Path(args.root) if args.root else None
def _inspect(args: argparse.Namespace) -> dict[str, Any]:
return inspect_canon(_root(args))
def _artifacts(args: argparse.Namespace) -> dict[str, Any]:
return list_artifacts(_root(args), kind=args.kind or None)
def _models(args: argparse.Namespace) -> dict[str, Any]:
return list_models(_root(args))
def _standards(args: argparse.Namespace) -> dict[str, Any]:
return list_standards(_root(args))
def _review_kit(args: argparse.Namespace) -> dict[str, Any]:
return review_kit(_root(args))
def _alignment_template(args: argparse.Namespace) -> dict[str, Any]:
return alignment_template(_root(args))
def _validate(args: argparse.Namespace) -> dict[str, Any]:
if args.write:
return write_validation_report(args.write, _root(args))
return validate_canon(_root(args))
def _index(args: argparse.Namespace) -> dict[str, Any]:
return generate_indexes(_root(args))
def _tree(args: argparse.Namespace) -> dict[str, Any]:
return generate_tree(_root(args))
def _agent_briefs(args: argparse.Namespace) -> dict[str, Any]:
return generate_agent_briefs(_root(args))
def _views(args: argparse.Namespace) -> dict[str, Any]:
if args.name:
return read_view(args.name, _root(args))
return list_views(_root(args))
def _graph(args: argparse.Namespace) -> dict[str, Any]:
return artifact_graph(_root(args), output_format=args.format)
def _capability_review(args: argparse.Namespace) -> dict[str, Any]:
return review_capability_record(args.record, _root(args))
def _emission_review(args):
from .contracts import review_emission
return review_emission(args.record, _root(args))
def _coverage(args):
from .contracts import coverage
from .service import load_context
return coverage(load_context(_root(args)))
def _check_generated(args):
from .maintenance import check_generated
from .service import load_context
return check_generated(load_context(_root(args)))
def _scope_inventory(args):
from .maintenance import scope_inventory
from .service import load_context
return dict(scope_inventory(load_context(_root(args))), ok=True)
def _concept_coverage(args):
from .maintenance import concept_candidates
from .service import load_context
report = concept_candidates(load_context(_root(args)))
if getattr(args, "artifact", None):
report = dict(report, artifacts=[item for item in report["artifacts"]
if item["artifact"] == args.artifact])
return dict(report, ok=True)
def _import_review(args):
from .federation import import_manifest_review
from .service import load_context
return import_manifest_review(load_context(_root(args)), args.manifest)
def _export_emission(args):
from .maintenance import export_emission_bundle
from .paths import infospace_root
return export_emission_bundle(infospace_root(_root(args)), Path(args.destination))
def _benchmark_reads(args):
from .maintenance import benchmark_reads
from .paths import infospace_root
return benchmark_reads(infospace_root(_root(args)))
def _profile_inspect(args: argparse.Namespace) -> dict[str, Any]:
return profile_inspect(args.profile, _root(args))
def _profile_validate(args: argparse.Namespace) -> dict[str, Any]:
return profile_validate(args.profile, _root(args))
def _profile_graph(args: argparse.Namespace) -> dict[str, Any]:
return profile_graph(args.profile, _root(args), output_format=args.format)
def _api(args: argparse.Namespace) -> dict[str, Any]:
serve(host=args.host, port=args.port, root=_root(args))
return {}
def _print_json(data: dict[str, Any]) -> None:
json.dump(data, sys.stdout, indent=2, sort_keys=True)
sys.stdout.write("\n")