From e1a631413134c2325aceeb57c741070ce19ffccc Mon Sep 17 00:00:00 2001 From: tegwick Date: Sun, 20 Sep 2026 23:43:34 +0200 Subject: [PATCH] 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 Assistant: claude-code Assistant-Model: opus Assistant-Process: 3588@bnt-lap001 Assistant-Session: 24b80f66-e5a7-4e61-99fe-2d422e6d17da --- infospace/agent/review-kit/review-kit.yaml | 2 + .../extension-boundary-review.template.md | 61 +++++++++ infospace/indexes/artifact-tree.yaml | 14 +- .../interfaces/interface-canon-boundary.md | 13 ++ .../interfaces/manifests/interface-canon.json | 92 +++++++++++++ infospace/interfaces/manifests/manifests.yaml | 22 +++ .../interfaces/manifests/security-canon.json | 69 ++++++++++ .../interfaces/security-canon-boundary.md | 13 ++ infospace/views/repository-tree.md | 6 +- src/info_tech_canon/cli.py | 12 ++ src/info_tech_canon/federation.py | 126 ++++++++++++++++++ src/info_tech_canon/service.py | 2 + tests/test_maintenance.py | 39 ++++++ ...INFO-WP-0028-import-manifest-resolution.md | 49 ++++++- 14 files changed, 513 insertions(+), 7 deletions(-) create mode 100644 infospace/agent/templates/extension-boundary-review.template.md create mode 100644 infospace/interfaces/manifests/interface-canon.json create mode 100644 infospace/interfaces/manifests/manifests.yaml create mode 100644 infospace/interfaces/manifests/security-canon.json create mode 100644 src/info_tech_canon/federation.py diff --git a/infospace/agent/review-kit/review-kit.yaml b/infospace/agent/review-kit/review-kit.yaml index a8167da..dcc374c 100644 --- a/infospace/agent/review-kit/review-kit.yaml +++ b/infospace/agent/review-kit/review-kit.yaml @@ -20,6 +20,7 @@ components: model_selection_guide: review-kit/alignment/model-selection-guide schema: review-kit/alignment/schema consumer_workplan_template: review-kit/alignment/workplan-template + extension_boundary_template: agent/templates/extension-boundary-review.template.md required_outputs: - repository_context - selected_canon_surfaces @@ -33,3 +34,4 @@ operating_rules: - Evidence quality and confidence must be recorded separately from reviewer judgment. - INTENT, SCOPE, and PURPOSES should be checked before proposing alignment work. - Namespace, role, scope, tenant, evidence, and purpose terms must not be conflated across native systems and canon concepts. + - An import manifest must be checked twice, by pinned blob hash and by name resolution against the ownership index. A hash proves the reviewed file is the pinned file; only resolution proves the concept named in the manifest exists in it. Record both counts, never one as evidence of the other. diff --git a/infospace/agent/templates/extension-boundary-review.template.md b/infospace/agent/templates/extension-boundary-review.template.md new file mode 100644 index 0000000..d06a0fa --- /dev/null +++ b/infospace/agent/templates/extension-boundary-review.template.md @@ -0,0 +1,61 @@ + + +# extension boundary + +Status: for the semantic +boundary documented below, reviewed by in the InfoTechCanon +repository on . State whether this is an agent review or human sign-off. + +Review target: at ``. +InfoTechCanon review base and import source: ``. + +## Accepted dispositions + + + +## Evidence + +**Hash verification.** pinned SHA-256 values match Git blobs at the +declared InfoTechCanon source revision, and those files are unchanged in the +review checkout. + +**Name resolution.** declared import names resolve in the InfoTechCanon +ownership index to the artifact the manifest pins. List every one that does not, +with what it resolves to instead and whether it is a citation error, a name +InfoTechCanon never used, or a contested concept. Only the third is a boundary +question; the first two are manifest corrections. + +**Ownership conflict.** of the partner's owned concepts collide with an +InfoTechCanon declaration. Name any that do. + +## Limits + +- Scope: which partner version, and that no stable promotion is asserted. +- What the acceptance does not record: consumer adoption, runtime claims, + conformance statements. +- Any proposal the partner references that this review does **not** accept. +- Resolution proves a name exists and names one owner. Whether the partner uses + the concept as its owner defines it is a semantic question this review must + answer in the dispositions above, not something the check can settle. + +## Re-verification + + diff --git a/infospace/indexes/artifact-tree.yaml b/infospace/indexes/artifact-tree.yaml index 82689c0..611f193 100644 --- a/infospace/indexes/artifact-tree.yaml +++ b/infospace/indexes/artifact-tree.yaml @@ -1,5 +1,5 @@ root: infospace -file_count: 294 +file_count: 298 files: - path: README.md directory: . @@ -229,6 +229,9 @@ files: - path: agent/templates/consumer-brief.template.md directory: agent/templates name: consumer-brief.template.md +- path: agent/templates/extension-boundary-review.template.md + directory: agent/templates + name: extension-boundary-review.template.md - path: artifacts/index.yaml directory: artifacts name: index.yaml @@ -580,6 +583,15 @@ files: - path: interfaces/interface-canon-boundary.md directory: interfaces name: interface-canon-boundary.md +- path: interfaces/manifests/interface-canon.json + directory: interfaces/manifests + name: interface-canon.json +- path: interfaces/manifests/manifests.yaml + directory: interfaces/manifests + name: manifests.yaml +- path: interfaces/manifests/security-canon.json + directory: interfaces/manifests + name: security-canon.json - path: interfaces/security-canon-boundary.md directory: interfaces name: security-canon-boundary.md diff --git a/infospace/interfaces/interface-canon-boundary.md b/infospace/interfaces/interface-canon-boundary.md index 50d81f5..9847903 100644 --- a/infospace/interfaces/interface-canon-boundary.md +++ b/infospace/interfaces/interface-canon-boundary.md @@ -75,3 +75,16 @@ than InterfaceCanon's: No InfoTechCanon concept is renamed, moved or removed, and the accepted dispositions are unchanged. The `Interface` and `Endpoint` pins are InterfaceCanon's to correct; the two Landscape declarations are made here. + + +## Standing check — 2026-09-20 (INFO-WP-0028) + +This partner's import manifest is registered at +`infospace/interfaces/manifests/interface-canon.json` as a provenance-preserving copy and is +re-resolved on every `make check`. Hash and name are checked together by +`info_tech_canon import-review`; a name that stops resolving, or resolves to a +different owner than the manifest pins, is reported as a +`federation_import_drift` warning naming this partner. It is a warning and not an +error because a stale partner pin is not this repository's file to fix. + +Current result: **23 of 25 declared imports resolve** and all seven pinned hashes match. `Interface` and `Endpoint` remain pinned to ITC-LAND while the Network Model declares them; raised with interface-canon by State Hub message `13d355f2-554b-4a3a-a0ef-3f69eeafeae9` on 2026-09-20 and unanswered as of that date. The two warnings stand until that pin is corrected or qualified in place. diff --git a/infospace/interfaces/manifests/interface-canon.json b/infospace/interfaces/manifests/interface-canon.json new file mode 100644 index 0000000..948d5ec --- /dev/null +++ b/infospace/interfaces/manifests/interface-canon.json @@ -0,0 +1,92 @@ +{ + "schema_version": 1, + "model": "interface-model", + "source_repository": "info-tech-canon", + "source_commit": "8eb1c83f2e119de57e7ad448b901381f8e92fb57", + "imports": [ + { + "model": "itc-land", + "path": "infospace/models/landscape/InfoTechCanonLandscapeModel.md", + "sha256": "4190b044fbd5e3eecf7b28e08608e99c7de3af5dd5f3a7554edee02423967fb1", + "concepts": [ + "Interface", + "Endpoint", + "SoftwareSystem", + "SoftwareComponent" + ] + }, + { + "model": "itc-ident", + "path": "infospace/models/identity/InfoTechCanonIdentityModel.md", + "sha256": "20bc844d9de07041c40009cc5e3d6eeb42aeb7adcbb86b0194d4b2517aa23f32", + "concepts": [ + "Identifier", + "Scoped Identifier", + "Synonymity Assertion" + ] + }, + { + "model": "itc-evid", + "path": "infospace/models/evidence/InfoTechCanonEvidenceModel.md", + "sha256": "a93472818b3ae60c07a4c6226090d63eebf1402dfd9f46dad643dbb5608ed086", + "concepts": [ + "Evidence", + "Evidence Source", + "Evidence strength" + ] + }, + { + "model": "itc-gov", + "path": "infospace/models/governance/InfoTechCanonGovernanceModel.md", + "sha256": "5f3174147ec0d6f5bb6479a7ce4dc130bf61ecc0bab3936895b92c6baf21deb5", + "concepts": [ + "Assertion", + "Policy", + "Decision" + ] + }, + { + "model": "itc-data", + "path": "infospace/models/data/InfoTechCanonDataModel.md", + "sha256": "cffbbde50808719adb73c6d6c642da8d0122bdfb3e9b6aaa6bc852330089afe3", + "concepts": [ + "Dataset", + "DataObject", + "Record", + "Representation", + "DataType", + "Constraint" + ], + "usage": "shared concepts referenced for mapping; no asserted equivalence" + }, + { + "model": "itc-infospace", + "path": "infospace/models/information-space/InfoTechCanonInformationSpaceModel.md", + "sha256": "ab48a679b8588b654b12687c5248a292aab13c11fe2005e060b185c551539e10", + "concepts": [ + "KnowledgeArtifact", + "SourceReference", + "Citation" + ], + "usage": "shared concepts referenced for mapping; no asserted equivalence" + }, + { + "model": "itc-net", + "path": "infospace/models/network/InfoTechCanonNetworkModel.md", + "sha256": "3ee5ed5e0423389320a0ccf86cd6b5dc55e0c1b4562b00e101d158cff6bd43c8", + "concepts": [ + "Interface", + "Port", + "Endpoint" + ], + "usage": "shared concepts referenced for mapping; no asserted equivalence" + } + ], + "review": { + "date": "2026-09-09", + "workplan": "INTERFACE-WP-0003", + "record": "../../mappings/review-2026-09-09.md", + "previous_manifest_commit": "dd6cdd7f83f881d7f94795b58351bbd06a056990", + "result": "existing four imports unchanged; data, information-space, and network boundaries added" + } +} diff --git a/infospace/interfaces/manifests/manifests.yaml b/infospace/interfaces/manifests/manifests.yaml new file mode 100644 index 0000000..849a0a1 --- /dev/null +++ b/infospace/interfaces/manifests/manifests.yaml @@ -0,0 +1,22 @@ +# Registered partner import manifests. +# +# These are provenance-preserving copies, owned by the partner repository and +# retrieved from it. Do not edit a copy to make a check pass: correct it in the +# partner repository and retrieve it again, recording the new revision here. +# The copies exist so that drift is caught when InfoTechCanon changes, not only +# when a partner happens to look. +# +# Decided under INFO-WP-0028-T02. +manifests: + - partner: security-canon + file: security-canon.json + retrieved_from: infospace/vocabulary/mode-of-authority/imports.json + partner_revision: e159fe7a1b2b9172fc979a284c4de88e4d08b933 + retrieved_at: "2026-09-20" + boundary: ../security-canon-boundary.md + - partner: interface-canon + file: interface-canon.json + retrieved_from: infospace/models/interface/imports.json + partner_revision: 37fbe245bfd00fb1271af368eec7f0161247d57f + retrieved_at: "2026-09-20" + boundary: ../interface-canon-boundary.md diff --git a/infospace/interfaces/manifests/security-canon.json b/infospace/interfaces/manifests/security-canon.json new file mode 100644 index 0000000..7c05638 --- /dev/null +++ b/infospace/interfaces/manifests/security-canon.json @@ -0,0 +1,69 @@ +{ + "manifest": "sec-authority:ModeOfAuthority imports", + "artifact": "sec-authority:ModeOfAuthority", + "artifact_version": "0.2.0", + "source_repository": "info-tech-canon", + "source_commit": "e1f98da015f32545254747c8e9f59b780c423d5b", + "pinned_at": "2026-09-20", + "note": "Concepts imported, not redefined. SHA-256 values are of the Git blob at source_commit. Amended 2026-09-20 by SECURITY-WP-0001-T03: Authority and Environment added as declared imports. Environment re-pointed 2026-09-20 from CARING to the Landscape Model, which the kernel map names as its owner. Name-resolution audit 2026-09-20 (InfoTechCanon INFO-WP-0027-T04): five of twelve imports named concepts their pinned artifact does not define.", + "imports": [ + { + "artifact": "InfoTechCanonCore", + "path": "infospace/kernel/InfoTechCanonCore.md", + "sha256": "74661caea603876f71b4258963bd11a338cba63ef57a00fb8c41e2e51d07742c", + "concepts": [ + "CanonArtifact", + "RelationshipDefinition" + ], + "note": "Core owns CanonArtifact, any identifiable unit of canon content, and RelationshipDefinition. Corrected 2026-09-20: the earlier manifest named Artifact and Relationship, which Core does not define." + }, + { + "artifact": "InfoTechCanonAccessControlModel", + "path": "infospace/models/access-control/InfoTechCanonAccessControlModel.md", + "sha256": "456e2534264b806098ac7be85d25e5e1cd1150b1e34934b07f46dabd909343bf", + "concepts": [ + "Subject", + "Principal", + "Operation" + ], + "note": "Operation replaces the earlier AccessOperations, which was a SecurityCanon compaction of the ITC-ACCESS name." + }, + { + "artifact": "InfoTechCanonIdentityModel", + "path": "infospace/models/identity/InfoTechCanonIdentityModel.md", + "sha256": "20bc844d9de07041c40009cc5e3d6eeb42aeb7adcbb86b0194d4b2517aa23f32", + "concepts": [ + "Scope" + ], + "note": "Scope is the general boundary, ITC-IDENT 2.10. PrincipalType was removed 2026-09-20: the name appears nowhere in the identity model, and the dimension is SecurityCanon's own classification of the imported Principal." + }, + { + "artifact": "InfoTechCanonOrganizationModel", + "path": "infospace/models/organization/InfoTechCanonOrganizationModel.md", + "sha256": "bdb1277e78c3a0802f8ea08e058be093452390e61ca0f7e979051e48680fe6c3", + "concepts": [ + "Actor", + "Authority", + "Ownership" + ], + "note": "Ownership moved here 2026-09-20; the kernel map assigns Ownership and Stewardship to Organization, not Core." + }, + { + "artifact": "InfoTechCanonPurposeDemandExtension", + "path": "infospace/models/governance/InfoTechCanonPurposeDemandExtension.md", + "sha256": "74637a7ed997691abf2c1370fffaa173010939a070d69853dc1f0a86cc8343bb", + "concepts": [ + "Purpose" + ] + }, + { + "artifact": "InfoTechCanonLandscapeModel", + "path": "infospace/models/landscape/InfoTechCanonLandscapeModel.md", + "sha256": "4190b044fbd5e3eecf7b28e08608e99c7de3af5dd5f3a7554edee02423967fb1", + "concepts": [ + "Environment" + ], + "note": "Environment is owned by the Landscape Model per the InfoTechCanon kernel map concept-owner table; CARING section 21.5 enumerates values for it. The RECOVERY value is carried as an upstream value proposal." + } + ] +} diff --git a/infospace/interfaces/security-canon-boundary.md b/infospace/interfaces/security-canon-boundary.md index c4f2bc2..6573ab5 100644 --- a/infospace/interfaces/security-canon-boundary.md +++ b/infospace/interfaces/security-canon-boundary.md @@ -106,3 +106,16 @@ the wrong artifact or a name InfoTechCanon never used, not a contested concept. file that was pinned. It says nothing about whether the concept named in the manifest exists in it. Both checks are needed, and only the first was run in the original acceptance. + + +## Standing check — 2026-09-20 (INFO-WP-0028) + +This partner's import manifest is registered at +`infospace/interfaces/manifests/security-canon.json` as a provenance-preserving copy and is +re-resolved on every `make check`. Hash and name are checked together by +`info_tech_canon import-review`; a name that stops resolving, or resolves to a +different owner than the manifest pins, is reported as a +`federation_import_drift` warning naming this partner. It is a warning and not an +error because a stale partner pin is not this repository's file to fix. + +Current result: **11 of 11 declared imports resolve**, all six pinned hashes match, no drift. diff --git a/infospace/views/repository-tree.md b/infospace/views/repository-tree.md index fbc0d94..2dc54a6 100644 --- a/infospace/views/repository-tree.md +++ b/infospace/views/repository-tree.md @@ -2,7 +2,7 @@ # Repository Tree -File count: **294** +File count: **298** - `README.md` - `agent/README.md` @@ -80,6 +80,7 @@ File count: **294** - `agent/templates/canon-interface-card.template.yaml` - `agent/templates/consumer-alignment-workplan.template.md` - `agent/templates/consumer-brief.template.md` +- `agent/templates/extension-boundary-review.template.md` - `artifacts/index.yaml` - `assimilation/README.md` - `assimilation/canon-federation/ASSIMILATION.md` @@ -197,6 +198,9 @@ File count: **294** - `interfaces/README.md` - `interfaces/federation.yaml` - `interfaces/interface-canon-boundary.md` +- `interfaces/manifests/interface-canon.json` +- `interfaces/manifests/manifests.yaml` +- `interfaces/manifests/security-canon.json` - `interfaces/security-canon-boundary.md` - `kernel/InfoTechCanonCore.md` - `kernel/InfoTechCanonKernelMap.md` diff --git a/src/info_tech_canon/cli.py b/src/info_tech_canon/cli.py index 6a176b9..6d15cc5 100644 --- a/src/info_tech_canon/cli.py +++ b/src/info_tech_canon/cli.py @@ -127,6 +127,11 @@ def build_parser() -> argparse.ArgumentParser: 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) @@ -261,6 +266,13 @@ def _concept_coverage(args): 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 diff --git a/src/info_tech_canon/federation.py b/src/info_tech_canon/federation.py new file mode 100644 index 0000000..d289018 --- /dev/null +++ b/src/info_tech_canon/federation.py @@ -0,0 +1,126 @@ +"""Resolve a partner import manifest: pinned blob hashes and concept names. + +A hash proves the reviewed file is the pinned file. It says nothing about +whether the concept named in the manifest exists in it. Both questions are +answered here, in one pass, so neither can be recorded without the other. +""" + +import hashlib +import json +from pathlib import Path +import subprocess + +import yaml + +from . import generation + +RESOLVED = "resolved" +WRONG_ARTIFACT = "wrong_artifact" +UNOWNED = "unowned" + + +def _manifest(path: Path) -> dict: + from .service import CanonServiceError + + try: + text = path.read_text(encoding="utf-8") + except OSError as exc: + raise CanonServiceError("manifest_unreadable", str(exc)) from exc + try: + data = json.loads(text) if path.suffix == ".json" else yaml.safe_load(text) + except (json.JSONDecodeError, yaml.YAMLError) as exc: + raise CanonServiceError("manifest_unparsable", str(exc)) from exc + if not isinstance(data, dict) or not isinstance(data.get("imports"), list): + raise CanonServiceError("manifest_shape", "Expected a mapping with an 'imports' list") + return data + + +def _blob_sha256(repository: Path, commit: str, path: str) -> str | None: + """Hash the file as the pinned revision holds it, not as it stands today.""" + try: + blob = subprocess.run(["git", "show", f"{commit}:{path}"], cwd=repository, + capture_output=True, check=True).stdout + except (OSError, subprocess.CalledProcessError): + return None + return hashlib.sha256(blob).hexdigest() + + +def import_manifest_review(context, manifest_path: str | Path) -> dict: + data = _manifest(Path(manifest_path)) + commit = data.get("source_commit") + repository = context.infospace_root.parent + owners: dict[str, set[str]] = {} + for item in generation.concept_ownership(context)["concepts"]: + owners.setdefault(generation._normalize_concept(item["concept"]), set()).add(item["owner"]) + by_path = {artifact.path: artifact.id for artifact in context.infospace.artifacts} + + entries, findings = [], [] + prefix = context.infospace_root.name + "/" + for entry in data["imports"]: + path = entry.get("path", "") + # Manifests pin repository-relative paths; the registry keys on infospace-relative. + pinned = by_path.get(path) or by_path.get(path[len(prefix):] if path.startswith(prefix) else path) + concepts = [] + for name in entry.get("concepts") or []: + found = sorted(owners.get(generation._normalize_concept(name), set())) + if pinned and found == [pinned]: + status = RESOLVED + elif found: + status = WRONG_ARTIFACT + else: + status = UNOWNED + concepts.append({"concept": name, "status": status, + "owner": found[0] if len(found) == 1 else found or None}) + if status != RESOLVED: + findings.append({"code": f"import_{status}", "concept": name, + "pinned_path": path, "pinned_artifact": pinned, + "resolves_to": found or None}) + expected = _blob_sha256(repository, commit, path) if commit else None + declared = entry.get("sha256") + if expected is None: + hash_status = "unverifiable" + elif declared == expected: + hash_status = "match" + else: + hash_status = "mismatch" + findings.append({"code": "import_hash_mismatch", "pinned_path": path, + "declared": declared, "actual": expected}) + entries.append({"path": path, "artifact": pinned, "hash": hash_status, + "declared_sha256": declared, "concepts": concepts}) + + resolved = sum(1 for item in entries for concept in item["concepts"] + if concept["status"] == RESOLVED) + total = sum(len(item["concepts"]) for item in entries) + return {"ok": not findings, "manifest": str(manifest_path), + "source_commit": commit, "entries": entries, "findings": findings, + "resolved": resolved, "declared": total, + "limit": "Resolution proves the name exists and names one owner. Whether the " + "partner uses the concept as its owner defines it stays with boundary review."} + + +REGISTERED = "interfaces/manifests/manifests.yaml" + + +def registered_manifests(context) -> list[dict]: + path = context.infospace_root / REGISTERED + if not path.exists(): + return [] + data = yaml.safe_load(path.read_text(encoding="utf-8")) or {} + return data.get("manifests") or [] + + +def registered_drift(context) -> list[dict]: + """Stale partner pins are the partner's to fix, so this warns and never errors.""" + warnings = [] + for item in registered_manifests(context): + manifest = context.infospace_root / REGISTERED + review = import_manifest_review(context, manifest.parent / item["file"]) + for finding in review["findings"]: + warnings.append({"code": "federation_import_drift", + "partner": item.get("partner"), + "partner_revision": item.get("partner_revision"), + "finding": finding["code"], + "concept": finding.get("concept"), + "pinned_path": finding.get("pinned_path"), + "resolves_to": finding.get("resolves_to")}) + return warnings diff --git a/src/info_tech_canon/service.py b/src/info_tech_canon/service.py index 8cd0284..64dc53e 100644 --- a/src/info_tech_canon/service.py +++ b/src/info_tech_canon/service.py @@ -233,6 +233,8 @@ def validate_canon(root: Path | str | None = None) -> dict[str, Any]: declaration = concept_declaration_checks(context, ownership) errors.extend(declaration["errors"]) warnings.extend(declaration["warnings"]) + from .federation import registered_drift + warnings.extend(registered_drift(context)) return { "ok": not errors, diff --git a/tests/test_maintenance.py b/tests/test_maintenance.py index dc8a94f..2b81daf 100644 --- a/tests/test_maintenance.py +++ b/tests/test_maintenance.py @@ -17,6 +17,7 @@ from info_tech_canon.maintenance import ( source_evidence, ) from info_tech_canon.contracts import coverage +from info_tech_canon.federation import import_manifest_review, registered_drift from info_tech_canon.service import ( DEFAULT_INFOSPACE_ROOT, concept_declaration_checks, @@ -199,3 +200,41 @@ def test_validation_coverage_reports_the_declaration_ratio(): assert report["silent_artifacts"] == ["kernel/itc-kernel-map"] assert report["undeclared"] < report["declared"] assert report["limit"] + + +def test_import_review_resolves_a_partner_manifest(tmp_path): + manifest = DEFAULT_INFOSPACE_ROOT / "interfaces/manifests/security-canon.json" + review = import_manifest_review(load_context(), manifest) + + assert review["ok"] is True + assert review["resolved"] == review["declared"] + assert {entry["hash"] for entry in review["entries"]} == {"match"} + + +def test_import_review_reports_a_name_its_artifact_does_not_define(tmp_path): + source = json.loads( + (DEFAULT_INFOSPACE_ROOT / "interfaces/manifests/security-canon.json").read_text()) + source["imports"][0]["concepts"] = ["Artifact"] + manifest = tmp_path / "imports.json" + manifest.write_text(json.dumps(source)) + + review = import_manifest_review(load_context(), manifest) + + assert review["ok"] is False + finding = review["findings"][0] + assert finding["concept"] == "Artifact" + assert finding["code"] in {"import_wrong_artifact", "import_unowned"} + + +def test_registered_drift_names_the_partner_not_the_canon(): + warnings = registered_drift(load_context()) + + assert all(item["code"] == "federation_import_drift" for item in warnings) + assert {item["partner"] for item in warnings} <= {"interface-canon", "security-canon"} + + +def test_import_review_cli_exits_non_zero_on_a_finding(capsys): + manifest = DEFAULT_INFOSPACE_ROOT / "interfaces/manifests/interface-canon.json" + assert main(["import-review", str(manifest)]) == 1 + payload = json.loads(capsys.readouterr().out) + assert [item["concept"] for item in payload["findings"]] == ["Interface", "Endpoint"] diff --git a/workplans/INFO-WP-0028-import-manifest-resolution.md b/workplans/INFO-WP-0028-import-manifest-resolution.md index c669059..23b076a 100644 --- a/workplans/INFO-WP-0028-import-manifest-resolution.md +++ b/workplans/INFO-WP-0028-import-manifest-resolution.md @@ -4,7 +4,7 @@ type: workplan title: "Make import-manifest name resolution a standing check" domain: infotech repo: info-tech-canon -status: proposed +status: active owner: claude topic_slug: canon-federation created: "2026-09-20" @@ -58,7 +58,7 @@ partner decides. ```task id: INFO-WP-0028-T01 -status: todo +status: done priority: high state_hub_task_id: "7c4e8a21-51a9-5658-b56f-01204e4cba34" ``` @@ -81,7 +81,7 @@ not require the partner repository to be checked out beside this one. ```task id: INFO-WP-0028-T02 -status: todo +status: done priority: high state_hub_task_id: "7491a8ac-1f96-5f2e-a9eb-f3995c528cae" ``` @@ -105,7 +105,7 @@ answer determines who may edit it. ```task id: INFO-WP-0028-T03 -status: todo +status: done priority: medium state_hub_task_id: "20eab825-8236-583e-b246-cbe86122cc7c" ``` @@ -121,7 +121,7 @@ review starts from a template that asks both questions. ```task id: INFO-WP-0028-T04 -status: todo +status: wait priority: medium state_hub_task_id: "cdf31f69-7a2e-5be0-a988-145dee48c063" ``` @@ -143,3 +143,42 @@ together; accepted manifests are re-resolved on every `make check` with drift reported as a warning that names the partner; the review kit asks for both results; and no open resolution finding remains against either accepted boundary. + +### Result — 2026-09-20 (T01, T02, T03) + +`import-review` takes any manifest path and returns JSON: per concept, whether +the name resolves in the ownership index and to which artifact; per entry, +whether the pinned SHA-256 matches the blob at the declared source commit. Both +checks run in one pass, so neither can be recorded without the other. It exits +non-zero on a finding, reads JSON or YAML, and needs no partner checkout. It +also carries its own limit in the payload: resolution proves a name exists and +names one owner, and nothing more. + +Manifests are registered under `infospace/interfaces/manifests/` with +`manifests.yaml` recording the partner, the source path, the partner revision +and the retrieval date. **They are provenance-preserving copies, owned by the +partner** — the decision T02 asked for. Correcting a copy to make a check pass +is explicitly forbidden in the file itself: fix it in the partner repository and +retrieve it again. `validate` re-resolves them and reports drift as +`federation_import_drift`, a warning naming the partner, never an error. + +Both accepted boundaries carry a standing-check section with the current result. +The review kit gains `extension-boundary-review.template.md`, which requires +hash count, resolution count and conflict count as three separate lines, and an +operating rule stating that one is never evidence of the other. + +Verified against both live manifests: security-canon resolves 11 of 11 with six +hashes matching; interface-canon resolves 23 of 25 with seven hashes matching, +the two exceptions being the known `Interface` and `Endpoint` pins. The check +finds exactly what the T04 audit found by hand, which is the point. + +### T04 — waiting on interface-canon + +The two open pins are InterfaceCanon's to correct; its accepted boundary already +treats Landscape `Interface` and Network attachment-point `Interface` as +contextual mappings rather than exact aliases, so this is a citation fix, not a +semantic dispute. Raised by State Hub message +`13d355f2-554b-4a3a-a0ef-3f69eeafeae9` on 2026-09-20, unread and unanswered at +the time of writing. The task is set to `wait` rather than done: the standing +warning is the follow-up mechanism, and it will clear itself when the pin is +corrected.