Files
2026-07-24 20:47:16 +00:00

266 lines
9.9 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""Search the bundled SAP template registry for templates that match a design pattern.
This is the "if SAP did something similar, find it for me" tool. Use cases
the LLM should reach for it:
- "I need to draw 4 zones with a vertical network divider — what SAP
template has that already?"
- "Which templates use exactly TRUST + Authenticate + A2A + MCP pills?"
- "Show me templates with ≥ 6 BTP service icons inside a Cloud Solutions
band, so I can match the spacing/sizing they use."
- "Which templates have Joule as a separate purple zone (not nested in BTP)?"
The script reads the precomputed `assets/reference-examples/template-profiles.json`
(built by `profile_template.py --build-registry`) and ranks templates by how
well their structural profile matches the query.
Usage:
find_pattern.py "vertical network divider"
find_pattern.py "joule purple zone"
find_pattern.py --pill TRUST --pill A2A --pill MCP
find_pattern.py --zones 4 --icons-min 8
find_pattern.py --pattern tri-zone-joule-btp-third-party
find_pattern.py --top 5 --json "identity flow at bottom"
Output: ranked templates with the matching evidence per template.
"""
from __future__ import annotations
import argparse
import json
import re
import sys
from dataclasses import dataclass, field
from pathlib import Path
THIS_DIR = Path(__file__).resolve().parent
REGISTRY_DEFAULT = THIS_DIR.parent / "assets" / "reference-examples" / "template-profiles.json"
@dataclass
class Match:
name: str
score: float
reasons: list[str] = field(default_factory=list)
profile: dict = field(default_factory=dict)
def load_registry(path: Path) -> dict:
if not path.exists():
print(
f"registry not found: {path}\n"
"Run: python3 .../scripts/profile_template.py --build-registry",
file=sys.stderr,
)
sys.exit(2)
return json.loads(path.read_text(encoding="utf-8"))
def textual_score(profile: dict, terms: set[str]) -> tuple[float, list[str]]:
"""Score how often the search terms appear in the profile's textual fields."""
haystack_parts = [
profile.get("title", ""),
profile.get("description", ""),
" ".join(profile.get("aliases") or []),
" ".join(profile.get("tags") or []),
" ".join(profile.get("detected_patterns") or []),
" ".join(p.get("label", "") for p in (profile.get("zones") or [])),
" ".join(p.get("label", "") for p in (profile.get("cards") or [])),
" ".join(profile.get("pill_vocab") or []),
]
haystack = " ".join(str(s) for s in haystack_parts).lower()
hits = []
score = 0.0
for t in terms:
n = haystack.count(t)
if n > 0:
hits.append(f"{t}×{n}")
score += n
return score, hits
def pattern_match(profile: dict, want_patterns: list[str]) -> tuple[float, list[str]]:
have = set(profile.get("detected_patterns") or [])
matched = [p for p in want_patterns if p in have]
return (len(matched) * 8.0, matched)
def pill_match(profile: dict, want_pills: list[str]) -> tuple[float, list[str]]:
have = set(p.lower() for p in (profile.get("pill_vocab") or []))
matched = [p for p in want_pills if p.lower() in have]
return (len(matched) * 5.0, matched)
def structure_match(profile: dict, want: dict) -> tuple[float, list[str]]:
"""Partial-credit scoring for desired counts (zones, cards, icons, pills, edges)."""
structure = profile.get("structure_summary", {})
score = 0.0
reasons = []
pairs = {
"zones": "top_level_zones",
"nested_zones": "nested_zones",
"cards": "cards",
"icons": "icons",
"pills": "pills",
"edges": "edges",
}
for key, prof_key in pairs.items():
if want.get(key) is not None:
target = int(want[key])
actual = int(structure.get(prof_key, 0))
# Award full points when exact, partial when within 20%
if actual == target:
score += 6.0
reasons.append(f"{key}={actual}")
elif abs(actual - target) <= max(2, target * 0.2):
score += 3.0
reasons.append(f"{key}={actual}~={target}")
if want.get(f"{key}_min") is not None:
target = int(want[f"{key}_min"])
actual = int(structure.get(prof_key, 0))
if actual >= target:
score += 2.0
reasons.append(f"{key}>={actual}")
return score, reasons
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("query", nargs="*", help="free-text search across titles, tags, patterns, labels")
ap.add_argument("--registry", type=Path, default=REGISTRY_DEFAULT)
ap.add_argument("--top", type=int, default=5)
ap.add_argument("--json", action="store_true")
ap.add_argument("--pattern", action="append", default=[],
help="require a specific detected_patterns tag (repeatable)")
ap.add_argument("--pill", action="append", default=[],
help="require a specific pill verb in vocab (repeatable)")
ap.add_argument("--zones", type=int, help="prefer templates with this many top-level zones")
ap.add_argument("--zones-min", type=int)
ap.add_argument("--cards", type=int)
ap.add_argument("--cards-min", type=int)
ap.add_argument("--icons", type=int)
ap.add_argument("--icons-min", type=int)
ap.add_argument("--pills", type=int)
ap.add_argument("--pills-min", type=int)
ap.add_argument("--edges", type=int)
ap.add_argument("--edges-min", type=int)
ap.add_argument("--family", help="restrict to a specific family tag (e.g. ra0029, btp, ext-mdi)")
ap.add_argument("--list-patterns", action="store_true",
help="print every detected_patterns tag observed in the registry")
ap.add_argument("--list-pills", action="store_true",
help="print every pill verb observed across the registry")
args = ap.parse_args()
reg = load_registry(args.registry)
profiles = reg.get("templates", {})
if args.list_patterns:
all_pats: dict[str, int] = {}
for prof in profiles.values():
for p in (prof.get("detected_patterns") or []):
all_pats[p] = all_pats.get(p, 0) + 1
for pat, n in sorted(all_pats.items(), key=lambda kv: -kv[1]):
print(f" {n:>3}× {pat}")
return 0
if args.list_pills:
all_pills: dict[str, int] = {}
for prof in profiles.values():
for p in (prof.get("pill_vocab") or []):
all_pills[p] = all_pills.get(p, 0) + 1
for pill, n in sorted(all_pills.items(), key=lambda kv: -kv[1]):
print(f" {n:>3}× {pill!r}")
return 0
free_text = " ".join(args.query).lower().strip()
terms = set(re.findall(r"[a-z0-9]+", free_text)) if free_text else set()
want_structure = {
"zones": args.zones, "zones_min": args.zones_min,
"cards": args.cards, "cards_min": args.cards_min,
"icons": args.icons, "icons_min": args.icons_min,
"pills": args.pills, "pills_min": args.pills_min,
"edges": args.edges, "edges_min": args.edges_min,
}
matches: list[Match] = []
for name, profile in profiles.items():
if args.family and profile.get("family") != args.family:
continue
score = 0.0
reasons: list[str] = []
if terms:
s, hits = textual_score(profile, terms)
if s > 0:
score += s
reasons.append("text: " + ", ".join(hits[:8]))
if args.pattern:
s, hits = pattern_match(profile, args.pattern)
if s > 0:
score += s
reasons.append("pattern: " + ", ".join(hits))
else:
# Required patterns not found — skip this template
continue
if args.pill:
s, hits = pill_match(profile, args.pill)
if s > 0:
score += s
reasons.append("pills: " + ", ".join(hits))
s, hits = structure_match(profile, want_structure)
if s > 0:
score += s
reasons.append("structure: " + ", ".join(hits))
# Light prior: prefer primary templates when scores are tied
if profile.get("primary"):
score += 0.5
reasons.append("(primary)")
if score > 0 or not (terms or args.pattern or args.pill or any(want_structure.values())):
matches.append(Match(name=name, score=score, reasons=reasons, profile=profile))
matches.sort(key=lambda m: (-m.score, m.name))
matches = matches[: args.top]
if args.json:
out = [{
"name": m.name,
"score": round(m.score, 1),
"reasons": m.reasons,
"title": m.profile.get("title", ""),
"description": m.profile.get("description", ""),
"structure": m.profile.get("structure_summary", {}),
"detected_patterns": m.profile.get("detected_patterns", []),
"pill_vocab": m.profile.get("pill_vocab", []),
} for m in matches]
print(json.dumps(out, indent=2))
return 0
if not matches:
print("no templates matched the criteria")
return 0
for i, m in enumerate(matches, 1):
print(f"{i}. {m.score:5.1f} {m.name}")
if m.profile.get("title"):
print(f" title : {m.profile['title']}")
if m.profile.get("structure_summary"):
print(f" structure: {m.profile['structure_summary']}")
if m.profile.get("detected_patterns"):
print(f" patterns : {', '.join(m.profile['detected_patterns'][:8])}")
if m.profile.get("pill_vocab"):
print(f" pills : {', '.join(repr(p) for p in m.profile['pill_vocab'][:6])}")
for r in m.reasons:
print(f" • {r}")
return 0
if __name__ == "__main__":
sys.exit(main())