2026-05-23 03:12:02 +02:00
|
|
|
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,
|
2026-05-23 03:12:02 +02:00
|
|
|
artifact_graph,
|
2026-05-23 03:32:16 +02:00
|
|
|
generate_agent_briefs,
|
|
|
|
|
generate_indexes,
|
|
|
|
|
generate_tree,
|
2026-05-23 03:12:02 +02:00
|
|
|
inspect_canon,
|
|
|
|
|
list_artifacts,
|
|
|
|
|
list_models,
|
|
|
|
|
list_standards,
|
2026-05-23 03:32:16 +02:00
|
|
|
list_views,
|
2026-05-23 04:26:28 +02:00
|
|
|
profile_graph,
|
2026-05-23 03:12:02 +02:00
|
|
|
profile_inspect,
|
2026-05-23 04:26:28 +02:00
|
|
|
profile_validate,
|
2026-05-23 03:32:16 +02:00
|
|
|
read_view,
|
2026-08-15 19:42:30 +02:00
|
|
|
review_capability_record,
|
2026-05-23 07:23:48 +02:00
|
|
|
review_kit,
|
2026-05-23 03:12:02 +02:00
|
|
|
validate_canon,
|
2026-05-23 03:32:16 +02:00
|
|
|
write_validation_report,
|
2026-05-23 03:12:02 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
2026-05-23 03:12:02 +02:00
|
|
|
validate = sub.add_parser("validate", help="Validate the canon infospace")
|
2026-05-23 03:32:16 +02:00
|
|
|
validate.add_argument(
|
|
|
|
|
"--write",
|
|
|
|
|
default="",
|
|
|
|
|
help="Write the JSON validation payload to this path.",
|
|
|
|
|
)
|
2026-05-23 03:12:02 +02:00
|
|
|
validate.set_defaults(handler=_validate)
|
|
|
|
|
|
2026-05-23 03:32:16 +02:00
|
|
|
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)
|
|
|
|
|
|
2026-05-23 03:12:02 +02:00
|
|
|
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)
|
2026-05-23 03:12:02 +02:00
|
|
|
|
2026-08-15 19:42:30 +02:00
|
|
|
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)
|
|
|
|
|
|
2026-09-05 00:50:09 +02:00
|
|
|
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)
|
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
|
|
|
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)
|
2026-09-05 00:50:09 +02:00
|
|
|
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)
|
|
|
|
|
|
2026-05-23 03:12:02 +02:00
|
|
|
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))
|
|
|
|
|
|
|
|
|
|
|
2026-05-23 03:12:02 +02:00
|
|
|
def _validate(args: argparse.Namespace) -> dict[str, Any]:
|
2026-05-23 03:32:16 +02:00
|
|
|
if args.write:
|
|
|
|
|
return write_validation_report(args.write, _root(args))
|
2026-05-23 03:12:02 +02:00
|
|
|
return validate_canon(_root(args))
|
|
|
|
|
|
|
|
|
|
|
2026-05-23 03:32:16 +02:00
|
|
|
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))
|
|
|
|
|
|
|
|
|
|
|
2026-05-23 03:12:02 +02:00
|
|
|
def _graph(args: argparse.Namespace) -> dict[str, Any]:
|
|
|
|
|
return artifact_graph(_root(args), output_format=args.format)
|
|
|
|
|
|
|
|
|
|
|
2026-08-15 19:42:30 +02:00
|
|
|
def _capability_review(args: argparse.Namespace) -> dict[str, Any]:
|
|
|
|
|
return review_capability_record(args.record, _root(args))
|
|
|
|
|
|
|
|
|
|
|
2026-09-05 00:50:09 +02:00
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
|
2026-09-05 00:50:09 +02:00
|
|
|
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)))
|
|
|
|
|
|
|
|
|
|
|
2026-05-23 03:12:02 +02:00
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
|
2026-05-23 03:12:02 +02:00
|
|
|
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")
|