Add validation indexes and generated views

This commit is contained in:
2026-05-23 03:32:16 +02:00
parent dc44208c9f
commit c112bf5c74
37 changed files with 2007 additions and 8 deletions

View File

@@ -0,0 +1,427 @@
from __future__ import annotations
from collections import defaultdict
from pathlib import Path
from typing import Any
import yaml
GENERATED_NOTICE = "<!-- GENERATED by info_tech_canon; do not edit by hand. -->"
def generate_indexes(context: Any) -> dict[str, Any]:
assets: list[dict[str, Any]] = []
ownership = concept_ownership(context)
import_matrix = relationship_matrix(context)
assets.append(
_write_yaml(
context.infospace_root / "indexes" / "concept-ownership.yaml",
ownership,
)
)
assets.append(
_write_yaml(
context.infospace_root / "indexes" / "import-matrix.yaml",
import_matrix,
)
)
assets.append(
_write_yaml(
context.infospace_root / "indexes" / "artifact-tree.yaml",
artifact_tree(context),
)
)
assets.extend(generate_views(context, ownership, import_matrix)["files"])
return _result("index", assets)
def generate_views(
context: Any,
ownership: dict[str, Any] | None = None,
import_matrix: dict[str, Any] | None = None,
) -> dict[str, Any]:
ownership = ownership or concept_ownership(context)
import_matrix = import_matrix or relationship_matrix(context)
files = [
_write_text(
context.infospace_root / "views" / "by-standard.md",
_render_by_standard(context),
),
_write_text(
context.infospace_root / "views" / "by-concept.md",
_render_by_concept(ownership),
),
_write_text(
context.infospace_root / "views" / "by-profile.md",
_render_by_profile(context),
),
_write_text(
context.infospace_root / "views" / "by-mapping-target.md",
_render_by_mapping_target(context),
),
_write_text(
context.infospace_root / "views" / "kernel-overview.md",
_render_kernel_overview(context),
),
_write_text(
context.infospace_root / "views" / "import-matrix.md",
_render_import_matrix(import_matrix),
),
]
return _result("views", files)
def generate_tree(context: Any) -> dict[str, Any]:
tree = artifact_tree(context)
files = [
_write_yaml(context.infospace_root / "indexes" / "artifact-tree.yaml", tree),
_write_text(
context.infospace_root / "views" / "repository-tree.md",
_render_repository_tree(tree),
),
]
return _result("tree", files)
def generate_agent_briefs(context: Any) -> dict[str, Any]:
files = [
_write_text(
context.infospace_root / "agent" / "global-agent-brief.md",
_render_global_agent_brief(context),
)
]
return _result("agent-briefs", files)
def list_generated_views(context: Any) -> dict[str, Any]:
views = []
for path in sorted((context.infospace_root / "views").glob("*.md")):
views.append(
{
"name": path.name,
"path": str(path.relative_to(context.infospace_root)),
"generated": _is_generated(path),
}
)
return {"ok": True, "count": len(views), "views": views}
def read_generated_view(context: Any, name: str) -> dict[str, Any]:
if "/" in name or "\\" in name:
raise ValueError("View name must be a single file name.")
path = context.infospace_root / "views" / name
if not path.is_file():
raise FileNotFoundError(name)
return {
"ok": True,
"name": name,
"path": str(path.relative_to(context.infospace_root)),
"generated": _is_generated(path),
"content": path.read_text(encoding="utf-8"),
}
def concept_ownership(context: Any) -> dict[str, Any]:
concepts: list[dict[str, Any]] = []
for artifact in sorted(context.infospace.artifacts, key=lambda item: item.id):
concepts.append(
{
"concept": artifact.title,
"owner": artifact.id,
"path": artifact.path,
"source": "artifact_title",
}
)
frontmatter = _frontmatter(context.infospace_root / artifact.path)
owned = frontmatter.get("owned_concepts") or []
if isinstance(owned, list):
for concept in owned:
concepts.append(
{
"concept": str(concept),
"owner": artifact.id,
"path": artifact.path,
"source": "frontmatter.owned_concepts",
}
)
by_key: dict[str, list[dict[str, Any]]] = defaultdict(list)
for item in concepts:
by_key[_normalize_concept(item["concept"])].append(item)
duplicates = [
{"normalized": key, "candidates": items}
for key, items in sorted(by_key.items())
if len(items) > 1
]
conflicts = [
{
"normalized": item["normalized"],
"owners": sorted({candidate["owner"] for candidate in item["candidates"]}),
"candidates": item["candidates"],
}
for item in duplicates
if len({candidate["owner"] for candidate in item["candidates"]}) > 1
]
return {
"concept_count": len(concepts),
"concepts": concepts,
"duplicate_candidates": duplicates,
"ownership_conflicts": conflicts,
}
def relationship_matrix(context: Any) -> dict[str, Any]:
artifact_ids = sorted(artifact.id for artifact in context.infospace.artifacts)
rows: list[dict[str, Any]] = []
for artifact in sorted(context.infospace.artifacts, key=lambda item: item.id):
targets: dict[str, list[str]] = {target: [] for target in artifact_ids}
for relationship in artifact.relationships:
target = relationship.get("target")
relation_type = str(relationship.get("type") or "related")
if isinstance(target, str) and target in targets:
targets[target].append(relation_type)
rows.append(
{
"artifact": artifact.id,
"targets": {
target: sorted(types)
for target, types in targets.items()
if types
},
}
)
return {"artifacts": artifact_ids, "rows": rows}
def artifact_tree(context: Any) -> dict[str, Any]:
files: list[dict[str, Any]] = []
for path in sorted(context.infospace_root.rglob("*")):
if path.is_file():
relative = path.relative_to(context.infospace_root)
files.append(
{
"path": str(relative),
"directory": str(relative.parent),
"name": path.name,
}
)
return {"root": "infospace", "file_count": len(files), "files": files}
def _render_by_standard(context: Any) -> str:
lines = _heading("By Standard")
standards = [
artifact
for artifact in context.infospace.artifacts
if artifact.kind in {"kernel", "standard"}
]
for artifact in sorted(standards, key=lambda item: item.id):
lines.extend(
[
f"## {artifact.title}",
"",
f"- ID: `{artifact.id}`",
f"- Kind: `{artifact.kind}`",
f"- Path: `{artifact.path}`",
f"- Relationships: {len(artifact.relationships)}",
"",
]
)
return "\n".join(lines).rstrip() + "\n"
def _render_by_concept(ownership: dict[str, Any]) -> str:
lines = _heading("By Concept")
lines.extend(
[
f"Concept count: **{ownership['concept_count']}**",
"",
"| Concept | Owner | Source |",
"| --- | --- | --- |",
]
)
for concept in ownership["concepts"]:
lines.append(
f"| {concept['concept']} | `{concept['owner']}` | `{concept['source']}` |"
)
lines.extend(["", "## Duplicate Candidates", ""])
duplicates = ownership["duplicate_candidates"]
if not duplicates:
lines.append("No duplicate concept candidates detected.")
else:
for duplicate in duplicates:
lines.append(f"- `{duplicate['normalized']}`")
lines.extend(["", "## Ownership Conflicts", ""])
conflicts = ownership["ownership_conflicts"]
if not conflicts:
lines.append("No ownership conflicts detected.")
else:
for conflict in conflicts:
owners = ", ".join(f"`{owner}`" for owner in conflict["owners"])
lines.append(f"- `{conflict['normalized']}` owned by {owners}")
return "\n".join(lines).rstrip() + "\n"
def _render_by_profile(context: Any) -> str:
lines = _heading("By Profile")
profiles = sorted((context.infospace_root / "profiles").glob("*/profile.yaml"))
if not profiles:
lines.append("No profiles have been registered yet.")
for path in profiles:
lines.extend(
[
f"## {path.parent.name}",
"",
f"- Path: `{path.relative_to(context.infospace_root)}`",
"",
]
)
return "\n".join(lines).rstrip() + "\n"
def _render_by_mapping_target(context: Any) -> str:
incoming: dict[str, list[tuple[str, str]]] = defaultdict(list)
for artifact in context.infospace.artifacts:
for relationship in artifact.relationships:
target = relationship.get("target")
relation_type = str(relationship.get("type") or "related")
if isinstance(target, str):
incoming[target].append((artifact.id, relation_type))
lines = _heading("By Mapping Target")
for target in sorted(incoming):
lines.extend([f"## `{target}`", ""])
for source, relation_type in sorted(incoming[target]):
lines.append(f"- `{source}` via `{relation_type}`")
lines.append("")
return "\n".join(lines).rstrip() + "\n"
def _render_kernel_overview(context: Any) -> str:
kind_counts: dict[str, int] = defaultdict(int)
relationship_counts: dict[str, int] = defaultdict(int)
for artifact in context.infospace.artifacts:
kind_counts[artifact.kind] += 1
for relationship in artifact.relationships:
relationship_counts[str(relationship.get("type") or "related")] += 1
lines = _heading("Kernel Overview")
lines.extend(
[
f"- Infospace: `{context.infospace.config.slug}`",
f"- Artifacts: {len(context.infospace.artifacts)}",
"",
"## Artifact Kinds",
"",
]
)
for kind, count in sorted(kind_counts.items()):
lines.append(f"- `{kind}`: {count}")
lines.extend(["", "## Relationship Types", ""])
for relation_type, count in sorted(relationship_counts.items()):
lines.append(f"- `{relation_type}`: {count}")
return "\n".join(lines).rstrip() + "\n"
def _render_import_matrix(matrix: dict[str, Any]) -> str:
artifacts = matrix["artifacts"]
lines = _heading("Import Matrix")
header = "| Artifact | " + " | ".join(f"`{artifact}`" for artifact in artifacts) + " |"
divider = "| --- | " + " | ".join("---" for _ in artifacts) + " |"
lines.extend([header, divider])
for row in matrix["rows"]:
cells = []
targets = row["targets"]
for artifact in artifacts:
cells.append(", ".join(f"`{item}`" for item in targets.get(artifact, [])))
lines.append(f"| `{row['artifact']}` | " + " | ".join(cells) + " |")
return "\n".join(lines).rstrip() + "\n"
def _render_repository_tree(tree: dict[str, Any]) -> str:
lines = _heading("Repository Tree")
lines.append(f"File count: **{tree['file_count']}**")
lines.append("")
for file_info in tree["files"]:
lines.append(f"- `{file_info['path']}`")
return "\n".join(lines).rstrip() + "\n"
def _render_global_agent_brief(context: Any) -> str:
lines = _heading("Global Agent Brief")
lines.extend(
[
"This brief summarizes the current canon service surface for agents.",
"",
f"- Infospace slug: `{context.infospace.config.slug}`",
f"- Artifact count: {len(context.infospace.artifacts)}",
"- Primary confidence command: `make validate`",
"- Refresh generated indexes and views with: `make index`",
"",
"## Useful Commands",
"",
"- `PYTHONPATH=src python3 -m info_tech_canon inspect`",
"- `PYTHONPATH=src python3 -m info_tech_canon validate`",
"- `PYTHONPATH=src python3 -m info_tech_canon graph`",
"- `PYTHONPATH=src python3 -m info_tech_canon index`",
"",
"## Consumption Notes",
"",
"- Treat `seeds/` as provenance.",
"- Treat `infospace/` as the service-consumable canon root.",
"- Generated files are marked and can be refreshed deterministically.",
]
)
return "\n".join(lines).rstrip() + "\n"
def _heading(title: str) -> list[str]:
return [GENERATED_NOTICE, "", f"# {title}", ""]
def _write_text(path: Path, content: str) -> dict[str, Any]:
path.parent.mkdir(parents=True, exist_ok=True)
old = path.read_text(encoding="utf-8") if path.exists() else None
changed = old != content
if changed:
path.write_text(content, encoding="utf-8")
return {"path": str(path), "changed": changed}
def _write_yaml(path: Path, data: dict[str, Any]) -> dict[str, Any]:
path.parent.mkdir(parents=True, exist_ok=True)
content = yaml.safe_dump(data, sort_keys=False)
return _write_text(path, content)
def _result(kind: str, files: list[dict[str, Any]]) -> dict[str, Any]:
return {
"ok": True,
"kind": kind,
"count": len(files),
"changed": [item for item in files if item["changed"]],
"files": files,
}
def _frontmatter(path: Path) -> dict[str, Any]:
text = path.read_text(encoding="utf-8")
if not text.startswith("---\n"):
return {}
end = text.find("\n---\n", 4)
if end == -1:
return {}
data = yaml.safe_load(text[4:end]) or {}
return data if isinstance(data, dict) else {}
def _normalize_concept(value: str) -> str:
return "-".join(value.lower().replace("_", "-").split())
def _is_generated(path: Path) -> bool:
try:
return path.read_text(encoding="utf-8").startswith(GENERATED_NOTICE)
except FileNotFoundError:
return False