diff --git a/history/2026-09-20_230456+0200-concept-declaration-baseline.md b/history/2026-09-20_230456+0200-concept-declaration-baseline.md new file mode 100644 index 0000000..e4fe6d2 --- /dev/null +++ b/history/2026-09-20_230456+0200-concept-declaration-baseline.md @@ -0,0 +1,128 @@ +# Concept declaration coverage — baseline measurement + +Date: 2026-09-20 23:04 (Europe/Berlin) +Task: INFO-WP-0027-T01 +Measured at: working tree on `37ad0c9`, before any declaration change +Command: `info_tech_canon concept-coverage` + +## What was measured + +`concept_ownership()` builds the ownership index from two sources only: each +artifact's title, and its `owned_concepts` frontmatter. This report adds a third +view — concepts an artifact *defines in prose* — so the gap between what the +canon defines and what it declares can be counted rather than estimated. + +Extraction covers three forms, over live artifacts only. Preserved source under +`assimilation/`, `seeds/` and `incoming/` is excluded: it records what others +wrote, not what this canon owns. + +- **bold** — `**Name** is|—|defines|denotes|identifies|represents` +- **heading** — a numbered section heading whose title is restated as a bold + definition within five lines. This is the form `itc-org:Authority` (ITC-ORG + 10.17) and `itc-ident:Scope` (ITC-IDENT 2.10) use, and the form that made + finding F-1 invisible. +- **table** — rows of a table whose first column is headed Concept, Term, Name + or Entity. Only the kernel map uses it, appropriately: it assigns concepts to + owners rather than defining them. + +Candidates are review input, never ownership. A concept becomes owned by being +declared, not by being matched here. + +## Result + +| Measure | Count | +| --- | --- | +| Live artifacts measured | 31 | +| Concepts declared by those artifacts | 113 | +| Concepts defined in their prose | 690 | +| Defined but undeclared | **637** | +| Artifacts with no `owned_concepts` frontmatter at all | **13** | + +Declaration coverage is about 16 percent of what the measured artifacts define. +The workplan estimated 519 undeclared terms from the bold form alone; adding the +heading and table forms raises it to 637, and nine artifacts each carry forty or +more undeclared definitions. + +The thirteen silent artifacts: `kernel/itc-core`, `kernel/itc-kernel-map`, +`model/access-control`, `model/data`, `model/devsecops`, `model/governance`, +`model/information-space`, `model/landscape`, `model/network`, +`model/observability`, `model/security`, `model/task`, `standard/tagging`. +This is one more than the twelve the workplan named — `kernel/itc-core` declares +`owned_concepts` nowhere despite defining 36 concepts by heading and 21 by bold +form, and it is the artifact every other artifact imports from. + +## Per-artifact + +| Artifact | Declared | Defined | Undeclared | Declares frontmatter | +| --- | --- | --- | --- | --- | +| `model/network` | 1 | 79 | 78 | no | +| `model/information-space` | 1 | 72 | 71 | no | +| `model/observability` | 1 | 68 | 67 | no | +| `model/devsecops` | 1 | 63 | 62 | no | +| `model/data` | 1 | 53 | 52 | no | +| `model/security` | 1 | 53 | 52 | no | +| `model/task` | 1 | 53 | 52 | no | +| `model/access-control` | 1 | 49 | 48 | no | +| `model/governance` | 1 | 46 | 45 | no | +| `kernel/itc-core` | 1 | 37 | 36 | no | +| `model/organization` | 3 | 28 | 25 | yes | +| `standard/tagging` | 1 | 26 | 25 | no | +| `kernel/itc-kernel-map` | 1 | 11 | 11 | no | +| `model/landscape` | 1 | 7 | 6 | no | +| `standard/caring` | 16 | 5 | 5 | yes | +| `model/capability` | 14 | 3 | 2 | yes | +| `comparison/repo-scoping/consumer-workplan-brief` | 1 | 0 | 0 | no | +| `comparison/repo-scoping/report` | 1 | 0 | 0 | no | +| `concept-area/family` | 2 | 0 | 0 | yes | +| `conformance/railiance-fabric/consumer-workplan-brief` | 1 | 0 | 0 | no | +| `evaluation/user-engine/consumer-workplan-brief` | 1 | 0 | 0 | no | +| `model/evidence` | 5 | 4 | 0 | yes | +| `model/identity` | 24 | 23 | 0 | yes | +| `model/purpose-demand-extension` | 10 | 9 | 0 | yes | +| `pattern/intent-scope-purposes` | 2 | 0 | 0 | yes | +| `practice-pattern/agentic-drives-functional` | 4 | 0 | 0 | yes | +| `practice-pattern/interface-deprecation-strangler` | 1 | 0 | 0 | no | +| `review-kit/alignment/workplan-template` | 1 | 0 | 0 | no | +| `scheme/practice-pattern` | 3 | 0 | 0 | yes | +| `standard/emission-cadence` | 6 | 0 | 0 | yes | +| `standard/repository-layout` | 5 | 1 | 0 | yes | +## Findings + +**F-1 reproduces.** `Authority` appears in `model/organization`'s undeclared +list. The SecurityCanon boundary review compared seven SecurityCanon concepts +against every `owned_concepts` declaration and recorded a clean result; this +measurement shows why that result was structurally unable to see the conflict +candidate that SECURITY-WP-0001-T03 later found by hand. + +**`Actor` is undeclared too.** The Organization Model declares three concepts — +`Community`, `Household` and its title — while defining 28. `Actor` is among the +undeclared, yet SecurityCanon imports `Actor` from this artifact by name, with a +pinned hash, and the identity model lists it as an ITC-ORG concept. The corpus +treats it as owned; the machine-readable declaration does not say so. The same +holds for `Ownership`, `Membership`, `Role` and `Accountability`. + +**The silent artifacts are the exposed ones.** Access control, security and +DevSecOps are precisely the surfaces that federation partners reference. The +artifacts with the best declarations — identity, CARING, the Purpose and Demand +extension — are the ones that have been through a boundary review. Declaration +quality tracks review history, not importance. + +**Extraction quality is good enough to review from, and not good enough to +trust.** Spot-checking `model/organization` returns Accountability, Actor, +Agent, Assignment, Authority, CollectiveActor, Competence, Group, Membership, +Organization, OrganizationEntity, Position — all real concepts. But the +heading form will also catch section titles that merely restate a defined term, +and no extractor can tell a concept this canon *owns* from one it *imports and +re-explains*. That judgment is T02's work, per artifact, by hand. + +## What this does not measure + +Ownership of concepts defined only in tables outside the kernel map, in YAML +payloads, or in prose that defines without the bold form. The 637 is a floor. + +## Next + +T02 declares concepts for the silent artifacts, ordered by federation exposure: +access control, security, DevSecOps, governance. `kernel/itc-core` should be +added to that order at the front — it was missed when the workplan was written, +and it is the most-imported artifact in the corpus. diff --git a/src/info_tech_canon/cli.py b/src/info_tech_canon/cli.py index 8cbc305..6a176b9 100644 --- a/src/info_tech_canon/cli.py +++ b/src/info_tech_canon/cli.py @@ -122,6 +122,11 @@ def build_parser() -> argparse.ArgumentParser: 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) + 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) @@ -245,6 +250,17 @@ def _scope_inventory(args): return dict(scope_inventory(load_context(_root(args))), ok=True) +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 diff --git a/src/info_tech_canon/maintenance.py b/src/info_tech_canon/maintenance.py index e89d12c..ac87f6d 100644 --- a/src/info_tech_canon/maintenance.py +++ b/src/info_tech_canon/maintenance.py @@ -7,6 +7,7 @@ import hashlib import io import json from pathlib import Path +import re import shutil import statistics import subprocess @@ -106,3 +107,96 @@ def benchmark_reads(root: Path, runs: int = 10) -> dict: return {"ok": True, "operation": "inspect", "runs": runs, "median_ms": statistics.median(samples), "max_ms": max(samples), "cache": "none; benchmark does not establish a production latency requirement"} + + +CONCEPT_BOLD = re.compile( + r"^(?:A |An |The )?\*\*(?P[A-Z][A-Za-z0-9 /\-]{2,60}?)\*\*" + r"\s*(?:—|-|is|are|defines|denotes|identifies|represents)\b" +) +CONCEPT_HEADING = re.compile(r"^#{2,4}\s+\d+(?:\.\d+)*\.?\s+(?P[A-Z][A-Za-z0-9 /\-]{2,60})\s*$") +CONCEPT_TABLE_HEADER = re.compile(r"^\|\s*(?:Concept|Term|Name|Entity)\s*\|", re.IGNORECASE) +CONCEPT_TABLE_ROW = re.compile(r"^\|\s*`?(?P[A-Z][A-Za-z0-9 /\-]{2,60}?)`?\s*\|") +SOURCE_ONLY_PREFIXES = ("assimilation/", "seeds/", "incoming/") + + +def _heading_is_defined(lines: list[str], start: int, name: str) -> bool: + """A numbered heading names a concept only if a definition follows it.""" + pattern = re.compile(r"^(?:A |An |The )?\*\*" + re.escape(name) + r"\*\*") + return any(pattern.match(lines[offset].strip()) for offset in range(start + 1, min(start + 6, len(lines)))) + + +def _extract_concepts(text: str) -> dict[str, list[str]]: + lines = text.splitlines() + found: dict[str, set[str]] = {"bold": set(), "heading": set(), "table": set()} + in_concept_table = False + for number, raw in enumerate(lines): + line = raw.strip() + if CONCEPT_TABLE_HEADER.match(line): + in_concept_table = True + continue + if in_concept_table: + if not line.startswith("|"): + in_concept_table = False + elif not set(line) <= set("|- :"): + match = CONCEPT_TABLE_ROW.match(line) + if match: + found["table"].add(match.group("name").strip()) + continue + match = CONCEPT_BOLD.match(line) + if match: + found["bold"].add(match.group("name").strip()) + continue + match = CONCEPT_HEADING.match(line) + if match and _heading_is_defined(lines, number, match.group("name").strip()): + found["heading"].add(match.group("name").strip()) + return {form: sorted(names) for form, names in found.items()} + + +def concept_candidates(context) -> dict: + """Measure declared concepts against candidates defined in artifact prose. + + Candidates are review input, never ownership. A concept becomes owned by + being declared, not by being matched here. + """ + ownership = generation.concept_ownership(context) + declared_by_owner: dict[str, set[str]] = {} + for item in ownership["concepts"]: + declared_by_owner.setdefault(item["owner"], set()).add( + generation._normalize_concept(item["concept"]) + ) + + artifacts = [] + for artifact in sorted(context.infospace.artifacts, key=lambda item: item.id): + if artifact.path.startswith(SOURCE_ONLY_PREFIXES) or not artifact.path.endswith(".md"): + continue + path = context.infospace_root / artifact.path + if not path.exists(): + continue + forms = _extract_concepts(path.read_text(encoding="utf-8")) + declared = declared_by_owner.get(artifact.id, set()) + candidates = sorted({name for names in forms.values() for name in names}) + undeclared = [name for name in candidates + if generation._normalize_concept(name) not in declared] + artifacts.append({ + "artifact": artifact.id, + "path": artifact.path, + "declared_count": len(declared), + "declares_frontmatter": bool( + generation._frontmatter(path).get("owned_concepts") + ), + "candidate_count": len(candidates), + "undeclared_count": len(undeclared), + "by_form": {form: len(names) for form, names in forms.items()}, + "undeclared": undeclared, + }) + + silent = [item["artifact"] for item in artifacts + if not item["declares_frontmatter"] and item["candidate_count"]] + return { + "artifact_count": len(artifacts), + "declared_total": sum(item["declared_count"] for item in artifacts), + "candidate_total": sum(item["candidate_count"] for item in artifacts), + "undeclared_total": sum(item["undeclared_count"] for item in artifacts), + "silent_artifacts": sorted(silent), + "artifacts": artifacts, + } diff --git a/tests/test_maintenance.py b/tests/test_maintenance.py index 3247abe..aacbfab 100644 --- a/tests/test_maintenance.py +++ b/tests/test_maintenance.py @@ -9,7 +9,12 @@ import pytest import yaml from info_tech_canon.cli import main -from info_tech_canon.maintenance import check_generated, export_emission_bundle, source_evidence +from info_tech_canon.maintenance import ( + check_generated, + concept_candidates, + export_emission_bundle, + source_evidence, +) from info_tech_canon.service import DEFAULT_INFOSPACE_ROOT, load_context, validate_canon @@ -114,3 +119,29 @@ def test_missing_mapping_schema_is_a_validation_finding(corpus): payload = validate_canon(corpus) assert not payload["ok"] assert any(e["code"] == "mapping_schema_unreadable" for e in payload["errors"]) + + +def test_concept_candidates_find_prose_definitions_the_declarations_miss(): + """The gap that let itc-org:Authority pass the SecurityCanon conflict check.""" + report = concept_candidates(load_context()) + organization = next(item for item in report["artifacts"] + if item["artifact"] == "model/organization") + + assert "Authority" in organization["undeclared"] + assert organization["undeclared_count"] > organization["declared_count"] + assert report["undeclared_total"] > 0 + assert "model/security" in report["silent_artifacts"] + + +def test_concept_candidates_ignore_preserved_source(): + report = concept_candidates(load_context()) + paths = [item["path"] for item in report["artifacts"]] + assert paths + assert not [path for path in paths if path.startswith(("assimilation/", "seeds/"))] + + +def test_concept_coverage_cli_reports_one_artifact(capsys): + assert main(["concept-coverage", "--artifact", "model/identity"]) == 0 + payload = json.loads(capsys.readouterr().out) + assert payload["ok"] is True + assert [item["artifact"] for item in payload["artifacts"]] == ["model/identity"] diff --git a/workplans/INFO-WP-0027-concept-declaration-coverage.md b/workplans/INFO-WP-0027-concept-declaration-coverage.md index a79693e..7b1c958 100644 --- a/workplans/INFO-WP-0027-concept-declaration-coverage.md +++ b/workplans/INFO-WP-0027-concept-declaration-coverage.md @@ -4,7 +4,7 @@ type: workplan title: "Close the concept-declaration gap so ownership conflicts are detectable" domain: infotech repo: info-tech-canon -status: proposed +status: active owner: claude topic_slug: canon-federation created: "2026-09-20" @@ -29,9 +29,10 @@ Measured on the working tree at `209bb5a`: - the ownership index holds **164 entries** — 81 from artifact titles, 83 from `owned_concepts` frontmatter; -- **12 live models and standards declare nothing at all**: access-control, data, - devsecops, governance, information-space, landscape, network, observability, - security, task, tagging, and the kernel map; +- **13 live artifacts declare nothing at all** (T01 measured one more than this + estimate): itc-core, the kernel map, access-control, data, devsecops, + governance, information-space, landscape, network, observability, security, + task, and tagging; - a single bold-definition pattern (`**Name** is|—`) finds **519 defined terms across 15 live artifacts** that appear in no declaration. That pattern is a lower bound, not a census: it misses numbered-section definitions and @@ -62,7 +63,7 @@ it. No concept is renamed, moved or removed by this workplan. ```task id: INFO-WP-0027-T01 -status: todo +status: done priority: high state_hub_task_id: "1e139d9a-8883-59af-a163-1c6da903dad6" ``` @@ -77,7 +78,31 @@ concepts, extracted candidates, and the difference. No corpus change and no frontmatter edit in this task. The report is the input to T02 and the baseline the coverage check in T03 measures against. -## Declare concepts for the twelve silent artifacts +### Result — 2026-09-20 (T01) + +Extractor added as `maintenance.concept_candidates()` with the CLI command +`concept-coverage`, covering the bold, numbered-heading and concept-table forms +over live artifacts, with `assimilation/`, `seeds/` and `incoming/` excluded as +preserved source. Baseline report: +`history/2026-09-20_230456+0200-concept-declaration-baseline.md`. + +Measured over 31 live artifacts: 113 concepts declared against 690 defined in +prose, leaving **637 defined but undeclared** — about 16 percent declaration +coverage. The workplan's 519 was the bold form alone; the heading form, which is +what hid `itc-org:Authority`, raises it. + +Two corrections to the workplan's own framing. The silent artifacts number +**thirteen, not twelve**: `kernel/itc-core` declares nothing while defining 57 +concepts across two forms, and it is the artifact every other artifact imports +from, so T02 should take it first. And the gap is wider than ownership of +obscure terms — `Actor` itself is undeclared in `model/organization`, though +SecurityCanon imports it from there by name with a pinned hash. + +Three tests cover the extractor, including one that asserts `Authority` appears +in the organization model's undeclared list, so the F-1 blind spot now has a +regression test. `make check` passes with 49 tests. + +## Declare concepts for the thirteen silent artifacts ```task id: INFO-WP-0027-T02 @@ -86,13 +111,14 @@ priority: high state_hub_task_id: "b086abf1-3b36-5b05-a8a8-c8b12c9ab280" ``` -Add reviewed `owned_concepts` frontmatter to the twelve live artifacts that +Add reviewed `owned_concepts` frontmatter to the thirteen live artifacts that declare none, working from the T01 candidates rather than from the regex output directly. Each declared concept must be one the artifact genuinely defines, not one it merely mentions or imports. -Order by federation exposure, not by file size: access-control, security, -devsecops and governance first, since those are the surfaces SecurityCanon, +Order by federation exposure, not by file size: `kernel/itc-core` first, since +every other artifact imports from it, then access-control, security, devsecops +and governance, since those are the surfaces SecurityCanon, InterfaceCanon and the small-saas profile reference. The kernel map is a different case and may end up declaring nothing — it assigns concepts to owners rather than defining them, and that judgment should be recorded either way.