feat: mirror tag-vocabulary summary and note-type validation
Ports two changes from the knowledge-base vault this skeleton is extracted from, keeping the shared scripts byte-identical across the pair: - `graph_summary` reports `by_tag` and `tag_count`, sized by `topTags`, and drops the `by_type`/`by_status` breakdowns. An MCP-only agent could not otherwise discover the tag spellings that `graph_search_nodes` matches verbatim. - `generate-graph.py --validate` now enforces a controlled vocabulary for a note's `type`, which is where the drift signal `by_type` used to give now lives. `AGENT_QUERY_GUIDE.md`, both skills and the-knowing-one quote the enforced enum. `tests/test_generate_graph.py` is new; `make test` is 50 tests. Regenerating the graph also drops a stale `GLOBAL_AGENTS` node for a file this skeleton does not carry. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
638cbe6b39
commit
1cb6369282
@@ -1,4 +1,13 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build `index/graph.json` from the vault's Markdown, and validate it.
|
||||
|
||||
The graph is the derived navigation layer agents query instead of walking the
|
||||
tree: one node per note (frontmatter plus link counts) and one edge per typed
|
||||
markdown link. `--validate` is the gate that keeps it trustworthy — it fails on
|
||||
links that resolve to nothing, on edge relationship types or note `type` values
|
||||
outside their controlled vocabularies, and on legacy `[[wiki links]]`.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
@@ -36,6 +45,24 @@ RELATION_TYPES = {
|
||||
}
|
||||
|
||||
|
||||
# Strict controlled vocabulary for a note's frontmatter `type`. A note without
|
||||
# one is a "note"; --validate fails on any value outside this set, which is what
|
||||
# keeps the facet meaningful as the vault grows.
|
||||
NOTE_TYPES = {
|
||||
"hub",
|
||||
"project",
|
||||
"area",
|
||||
"guideline",
|
||||
"policy",
|
||||
"feature",
|
||||
"roadmap",
|
||||
"decision",
|
||||
"note",
|
||||
"meta",
|
||||
"reference",
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class LinkRef:
|
||||
target: str # raw target text (resolution key: id / rel-path / title / stem, or a URL)
|
||||
@@ -402,7 +429,8 @@ def parse_args() -> argparse.Namespace:
|
||||
parser.add_argument(
|
||||
"--validate",
|
||||
action="store_true",
|
||||
help="validate links: unresolved targets, edge relationship types, "
|
||||
help="validate the graph: unresolved targets, edge relationship types, "
|
||||
"note types, "
|
||||
"and legacy [[wiki links]] (all rejected)",
|
||||
)
|
||||
parser.add_argument(
|
||||
@@ -453,6 +481,25 @@ def print_invalid_types(invalid: List[Dict[str, object]]) -> None:
|
||||
print(f"- {e.get('from')} -> {e.get('to')}: type={e.get('type')!r}")
|
||||
|
||||
|
||||
def find_invalid_node_types(graph: Dict[str, object]) -> List[Dict[str, object]]:
|
||||
return [
|
||||
n
|
||||
for n in graph.get("nodes", []) # type: ignore[union-attr]
|
||||
if n.get("type") not in NOTE_TYPES
|
||||
]
|
||||
|
||||
|
||||
def print_invalid_node_types(invalid: List[Dict[str, object]]) -> None:
|
||||
if not invalid:
|
||||
print("Validation passed: all node types are in the controlled vocabulary")
|
||||
return
|
||||
|
||||
print(f"Validation failed: {len(invalid)} note(s) with unknown type")
|
||||
print(f" allowed: {', '.join(sorted(NOTE_TYPES))}")
|
||||
for n in invalid:
|
||||
print(f"- {n.get('id')} ({n.get('path')}): type={n.get('type')!r}")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
args = parse_args()
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
@@ -490,8 +537,10 @@ def main() -> None:
|
||||
print_unresolved(unresolved)
|
||||
invalid = find_invalid_types(graph)
|
||||
print_invalid_types(invalid)
|
||||
invalid_nodes = find_invalid_node_types(graph)
|
||||
print_invalid_node_types(invalid_nodes)
|
||||
print_wiki_links(wiki_uses)
|
||||
if unresolved or invalid or wiki_uses:
|
||||
if unresolved or invalid or invalid_nodes or wiki_uses:
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user