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