From a28526e540df09f0a4c3d43f6adc4ed51ce2b71a Mon Sep 17 00:00:00 2001 From: tegwick Date: Sun, 20 Sep 2026 23:24:27 +0200 Subject: [PATCH] Report and enforce concept-declaration coverage (T03) validation-coverage gains a concept_declaration block: declared, defined in prose, undeclared, ratio, silent artifacts, and the extraction limit stated in words. The ratio sits slightly above one because seed-concept lists and YAML payload concepts are declared but not extractable, and saying so in the report is better than a number that looks complete. Two checks carry different weights. concept_declaration_missing is an error: an artifact that defines concepts and declares none, with the kernel map exempt by name because it assigns concepts rather than defining them. Zero today, so a new artifact added without declarations fails. concept_defined_without_owner is a warning over concepts no artifact declares; a name another artifact owns is an import rather than a gap, which keeps the warning from firing 57 times and training reviewers to ignore it. Three warnings today and each is real: the Organization Model defines eleven concepts nobody owns, CARING four including Effective Access and Declared Access, and the Capability Model two. They are carried as T07 rather than declared in passing, because declaring without a boundary review is the mistake this workplan exists to fix. make check passes with 53 tests, clean validation and three 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 --- src/info_tech_canon/contracts.py | 19 ++++++- src/info_tech_canon/service.py | 33 ++++++++++++ tests/test_maintenance.py | 38 ++++++++++++- ...FO-WP-0027-concept-declaration-coverage.md | 53 ++++++++++++++++++- 4 files changed, 140 insertions(+), 3 deletions(-) diff --git a/src/info_tech_canon/contracts.py b/src/info_tech_canon/contracts.py index 96e08ec..526873a 100644 --- a/src/info_tech_canon/contracts.py +++ b/src/info_tech_canon/contracts.py @@ -57,10 +57,27 @@ def coverage(context: Any) -> dict: schema = bindings.get(artifact.kind) rows.append({"id": artifact.id, "kind": artifact.kind, "schema": schema, "checks": "schema-and-references" if schema else "structural-or-specialized"}) + from .maintenance import concept_candidates + + concepts = concept_candidates(context) + declared = concepts["declared_total"] + candidates = concepts["candidate_total"] return {"ok": True, "artifacts": rows, "specialized": ["capability catalog/record", "practice pattern", "emission cadence example", "consumer packs"], - "not_proven": ["ownership of concepts only in prose", "all Markdown links", + "concept_declaration": { + "declared": declared, + "defined_in_prose": candidates, + "undeclared": concepts["undeclared_total"], + "ratio": round(declared / candidates, 3) if candidates else None, + "silent_artifacts": concepts["silent_artifacts"], + "measured_by": "maintenance.concept_candidates", + "limit": "Extraction reads bold, numbered-heading and concept-table forms. " + "Concepts listed in seed-concept blocks or defined in YAML payloads " + "are declared but not extracted, so the ratio may exceed one.", + }, + "not_proven": ["ownership of concepts defined outside the extracted forms", + "all Markdown links", "consumer adoption", "cross-version interoperability", "CARING effective access", "mapping rationale completeness"]} diff --git a/src/info_tech_canon/service.py b/src/info_tech_canon/service.py index e9d78c2..8cd0284 100644 --- a/src/info_tech_canon/service.py +++ b/src/info_tech_canon/service.py @@ -230,6 +230,9 @@ def validate_canon(root: Path | str | None = None) -> dict[str, Any]: ownership = generation.concept_ownership(context) errors.extend(dict(item, code="concept_ownership_conflict") for item in ownership["ownership_conflicts"]) + declaration = concept_declaration_checks(context, ownership) + errors.extend(declaration["errors"]) + warnings.extend(declaration["warnings"]) return { "ok": not errors, @@ -241,6 +244,36 @@ def validate_canon(root: Path | str | None = None) -> dict[str, Any]: } +#: The kernel map assigns concepts to owners rather than defining them, so it is +#: the one artifact allowed to define concept names and declare none. +DECLARATION_EXEMPT = {"kernel/itc-kernel-map"} + + +def concept_declaration_checks(context, ownership: dict) -> dict: + """Undeclared prose is a warning; defining concepts and declaring none is an error. + + A defined name another artifact owns is an import, not a gap, so only names + no artifact declares are reported. + """ + from .maintenance import concept_candidates + + owned = {generation._normalize_concept(item["concept"]) for item in ownership["concepts"]} + errors, warnings = [], [] + for item in concept_candidates(context)["artifacts"]: + unowned = [name for name in item["undeclared"] + if generation._normalize_concept(name) not in owned] + if not item["declares_frontmatter"] and item["candidate_count"] \ + and item["artifact"] not in DECLARATION_EXEMPT: + errors.append({"code": "concept_declaration_missing", + "artifact_id": item["artifact"], "path": item["path"], + "defined": item["candidate_count"]}) + if unowned: + warnings.append({"code": "concept_defined_without_owner", + "artifact_id": item["artifact"], "path": item["path"], + "concepts": unowned}) + return {"errors": errors, "warnings": warnings} + + def write_validation_report( destination: Path | str, root: Path | str | None = None, diff --git a/tests/test_maintenance.py b/tests/test_maintenance.py index aaef6ac..6dd2f0e 100644 --- a/tests/test_maintenance.py +++ b/tests/test_maintenance.py @@ -16,7 +16,13 @@ from info_tech_canon.maintenance import ( export_emission_bundle, source_evidence, ) -from info_tech_canon.service import DEFAULT_INFOSPACE_ROOT, load_context, validate_canon +from info_tech_canon.contracts import coverage +from info_tech_canon.service import ( + DEFAULT_INFOSPACE_ROOT, + concept_declaration_checks, + load_context, + validate_canon, +) @pytest.fixture @@ -152,3 +158,33 @@ def test_concept_coverage_cli_reports_one_artifact(capsys): payload = json.loads(capsys.readouterr().out) assert payload["ok"] is True assert [item["artifact"] for item in payload["artifacts"]] == ["model/identity"] + + +def test_declaration_checks_warn_on_unowned_and_error_on_silence(tmp_path): + context = load_context() + ownership = concept_ownership(context) + result = concept_declaration_checks(context, ownership) + + assert not result["errors"] + warned = {item["artifact_id"] for item in result["warnings"]} + assert "model/organization" in warned + assert all(item["code"] == "concept_defined_without_owner" for item in result["warnings"]) + + +def test_declaration_error_fires_when_an_artifact_declares_nothing(corpus): + target = corpus / "models/security/InfoTechCanonSecurityModel.md" + text = target.read_text(encoding="utf-8") + target.write_text(text.split("---\n", 2)[2], encoding="utf-8") + + context = load_context(corpus) + result = concept_declaration_checks(context, concept_ownership(context)) + + assert [item["artifact_id"] for item in result["errors"]] == ["model/security"] + + +def test_validation_coverage_reports_the_declaration_ratio(): + report = coverage(load_context())["concept_declaration"] + + assert report["silent_artifacts"] == ["kernel/itc-kernel-map"] + assert report["undeclared"] < report["declared"] + assert report["limit"] diff --git a/workplans/INFO-WP-0027-concept-declaration-coverage.md b/workplans/INFO-WP-0027-concept-declaration-coverage.md index ea7e22e..fc9dbf3 100644 --- a/workplans/INFO-WP-0027-concept-declaration-coverage.md +++ b/workplans/INFO-WP-0027-concept-declaration-coverage.md @@ -190,7 +190,7 @@ attribute-based decisions). `Exposure` and `Investigation` go to Security; ```task id: INFO-WP-0027-T03 -status: todo +status: done priority: medium state_hub_task_id: "48dbd089-a0bf-5d02-b574-b1acf554f658" ``` @@ -207,6 +207,57 @@ undeclared prose, and would train reviewers to ignore it. An error is appropriate for one narrower case — an artifact that declares nothing while defining concepts that another artifact references by qualified id. +### Result — 2026-09-20 (T03) + +`validation-coverage` now carries a `concept_declaration` block: declared, +defined in prose, undeclared, the ratio, the silent artifacts, and the +extraction limit in words. It reports 699 declared against 690 extracted, a +ratio slightly above one, because Landscape's seed-concept lists and concepts +defined in YAML payloads are declared but not extractable. The limit is stated +in the report rather than hidden in the number. `not_proven` no longer claims +ownership of prose concepts is unproven wholesale; it names the forms extraction +does not read. + +Enforcement is recorded as two checks with different weights, as recommended: + +- **`concept_declaration_missing` — error.** An artifact that defines concepts + and declares none. `kernel/itc-kernel-map` is exempt by name, with the reason + in the code: it assigns concepts to owners rather than defining them. Zero + today, and a new artifact added without declarations fails. +- **`concept_defined_without_owner` — warning.** A concept defined in prose that + *no* artifact declares. A name another artifact owns is an import, not a gap, + so imports do not warn — which is what keeps the check from firing 57 times + and training reviewers to ignore it. + +Three warnings today, and they are real: `model/organization` defines +`Assignment`, `Availability`, `Capacity`, `CollectiveActor`, `Competence`, +`Group`, `OrganizationEntity`, `OrganizationalCapability`, `Post`, +`ReportingLine` and `Skill` that nobody owns; `standard/caring` defines +`Effective Access`, `Declared Access`, `Derived Capability` and +`Capability Profile`, which are central CARING concepts; `model/capability` +defines `Supply` and `Capacity behaviour`. These are the check's first real +finding and are carried as T07 rather than declared here — declaring them +without a boundary review would repeat the mistake this workplan exists to fix. + +## Declare the concepts nobody owns + +```task +id: INFO-WP-0027-T07 +status: todo +priority: medium +``` + +Seventeen concepts are defined in prose and declared by no artifact, surfaced by +the `concept_defined_without_owner` warning added in T03. Declare them to their +defining artifact where that is right, with a boundary review for each artifact +touched, and resolve the ones that are not — `Capacity` in the Organization +Model against `Capacity behaviour` in the Capability Model is one concept or two, +and the answer decides who declares it. + +CARING is the priority: `Effective Access` and `Declared Access` carry the +standard's central distinction, they are referenced by the Kubernetes RBAC +benchmark, and they are undeclared. + ## Re-verify the federation boundaries against the enlarged index ```task