info-tech-canon/src/info_tech_canon/cli.py

296 lines
9.7 KiB
Python
Raw Normal View History

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,
2026-05-23 07:23:48 +02:00
alignment_template,
artifact_graph,
generate_agent_briefs,
generate_indexes,
generate_tree,
inspect_canon,
list_artifacts,
list_models,
list_standards,
list_views,
2026-05-23 04:26:28 +02:00
profile_graph,
profile_inspect,
2026-05-23 04:26:28 +02:00
profile_validate,
read_view,
review_capability_record,
2026-05-23 07:23:48 +02:00
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)
2026-05-23 07:23:48 +02:00
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)
2026-05-23 04:26:28 +02:00
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)
Measure the concept-declaration gap (INFO-WP-0027-T01) Adds maintenance.concept_candidates() and the concept-coverage CLI command, which measure concepts an artifact defines in prose against the concepts it declares. Extraction covers the bold form, the numbered-heading form that hid itc-org:Authority, and the concept-table form the kernel map uses; preserved source under assimilation, seeds and incoming is excluded. Candidates are review input, never ownership. Baseline over 31 live artifacts: 113 concepts declared against 690 defined, leaving 637 defined but undeclared, about 16 percent coverage. The workplan's 519 counted the bold form alone. Two corrections to the workplan's framing, applied there. Thirteen artifacts declare nothing rather than twelve: kernel/itc-core defines 57 concepts across two forms and declares none, and it is the artifact every other artifact imports from, so it goes first in T02. The gap also reaches further than obscure terms — Actor is undeclared in the organization model although SecurityCanon imports it from there by name against a pinned hash. Three tests cover the extractor, one asserting that Authority appears in the organization model's undeclared list, so the blind spot that produced finding F-1 now has a regression test. make check passes with 49 tests. 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:06:09 +02:00
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)
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))
2026-05-23 07:23:48 +02:00
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)
Measure the concept-declaration gap (INFO-WP-0027-T01) Adds maintenance.concept_candidates() and the concept-coverage CLI command, which measure concepts an artifact defines in prose against the concepts it declares. Extraction covers the bold form, the numbered-heading form that hid itc-org:Authority, and the concept-table form the kernel map uses; preserved source under assimilation, seeds and incoming is excluded. Candidates are review input, never ownership. Baseline over 31 live artifacts: 113 concepts declared against 690 defined, leaving 637 defined but undeclared, about 16 percent coverage. The workplan's 519 counted the bold form alone. Two corrections to the workplan's framing, applied there. Thirteen artifacts declare nothing rather than twelve: kernel/itc-core defines 57 concepts across two forms and declares none, and it is the artifact every other artifact imports from, so it goes first in T02. The gap also reaches further than obscure terms — Actor is undeclared in the organization model although SecurityCanon imports it from there by name against a pinned hash. Three tests cover the extractor, one asserting that Authority appears in the organization model's undeclared list, so the blind spot that produced finding F-1 now has a regression test. make check passes with 49 tests. 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:06:09 +02:00
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 _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))
2026-05-23 04:26:28 +02:00
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")