Files
rune/scripts/query_domain.py
Nick 6fea6163c3 feat(factory): domain registry — 58 domains, 574 package paths, full-dichotomy instant lookup + gate
docs/factory/domains.json covers tui/ 37 + src/go/ 95 + src/rust/ 103
+ src/rune/ 339 (+4 retirement-pending). Every row: packages, key files,
batteries, serve endpoints, symptoms, contract IDs, retirement links.
query_domain.py: fuzzy lookup by domain OR failure symptom (real runs
proven). Gate: unmapped package/phantom path/malformed row = RED.
19 tests red-first. Main-session verify: lookups green own runs.
2026-10-17 16:22:23 +01:00

172 lines
5.9 KiB
Python

#!/usr/bin/env python3
"""query_domain.py — the instant domain lookup (wave wave-domain-registry).
Answers the owner critique: diagnosis must be a lookup, not grep-exploration.
`python scripts/query_domain.py <symptom-or-domain>` prints the matching
domain row from docs/factory/domains.json — its packages across all four
worlds (tui/, src/go/, src/rust/ crates, src/rune/ authority modules), the
key files (where a fix starts), the test batteries, serve-surface endpoints,
and the failure symptoms that fingerprint the domain.
Matching is fuzzy on purpose: domain names, aliases, summaries, symptoms,
and package basenames all score. Exit 0 with the best hit; exit 1 with the
closest matches when nothing clears the bar (a miss is loud, never blank).
"""
from __future__ import annotations
import argparse
import difflib
import json
import re
import sys
from pathlib import Path
REPO = Path(__file__).resolve().parent.parent
REGISTRY = REPO / "docs/factory/domains.json"
TOKEN_RE = re.compile(r"[a-z0-9]+")
SCORE_HIT = 6.0 # a query token matching a high-signal field
SCORE_PACKAGE = 3.0 # package basename hit
SCORE_RATIO = 4.0 # fuzzy phrase ratio on name/symptom strings
def _tokens(text: str) -> set[str]:
return set(TOKEN_RE.findall(text.lower()))
def _domain_text(row: dict) -> str:
parts = [row["domain"], " ".join(row.get("aliases", [])),
row.get("summary", "")]
parts.extend(row.get("symptoms", []))
parts.extend(str(kf.get("path", "")) for kf in row.get("key_files", []))
return " ".join(parts)
def _package_names(row: dict) -> list[str]:
names: list[str] = []
for pkg_list in row.get("packages", {}).values():
for p in pkg_list:
names.extend(TOKEN_RE.findall(Path(p).name.lower()))
names.extend(TOKEN_RE.findall(Path(p).parent.name.lower()))
return names
def score_row(row: dict, query: str) -> float:
q = " ".join(query.lower().split())
q_tokens = _tokens(q)
if not q_tokens:
return 0.0
name = row["domain"].lower()
aliases = [a.lower() for a in row.get("aliases", [])]
score = 0.0
# exact / substring dominance
if q == name or q in aliases:
return 1000.0
if q in name or any(q in a for a in aliases):
score += 100.0
if name in q:
score += 60.0
high_signal = _tokens(_domain_text(row))
score += SCORE_HIT * len(q_tokens & high_signal)
pkg_tokens = set(_package_names(row))
score += SCORE_PACKAGE * len(q_tokens & pkg_tokens)
# fuzzy phrase ratio keeps typos and phrasings honest
candidates = [name] + aliases + [s.lower() for s in row.get("symptoms", [])]
best = max(difflib.SequenceMatcher(None, q, c).ratio() for c in candidates)
score += SCORE_RATIO * best
# strong symptom phrases should dominate over incidental token overlap
for s in row.get("symptoms", []):
s_low = s.lower()
if q in s_low or s_low in q:
score += 50.0
break
return score
def _format_row(row: dict) -> str:
lines = [f"domain: {row['domain']}"]
if row.get("aliases"):
lines.append(f" aliases: {', '.join(row['aliases'])}")
lines.append(f" summary: {row['summary']}")
pkgs = row.get("packages", {})
for world in ("tui", "src_go", "src_rust", "src_rune"):
if pkgs.get(world):
lines.append(f" {world}: {' '.join(pkgs[world])}")
if row.get("serve_endpoints"):
lines.append(f" serve-surface: {' '.join(row['serve_endpoints'])}")
lines.append(" fix starts at:")
for kf in row["key_files"][:4]:
lines.append(f" - {kf['path']} ({kf['why']})")
lines.append(" test batteries:")
for tb in row["test_batteries"][:6]:
lines.append(f" - {tb}")
lines.append(" failure symptoms:")
for s in row["symptoms"][:5]:
lines.append(f" - {s}")
if row.get("scaffold_contract"):
lines.append(f" scaffold contract: {row['scaffold_contract']}")
if row.get("retirement_row"):
lines.append(f" retirement-map row: {row['retirement_row']}")
return "\n".join(lines)
def main(argv: list[str] | None = None) -> int:
ap = argparse.ArgumentParser(description="domain registry lookup")
ap.add_argument("query", nargs="+", help="symptom or domain name")
ap.add_argument("--json", action="store_true", dest="as_json",
help="emit the matched row(s) as JSON")
ap.add_argument("--all", action="store_true",
help="list every domain name and exit 0")
args = ap.parse_args(argv)
if not REGISTRY.is_file():
print(f"RED: {REGISTRY} missing — the domain registry IS the map",
file=sys.stderr)
return 1
doc = json.loads(REGISTRY.read_text())
rows = doc["domains"]
if args.all:
for row in rows:
print(row["domain"])
return 0
query = " ".join(args.query)
scored = sorted(
((score_row(r, query), r) for r in rows),
key=lambda pair: pair[0], reverse=True)
top_score, top = scored[0]
# A hit must actually relate to the query: some token overlap or a
# strong fuzzy ratio — never a free win off pure noise.
q_tokens = _tokens(query)
relates = (top_score >= 9.0
and (q_tokens & (_tokens(_domain_text(top))
| set(_package_names(top)))))
if relates:
if args.as_json:
print(json.dumps(top, indent=2))
else:
print(_format_row(top))
if len(scored) > 1 and scored[1][0] >= 0.5 * top_score:
other = scored[1][1]["domain"]
print(f"(also plausible: {other} — re-query it by name)")
return 0
print(f"no domain matched {query!r}. closest matches:")
for score, row in scored[:3]:
print(f" {row['domain']} (score {score:.1f})")
print("try: python scripts/query_domain.py --all")
return 1
if __name__ == "__main__":
sys.exit(main())