generated from coulomb/repo-seed
Add validation indexes and generated views
This commit is contained in:
427
src/info_tech_canon/generation.py
Normal file
427
src/info_tech_canon/generation.py
Normal 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
|
||||
Reference in New Issue
Block a user