feat(svglide): 完成 beautiful 模板知识吸收闭环

补齐 34 个 beautiful-html-template family 的 cjk_policy、family_usage_policy、extension_grammar 和 cover/mid/late benchmark roles。

接入 matcher、planner、theme selector、preflight、quality gate 与 dry-run receipt,阻断跨 family 混用、CJK fake italic、未授权 recolor、source inventory claim escalation 等 M15 问题。

验证:M15 targeted 150 tests OK;full unittest discover 736 tests OK;dry-run internal-review / zhipu-minimax passed;json/schema ok;git diff --check OK。
This commit is contained in:
songtianyi.theo
2026-06-23 00:16:26 +08:00
parent 985089291f
commit 480d4e2fbb
12 changed files with 6422 additions and 136 deletions

View File

@@ -28,6 +28,9 @@
"claim_level",
"runtime_policy",
"font_policy",
"cjk_policy",
"family_usage_policy",
"extension_grammar",
"semantic_fit",
"design_tokens",
"visual_dna",
@@ -60,6 +63,62 @@
"required": ["fallback_stack", "font_role_map"],
"additionalProperties": true
},
"cjk_policy": {
"type": "object",
"required": [
"strategy",
"display_font_cn",
"body_font_cn",
"runtime_font_policy",
"emphasis_policy",
"italic_policy",
"letter_spacing_policy",
"mixed_run_spacing",
"known_degradation",
"source_section_sha256"
],
"additionalProperties": true,
"properties": {
"runtime_font_policy": {"const": "system_font_only_no_remote_dependency"},
"mixed_run_spacing": {"enum": ["pangu_spacing", "none_required"]}
}
},
"family_usage_policy": {
"type": "object",
"required": [
"closed_visual_system",
"cross_family_layout_mix_allowed",
"recolor_allowed",
"font_substitution_allowed",
"extend_missing_layout_policy"
],
"additionalProperties": true,
"properties": {
"closed_visual_system": {"const": true},
"cross_family_layout_mix_allowed": {"const": false},
"recolor_allowed": {"const": false},
"font_substitution_allowed": {"const": false},
"extend_missing_layout_policy": {
"type": "object",
"required": ["same_fonts", "same_palette", "same_spacing_rhythm", "same_component_grammar", "same_decorative_vocabulary", "same_chrome"],
"additionalProperties": true
}
}
},
"extension_grammar": {
"type": "object",
"required": [
"layout_rhythm",
"spacing_rhythm",
"component_grammar",
"chrome_rules",
"decorative_vocabulary",
"allowed_new_layouts",
"forbidden_mutations",
"source_basis"
],
"additionalProperties": true
},
"semantic_fit": {
"type": "object",
"required": ["best_for", "industries", "tones", "avoid_when"],
@@ -75,6 +134,21 @@
"required": ["palette_roles", "typography_roles", "decorative_motifs", "visual_effects", "screenshot_benchmarks"],
"additionalProperties": true,
"properties": {
"screenshot_benchmarks": {
"type": "array",
"minItems": 3,
"items": {
"type": "object",
"required": ["path", "role", "slide_number", "why_selected", "visual_targets", "acceptance_use"],
"additionalProperties": true,
"properties": {
"role": {"enum": ["cover_reference", "mid_deck_reference", "late_deck_reference"]},
"slide_number": {"type": "integer", "minimum": 1},
"visual_targets": {"type": "array", "minItems": 1, "items": {"type": "string"}},
"acceptance_use": {"type": "array", "minItems": 1, "items": {"type": "string"}}
}
}
},
"visual_effects": {
"type": "array",
"items": {

View File

@@ -11,5 +11,16 @@
"live_submit_missing_file_token",
"unowned_decorative_primitive",
"decorative_motif_overuse"
],
"phase_1_knowledge_absorption": [
"cross_family_layout_mix",
"missing_extension_grammar",
"remote_font_dependency",
"cjk_fake_italic",
"cjk_letter_spacing_inherited",
"cjk_mixed_run_spacing_missing",
"family_recolor_without_override",
"source_inventoried_claim_escalation",
"missing_screenshot_benchmark_role"
]
}

View File

@@ -337,6 +337,235 @@ def screenshot_paths(source_root: Path, slug: str) -> list[str]:
return [f"beautiful-html-templates/screenshots/{path.name}" for path in matches[:3]]
def readme_gallery_by_slug(source_root: Path) -> dict[str, list[str]]:
readme = source_root / "README.md"
if not readme.exists():
return {}
by_slug: dict[str, list[str]] = {}
for match in re.finditer(r"\./screenshots/([a-z0-9-]+-\d+\.png)", readme.read_text(encoding="utf-8")):
name = match.group(1)
slug = re.sub(r"-\d+\.png$", "", name)
by_slug.setdefault(slug, []).append(f"beautiful-html-templates/screenshots/{name}")
return {slug: paths[:3] for slug, paths in by_slug.items() if paths}
def screenshot_slide_number(path: str) -> int:
match = re.search(r"-(\d+)\.png$", path)
return int(match.group(1)) if match else 0
def screenshot_benchmarks(source_root: Path, slug: str, template_json: dict[str, Any], visual_targets: list[str], gallery_by_slug: dict[str, list[str]]) -> list[dict[str, Any]]:
paths = gallery_by_slug.get(slug) or screenshot_paths(source_root, slug)
roles = ["cover_reference", "mid_deck_reference", "late_deck_reference"]
tagline = str(template_json.get("tagline") or template_json.get("description") or template_json.get("best_for") or slug)
out: list[dict[str, Any]] = []
for index, path in enumerate(paths[:3]):
role = roles[index] if index < len(roles) else "reference"
out.append(
{
"path": path,
"role": role,
"slide_number": screenshot_slide_number(path),
"why_selected": f"README gallery {role.replace('_', ' ')} for {slug}: {tagline[:180]}",
"visual_targets": visual_targets[:6] or ["palette", "type_scale", "chrome", "density", "layout_balance"],
"acceptance_use": ["matcher_thumbnail", "visual_qa", "few_shot_reference"],
}
)
return out
def markdown_section(markdown: str, heading: str) -> str:
lines = markdown.splitlines()
start: int | None = None
start_level = 0
for index, line in enumerate(lines):
match = re.match(r"^(#{2,6})\s+(.+?)\s*$", line)
if not match:
continue
if match.group(2).strip().lower() == heading.lower():
start = index
start_level = len(match.group(1))
break
if start is None:
return ""
end = len(lines)
for index in range(start + 1, len(lines)):
match = re.match(r"^(#{2,6})\s+", lines[index])
if match and len(match.group(1)) <= start_level:
end = index
break
return "\n".join(lines[start:end]).strip()
def first_section_paragraph(section: str, heading: str) -> str:
subsection = markdown_section(section, heading)
if not subsection:
return ""
chunks = []
for raw in subsection.splitlines()[1:]:
line = raw.strip()
if not line or line.startswith("#") or line.startswith("|") or line.startswith("```"):
continue
if line.startswith("- "):
line = line[2:].strip()
chunks.append(line)
if len(" ".join(chunks)) > 240:
break
return " ".join(chunks)[:360]
CJK_FONT_PATTERNS = [
r"Noto\s+(?:Sans|Serif)\s+(?:SC|TC|JP|CJK)",
r"Source\s+Han\s+(?:Sans|Serif)",
r"LXGW\s+[A-Za-z ]+",
r"ZCOOL\s+[A-Za-z]+",
r"PingFang\s+SC",
r"Microsoft\s+YaHei",
r"SimHei",
r"SimSun",
r"Yozai",
r"悠哉字体\s*Yozai",
r"霞鹜[^|,,。)`*]+",
r"站酷[^|,,。)`*]+",
]
def clean_font_name(value: str) -> str:
cleaned = re.sub(r"[`*_\"'()\[\]]", "", value).strip()
cleaned = re.sub(r"\s+", " ", cleaned)
return cleaned[:80]
def cjk_font_candidates(cjk_section: str) -> list[str]:
candidates: list[str] = []
for pattern in CJK_FONT_PATTERNS:
for match in re.finditer(pattern, cjk_section, flags=re.IGNORECASE):
name = clean_font_name(match.group(0))
if name and name.lower() not in {item.lower() for item in candidates}:
candidates.append(name)
return candidates
def classify_italic_policy(cjk_section: str, full_design: str) -> str:
text = f"{cjk_section}\n{full_design}".lower()
if re.search(r"never italic|no italic|drop italic|italic does not exist|fake italic|oblique", text):
return "drop_italic"
if re.search(r"color shift|color-only|color only|emphasis.*color", text):
return "color_only_emphasis"
if re.search(r"weight|bold", text):
return "weight_only_emphasis"
return "latin_only"
def classify_letter_spacing_policy(cjk_section: str) -> str:
text = cjk_section.lower()
if re.search(r"letter-spacing|tracking|tracked|uppercase|text-transform", text):
return "reset_letter_spacing_for_cjk_keep_latin_labels"
return "reset_letter_spacing_for_cjk"
def extract_cjk_policy(design_md: str, template_json: dict[str, Any]) -> dict[str, Any]:
cjk_section = markdown_section(design_md, "CJK & International Content")
fonts = cjk_font_candidates(cjk_section)
display_font = fonts[0] if fonts else "Noto Sans SC"
body_font = fonts[1] if len(fonts) > 1 else display_font
known_gap = first_section_paragraph(cjk_section, "Known CJK Gap") or first_section_paragraph(cjk_section, "Aesthetic Notes")
if not known_gap:
known_gap = "Preserve the family rhythm while replacing unsafe remote CJK font loading with SVGlide system font roles."
italic_policy = classify_italic_policy(cjk_section, design_md)
emphasis_policy = "color_or_weight_only" if italic_policy != "latin_only" else "latin_only_emphasis"
return {
"strategy": "replace_family_whole_element" if fonts else "single_cjk_family_all_roles",
"display_font_cn": display_font,
"body_font_cn": body_font,
"mono_font_policy": "latin_only_or_system_mono",
"runtime_font_policy": "system_font_only_no_remote_dependency",
"runtime_font_stack": ["system-sans-cjk", "system-sans-cjk-heavy", "system-sans-cjk-regular", "system-mono"],
"emphasis_policy": emphasis_policy,
"italic_policy": italic_policy,
"letter_spacing_policy": classify_letter_spacing_policy(cjk_section),
"mixed_run_spacing": "pangu_spacing" if re.search(r"Pangu|盘古", cjk_section, re.IGNORECASE) else "none_required",
"latin_accent_policy": "latin_only_allowed_when_semantic_annotation",
"known_degradation": known_gap,
"design_intent_font_pairing": fonts[:6],
"source_section_heading": "CJK & International Content",
"source_section_sha256": hashlib.sha256(cjk_section.encode("utf-8")).hexdigest(),
}
def extract_family_usage_policy(agents_md: str) -> dict[str, Any]:
return {
"closed_visual_system": True,
"cross_family_layout_mix_allowed": False,
"recolor_allowed": False,
"font_substitution_allowed": False,
"decorative_elements_policy": "identity_element_not_noise",
"extend_missing_layout_policy": {
"same_fonts": True,
"same_palette": True,
"same_spacing_rhythm": True,
"same_component_grammar": True,
"same_decorative_vocabulary": True,
"same_chrome": True,
},
"soft_matching_policy": {
"tone_first": True,
"occasion_is_soft_signal": True,
"avoid_for_is_soft_warning": True,
"taste_can_override_industry": True,
"formality_density_as_sanity_check": True,
},
"hard_rules": [
"preserve_fonts_palette_grid_slide_classes_decorative_elements",
"do_not_recolor_without_explicit_brand_override",
"do_not_mix_template_families",
"extend_missing_layout_inside_same_family",
"do_not_strip_identity_decorations",
],
"source": "beautiful-html-templates/AGENTS.md",
"source_sha256": hashlib.sha256(agents_md.encode("utf-8")).hexdigest(),
}
def extract_extension_grammar(
slug: str,
template_json: dict[str, Any],
design_blocks: dict[str, dict[str, str]],
class_names: list[str],
combined_text: str,
) -> dict[str, Any]:
density = str(template_json.get("density") or "medium")
formality = str(template_json.get("formality") or "medium")
tagline = str(template_json.get("tagline") or template_json.get("description") or template_json.get("best_for") or slug)
component_tokens = sorted(
{
token
for token in (
list(design_blocks.get("components", {}).keys())
+ [name.removeprefix("layout-") for name in class_names if name.startswith(("layout-", "s-", "card", "panel", "stat"))]
)
if token
}
)
chrome = [name for name in class_names if re.search(r"footer|page|number|chrome|label|eyebrow|nav|badge|stamp", name)]
motifs = classify_decorative_motifs(combined_text)
return {
"layout_rhythm": f"{slug}: {density} density, {formality} formality. {tagline[:180]}",
"spacing_rhythm": "Reuse source spacing tokens and grid gaps: " + json.dumps(design_blocks.get("spacing", {}), ensure_ascii=False)[:220],
"component_grammar": component_tokens[:10] or DEFAULT_COMPONENTS[:6],
"chrome_rules": chrome[:8] or ["preserve family header/footer labels", "preserve page-number rhythm", "no unrelated chrome"],
"decorative_vocabulary": motifs,
"allowed_new_layouts": ["comparison", "risk", "timeline", "action_plan", "case_evidence", "metric_dashboard"],
"forbidden_mutations": ["new_palette", "cross_family_components", "fake_italic", "new_decorative_motif", "remote_font_dependency"],
"density_limits": density,
"source_basis": {
"template_id": slug,
"design_signal_sha256": hashlib.sha256(combined_text[:12000].encode("utf-8")).hexdigest(),
"class_name_count": len(class_names),
},
}
def variant_record(variant_id: str) -> dict[str, Any]:
role_map = {
"cover": ["cover"],
@@ -378,6 +607,8 @@ def extract_family(
slug: str,
inventory_by_slug: dict[str, list[dict[str, Any]]] | None = None,
absorptions_by_slug: dict[str, list[dict[str, Any]]] | None = None,
agents_md: str = "",
gallery_by_slug: dict[str, list[str]] | None = None,
) -> dict[str, Any]:
template_dir = source_root / "templates" / slug
template_json = load_json(template_dir / "template.json")
@@ -417,8 +648,9 @@ def extract_family(
css_tokens = css_variables(template_html)
class_names = css_class_names(template_html)
layout_variants = html_layout_variants(template_html)
screenshots = screenshot_paths(source_root, slug)
screenshots = (gallery_by_slug or {}).get(slug) or screenshot_paths(source_root, slug)
palette_roles = palette_roles_from_tokens(palette, design_blocks.get("colors", {}), css_tokens)
visual_targets = ["palette", "type_scale", "chrome", "density", "layout_balance"] + classify_decorative_motifs(combined)[:2]
return {
"template_id": slug,
"source": {
@@ -456,6 +688,9 @@ def extract_family(
"mono": "system-mono",
},
},
"cjk_policy": extract_cjk_policy(design_md, template_json),
"family_usage_policy": extract_family_usage_policy(agents_md),
"extension_grammar": extract_extension_grammar(slug, template_json, design_blocks, class_names, combined),
"semantic_fit": {
"best_for": words(template_json.get("best_for")) or words(template_json.get("occasion")),
"industries": classify_industries(combined),
@@ -485,7 +720,7 @@ def extract_family(
},
"decorative_motifs": classify_decorative_motifs(combined),
"visual_effects": classify_visual_effects(combined),
"screenshot_benchmarks": [{"path": path, "role": "reference"} for path in screenshots],
"screenshot_benchmarks": screenshot_benchmarks(source_root, slug, template_json, visual_targets, gallery_by_slug or {}),
"density": str(template_json.get("density") or "medium"),
},
"svglide_mapping": {
@@ -509,6 +744,8 @@ def extract_registry(source_root: str | None = None) -> dict[str, Any]:
slugs = sorted(str(item.get("slug")) for item in templates if item.get("slug"))
inventory_by_slug = load_inventory_by_slug()
absorptions_by_slug = load_absorptions_by_slug()
agents_md = (root / "AGENTS.md").read_text(encoding="utf-8") if (root / "AGENTS.md").exists() else ""
gallery_by_slug = readme_gallery_by_slug(root)
return {
"version": "beautiful-html-template-families/v1",
"source": {
@@ -518,7 +755,7 @@ def extract_registry(source_root: str | None = None) -> dict[str, Any]:
"inventory_item_count": sum(len(items) for items in inventory_by_slug.values()),
"absorbed_family_count": len(absorptions_by_slug),
},
"families": [extract_family(root, slug, inventory_by_slug, absorptions_by_slug) for slug in slugs],
"families": [extract_family(root, slug, inventory_by_slug, absorptions_by_slug, agents_md, gallery_by_slug) for slug in slugs],
}

View File

@@ -5,6 +5,7 @@
from __future__ import annotations
import argparse
import hashlib
import json
from html import escape
from pathlib import Path
@@ -27,6 +28,28 @@ RECIPES = [
("spotlight_annotation", "annotation", ["spotlight", "annotation"], ["typography"]),
("brand_system", "brand", ["typography", "geometric_shape"], ["typography"]),
]
M15_POLICY_CODES = [
"cross_family_layout_mix",
"missing_extension_grammar",
"remote_font_dependency",
"cjk_fake_italic",
"cjk_letter_spacing_inherited",
"cjk_mixed_run_spacing_missing",
"family_recolor_without_override",
"source_inventoried_claim_escalation",
"missing_screenshot_benchmark_role",
]
def sha256_payload(value: Any) -> str:
blob = json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode("utf-8")
return "sha256:" + hashlib.sha256(blob).hexdigest()
def benchmark_roles(family: dict[str, Any]) -> list[str]:
visual_dna = family.get("visual_dna") if isinstance(family.get("visual_dna"), dict) else {}
benchmarks = visual_dna.get("screenshot_benchmarks") if isinstance(visual_dna.get("screenshot_benchmarks"), list) else []
return [str(item.get("role")) for item in benchmarks if isinstance(item, dict) and item.get("role")]
def style_plan_fields(case_id: str) -> dict[str, Any]:
@@ -158,12 +181,15 @@ def run_case(case_id: str, query: str, out_dir: Path, include_images: bool) -> d
case_dir = out_dir / case_id
case_dir.mkdir(parents=True, exist_ok=True)
matched_plan = beautiful_template_matcher.plan_with_template_family(query, page_count=10)
selected_family_id = matched_plan["template_family_selection"]["selected_template_id"]
selected_family = beautiful_template_matcher.load_family(selected_family_id)
slides: list[dict[str, Any]] = []
svg_paths: list[str] = []
for raw_slide in matched_plan["slides"]:
page = int(raw_slide["page"])
include_image = include_images and page in {1, 7}
slide, svg = make_slide(page, raw_slide["template_variant"], raw_slide.get("semantic_blocks", []), raw_slide["component_selection"], include_image)
slide["template_family_id"] = selected_family_id
svg_path = case_dir / f"page-{page:03d}.svg"
svg_path.write_text(svg, encoding="utf-8")
svg_paths.append(str(svg_path))
@@ -183,11 +209,19 @@ def run_case(case_id: str, query: str, out_dir: Path, include_images: bool) -> d
required_slots = sum(len(svg_preflight.required_image_slots(slide)) for slide in slides)
rendered_images = sum(file.get("visual_primitives", {}).get("counts", {}).get("image", 0) for file in preflight["files"])
issues = preflight.get("plan", {}).get("issues", []) + [issue for file in preflight["files"] for issue in file.get("issues", [])]
issue_codes = {issue.get("code") for issue in issues if isinstance(issue, dict)}
receipt = {
"version": "beautiful-template-e2e-dry-run/v1",
"case_id": case_id,
"query": query,
"selected_template_id": matched_plan["template_family_selection"]["selected_template_id"],
"selected_template_family": selected_family_id,
"claim_level": selected_family.get("claim_level"),
"cjk_policy_hash": sha256_payload(selected_family.get("cjk_policy")),
"family_usage_policy_hash": sha256_payload(selected_family.get("family_usage_policy")),
"extension_grammar_hash": sha256_payload(selected_family.get("extension_grammar")),
"benchmark_roles": benchmark_roles(selected_family),
"preflight_policy_checks": {code: ("failed" if code in issue_codes else "passed") for code in M15_POLICY_CODES},
"candidate_template_ids": matched_plan["template_family_selection"]["candidate_template_ids"],
"slide_count": len(slides),
"template_variant_count": len({slide["template_variant"] for slide in slides}),
@@ -227,8 +261,23 @@ def run_all(out_dir: Path = DEFAULT_OUT_DIR) -> dict[str, Any]:
def main() -> int:
parser = argparse.ArgumentParser(description="Run beautiful template matcher-to-preflight dry-run fixtures.")
parser.add_argument("--out-dir", default=str(DEFAULT_OUT_DIR))
parser.add_argument("--query", default=None)
parser.add_argument("--case-id", default="ad-hoc")
parser.add_argument("--dry-run", action="store_true")
parser.add_argument("--include-images", action="store_true")
args = parser.parse_args()
summary = run_all(Path(args.out_dir))
if args.query:
receipt = run_case(args.case_id, args.query, Path(args.out_dir), include_images=args.include_images)
summary = {
"version": "beautiful-template-e2e-dry-run-summary/v1",
"status": receipt["status"],
"receipt_count": 1,
"receipts": [receipt],
}
Path(args.out_dir).mkdir(parents=True, exist_ok=True)
(Path(args.out_dir) / "summary.json").write_text(json.dumps(summary, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
else:
summary = run_all(Path(args.out_dir))
print(json.dumps(summary, ensure_ascii=False, indent=2))
return 0 if summary["status"] == "passed" else 1

View File

@@ -0,0 +1,345 @@
# Copyright (c) 2026 Lark Technologies Pte. Ltd.
# SPDX-License-Identifier: MIT
from __future__ import annotations
import json
import sys
import tempfile
import unittest
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
import beautiful_template_asset_extractor
import beautiful_template_e2e_dry_run
import beautiful_template_matcher
import svglide_prompt_planner
import svglide_quality_gate
import svglide_theme_template_selector
import svg_preflight
REFERENCES_DIR = Path(__file__).resolve().parent.parent / "references"
SOURCE_ROOT = Path("/Users/bytedance/bd-projects/beautiful-html-templates")
REQUIRED_M15_CODES = {
"cross_family_layout_mix",
"missing_extension_grammar",
"remote_font_dependency",
"cjk_fake_italic",
"cjk_letter_spacing_inherited",
"cjk_mixed_run_spacing_missing",
"family_recolor_without_override",
"source_inventoried_claim_escalation",
"missing_screenshot_benchmark_role",
}
REQUIRED_BENCHMARK_ROLES = {"cover_reference", "mid_deck_reference", "late_deck_reference"}
def load_json(name: str) -> dict:
return json.loads((REFERENCES_DIR / name).read_text(encoding="utf-8"))
def family_by_id(registry: dict) -> dict[str, dict]:
return {family["template_id"]: family for family in registry["families"]}
def all_issue_codes(payload: object) -> set[str]:
codes: set[str] = set()
if isinstance(payload, dict):
for key, value in payload.items():
if key in {"code", "id"} and isinstance(value, str):
codes.add(value)
elif isinstance(value, str) and key.endswith("codes"):
codes.add(value)
else:
codes.update(all_issue_codes(value))
elif isinstance(payload, list):
for item in payload:
if isinstance(item, str):
codes.add(item)
else:
codes.update(all_issue_codes(item))
return codes
def issue_codes(result: dict) -> set[str]:
codes: set[str] = set()
for issue in result.get("issues", []):
if isinstance(issue, dict) and issue.get("code"):
codes.add(issue["code"])
return codes
class BeautifulTemplateKnowledgeAbsorptionTest(unittest.TestCase):
def test_issue_codes_freeze_m15_contract(self) -> None:
codes = all_issue_codes(load_json("beautiful-template-issue-codes.json"))
self.assertTrue(REQUIRED_M15_CODES <= codes, REQUIRED_M15_CODES - codes)
def test_all_families_have_cjk_policy_usage_policy_and_extension_grammar(self) -> None:
registry = load_json("beautiful-html-template-families.json")
self.assertEqual(len(registry["families"]), 34)
cjk_signatures: set[str] = set()
extension_signatures: set[str] = set()
for family in registry["families"]:
with self.subTest(family=family["template_id"]):
cjk = family.get("cjk_policy")
usage = family.get("family_usage_policy")
grammar = family.get("extension_grammar")
self.assertIsInstance(cjk, dict)
self.assertIsInstance(usage, dict)
self.assertIsInstance(grammar, dict)
for key in [
"strategy",
"display_font_cn",
"body_font_cn",
"runtime_font_policy",
"emphasis_policy",
"italic_policy",
"letter_spacing_policy",
"mixed_run_spacing",
"known_degradation",
"source_section_sha256",
]:
self.assertTrue(cjk.get(key), f"{family['template_id']} missing cjk_policy.{key}")
self.assertEqual(cjk["runtime_font_policy"], "system_font_only_no_remote_dependency")
self.assertTrue(usage.get("closed_visual_system"))
self.assertFalse(usage.get("cross_family_layout_mix_allowed"))
self.assertFalse(usage.get("recolor_allowed"))
self.assertFalse(usage.get("font_substitution_allowed"))
self.assertTrue(usage.get("extend_missing_layout_policy", {}).get("same_component_grammar"))
for key in [
"layout_rhythm",
"spacing_rhythm",
"component_grammar",
"chrome_rules",
"decorative_vocabulary",
"allowed_new_layouts",
"forbidden_mutations",
"source_basis",
]:
self.assertTrue(grammar.get(key), f"{family['template_id']} missing extension_grammar.{key}")
cjk_signatures.add(json.dumps(cjk, sort_keys=True, ensure_ascii=False))
extension_signatures.add(json.dumps(grammar, sort_keys=True, ensure_ascii=False))
self.assertGreaterEqual(len(cjk_signatures), 20)
self.assertGreaterEqual(len(extension_signatures), 20)
def test_screenshot_benchmarks_have_cover_mid_late_roles(self) -> None:
for family in load_json("beautiful-html-template-families.json")["families"]:
benchmarks = family["visual_dna"]["screenshot_benchmarks"]
roles = {item.get("role") for item in benchmarks}
with self.subTest(family=family["template_id"]):
self.assertEqual(roles, REQUIRED_BENCHMARK_ROLES)
for item in benchmarks:
self.assertRegex(item["path"], rf"beautiful-html-templates/screenshots/{family['template_id']}-\d+\.png")
self.assertIsInstance(item.get("slide_number"), int)
self.assertGreater(item["slide_number"], 0)
self.assertTrue(item.get("why_selected"))
self.assertTrue(item.get("visual_targets"))
self.assertTrue(item.get("acceptance_use"))
self.assertNotEqual(item.get("role"), "reference")
def test_cjk_policy_contains_no_remote_font_runtime_dependency(self) -> None:
registry = load_json("beautiful-html-template-families.json")
for family in registry["families"]:
cjk = family["cjk_policy"]
runtime_blob = json.dumps(
{
"runtime_font_policy": cjk.get("runtime_font_policy"),
"runtime_font_stack": cjk.get("runtime_font_stack"),
"font_role_map": family.get("font_policy", {}).get("font_role_map"),
},
ensure_ascii=False,
)
with self.subTest(family=family["template_id"]):
self.assertNotIn("fonts.googleapis.com", runtime_blob)
self.assertNotIn("@font-face", runtime_blob)
self.assertNotIn("http://", runtime_blob)
self.assertNotIn("https://", runtime_blob)
def test_source_inventoried_families_do_not_claim_absorbed(self) -> None:
for family in load_json("beautiful-html-template-families.json")["families"]:
if family["status"] == "source_inventoried":
self.assertEqual(family["claim_level"], "source_inventory_only", family["template_id"])
self.assertFalse(family.get("svglide_mapping", {}).get("svglide_asset_ids"), family["template_id"])
def test_extractor_reads_cjk_sections_from_all_design_md(self) -> None:
registry = beautiful_template_asset_extractor.extract_registry()
self.assertEqual(len(registry["families"]), 34)
for family in registry["families"]:
with self.subTest(family=family["template_id"]):
cjk = family.get("cjk_policy", {})
self.assertEqual(cjk.get("source_section_heading"), "CJK & International Content")
self.assertTrue(cjk.get("source_section_sha256"))
self.assertIn(cjk.get("mixed_run_spacing"), {"pangu_spacing", "none_required"})
self.assertIn("letter", cjk.get("letter_spacing_policy", ""))
self.assertTrue(cjk.get("known_degradation"))
def test_extractor_assigns_screenshot_benchmark_roles(self) -> None:
registry = beautiful_template_asset_extractor.extract_registry()
for family in registry["families"]:
roles = {item.get("role") for item in family["visual_dna"]["screenshot_benchmarks"]}
self.assertEqual(roles, REQUIRED_BENCHMARK_ROLES, family["template_id"])
def test_matcher_outputs_policy_summaries_without_claim_escalation(self) -> None:
result = beautiful_template_matcher.match_templates("内部业务复盘,管理层阅读,有指标、问题、原因、后续动作", limit=5)
self.assertGreaterEqual(len(result["matches"]), 3)
for match in result["matches"]:
with self.subTest(match=match["template_id"]):
self.assertIn(match.get("claim_level"), {"svglide_absorbed", "source_inventory_only"})
self.assertTrue(match.get("family_usage_policy_summary"))
self.assertTrue(match.get("cjk_policy_summary"))
self.assertTrue(match.get("extension_grammar_summary"))
self.assertEqual(set(match.get("benchmark_roles", [])), REQUIRED_BENCHMARK_ROLES)
if match.get("status") == "source_inventoried":
self.assertEqual(match["claim_level"], "source_inventory_only")
def test_prompt_context_includes_policy_summaries(self) -> None:
templates = svglide_prompt_planner.template_registry_bundle()
self.assertTrue(templates)
sample = next((item for item in templates if item.get("source_template_id")), None)
self.assertIsNotNone(sample)
for key in [
"source_template_id",
"claim_level",
"family_usage_policy_summary",
"cjk_policy_summary",
"extension_grammar_summary",
"benchmark_roles",
]:
self.assertIn(key, sample)
family_context = svglide_prompt_planner.template_family_policy_context_bundle()
self.assertEqual(len(family_context), 34)
def test_theme_template_selector_preserves_policy_fields(self) -> None:
templates = [
item
for item in svglide_theme_template_selector.load_template_registry().get("templates", [])
if isinstance(item, dict) and item.get("source_template_id")
]
self.assertTrue(templates)
scored = svglide_theme_template_selector.score_template({}, templates[0], brief="内部复盘")
for key in [
"source_template_id",
"claim_level",
"family_usage_policy_summary",
"cjk_policy_summary",
"extension_grammar_summary",
"benchmark_roles",
]:
self.assertIn(key, scored)
def test_quality_gate_blocks_m15_preflight_codes(self) -> None:
with tempfile.TemporaryDirectory() as tmpdir:
project = Path(tmpdir)
check_path = project / "06-check/preflight.json"
check_path.parent.mkdir(parents=True, exist_ok=True)
check_path.write_text(
json.dumps(
{
"summary": {"error_count": 0},
"plan": {"issues": [{"level": "error", "code": "cross_family_layout_mix"}]},
},
ensure_ascii=False,
)
+ "\n",
encoding="utf-8",
)
check = svglide_quality_gate.load_check(project, "preflight", Path("06-check/preflight.json"), required=True, profile="production")
codes = {issue["code"] for issue in check["issues"]}
self.assertIn("m15_policy_gate_failed", codes)
def test_preflight_rejects_family_usage_misuse(self) -> None:
plan = {
"output_mode": "svglide-svg",
"page_count": 1,
"template_family_selection": {
"enabled": True,
"source": "beautiful-html-template-families",
"selected_template_id": "blue-professional",
"claim_level": "svglide_absorbed",
"palette_override": {"primary": "#ff0000"},
},
"slides": [
{
"page": 1,
"title": "复盘",
"template_family_id": "studio",
"template_variant": "custom_risk_board",
"variant_source": "generated_extension",
"semantic_blocks": [{"block_id": "title_1", "type": "title", "content": "复盘"}],
"component_selection": [{"component_id": "title_block", "binds": ["title_1"]}],
"asset_strategy": {"strategy_id": "structured_fallback", "no_fake_data": True},
"asset_contract": "none_required",
"risk_flags": [],
"source_policy": "prompt only",
"content_density_contract": "medium-density structured template page",
}
],
}
codes = issue_codes(svg_preflight.lint_plan(plan))
self.assertIn("cross_family_layout_mix", codes)
self.assertIn("missing_extension_grammar", codes)
self.assertIn("family_recolor_without_override", codes)
def test_preflight_rejects_source_inventoried_claim_escalation(self) -> None:
plan = {
"output_mode": "svglide-svg",
"page_count": 1,
"template_family_selection": {
"enabled": True,
"source": "beautiful-html-template-families",
"selected_template_id": "8-bit-orbit",
"claim_level": "svglide_absorbed",
},
"slides": [
{
"page": 1,
"title": "Demo",
"template_variant": "cover",
"semantic_blocks": [{"block_id": "title_1", "type": "title", "content": "Demo"}],
"component_selection": [{"component_id": "title_block", "binds": ["title_1"]}],
"asset_strategy": {"strategy_id": "structured_fallback", "no_fake_data": True},
"asset_contract": "none_required",
"risk_flags": [],
"source_policy": "prompt only",
"content_density_contract": "medium-density structured template page",
}
],
}
self.assertIn("source_inventoried_claim_escalation", issue_codes(svg_preflight.lint_plan(plan)))
def test_preflight_rejects_cjk_fake_italic_and_letter_spacing(self) -> None:
svg = """<svg xmlns="http://www.w3.org/2000/svg" xmlns:slide="https://slides.bytedance.com/ns" slide:role="slide" slide:contract-version="svglide-authoring-contract/v1" width="960" height="540" viewBox="0 0 960 540">
<rect slide:role="shape" x="0" y="0" width="960" height="540" fill="#fff" />
<foreignObject slide:role="shape" slide:shape-type="text" x="80" y="80" width="600" height="120">
<div xmlns="http://www.w3.org/1999/xhtml" style="font-size:32px;font-style:italic;letter-spacing:4px;color:#111;">内部复盘 AI 产品 2026</div>
</foreignObject>
</svg>"""
codes = issue_codes(svg_preflight.lint_svg(svg))
self.assertIn("cjk_fake_italic", codes)
self.assertIn("cjk_letter_spacing_inherited", codes)
def test_dry_run_receipt_records_policy_hashes_and_preflight_checks(self) -> None:
with tempfile.TemporaryDirectory() as tmpdir:
summary = beautiful_template_e2e_dry_run.run_all(Path(tmpdir))
self.assertEqual(summary["status"], "passed")
for receipt in summary["receipts"]:
with self.subTest(case=receipt["case_id"]):
self.assertTrue(receipt.get("selected_template_family"))
self.assertTrue(receipt.get("claim_level"))
self.assertRegex(receipt.get("cjk_policy_hash", ""), r"^sha256:[0-9a-f]{64}$")
self.assertRegex(receipt.get("family_usage_policy_hash", ""), r"^sha256:[0-9a-f]{64}$")
self.assertRegex(receipt.get("extension_grammar_hash", ""), r"^sha256:[0-9a-f]{64}$")
self.assertEqual(set(receipt.get("benchmark_roles", [])), REQUIRED_BENCHMARK_ROLES)
checks = receipt.get("preflight_policy_checks", {})
for code in [
"cross_family_layout_mix",
"cjk_fake_italic",
"cjk_letter_spacing_inherited",
"family_recolor_without_override",
]:
self.assertEqual(checks.get(code), "passed")
if __name__ == "__main__":
unittest.main()

View File

@@ -19,7 +19,7 @@ COMPONENT_REGISTRY_PATH = REFERENCES_DIR / "component-registry.json"
ASSET_STRATEGY_PATH = REFERENCES_DIR / "asset-strategy-registry.json"
INTERNAL_REVIEW_RE = re.compile(r"复盘|经营|季度|管理层|指标|问题|原因|后续|review|business", re.IGNORECASE)
INTERNAL_REVIEW_RE = re.compile(r"复盘|经营|管理层|指标|问题|原因|后续|review|business", re.IGNORECASE)
CULTURE_EVENT_RE = re.compile(r"艺术|展|活动|海报|文化|青年|poster|exhibition|biennale", re.IGNORECASE)
COMPANY_PRODUCT_RE = re.compile(r"公司|产品|竞品|MiniMax|智谱|brand|company|product|logo|screenshot", re.IGNORECASE)
QUANTIFIED_RE = re.compile(r"\d+|同比|环比|增长|下降|占比|排名|trend|share|%|KPI", re.IGNORECASE)
@@ -80,6 +80,42 @@ def normalize_text(value: Any) -> str:
return str(value or "")
def policy_summary(value: Any, keys: list[str]) -> dict[str, Any]:
if not isinstance(value, dict):
return {}
return {key: value.get(key) for key in keys if value.get(key) not in (None, "", [])}
def cjk_policy_summary(family: dict[str, Any]) -> dict[str, Any]:
return policy_summary(
family.get("cjk_policy"),
["strategy", "display_font_cn", "body_font_cn", "runtime_font_policy", "italic_policy", "letter_spacing_policy", "mixed_run_spacing"],
)
def family_usage_policy_summary(family: dict[str, Any]) -> dict[str, Any]:
return policy_summary(
family.get("family_usage_policy"),
["closed_visual_system", "cross_family_layout_mix_allowed", "recolor_allowed", "font_substitution_allowed", "decorative_elements_policy"],
)
def extension_grammar_summary(family: dict[str, Any]) -> dict[str, Any]:
grammar = family.get("extension_grammar") if isinstance(family.get("extension_grammar"), dict) else {}
return {
"layout_rhythm": grammar.get("layout_rhythm"),
"component_grammar": grammar.get("component_grammar", [])[:6] if isinstance(grammar.get("component_grammar"), list) else grammar.get("component_grammar"),
"decorative_vocabulary": grammar.get("decorative_vocabulary", [])[:6] if isinstance(grammar.get("decorative_vocabulary"), list) else grammar.get("decorative_vocabulary"),
"forbidden_mutations": grammar.get("forbidden_mutations", [])[:6] if isinstance(grammar.get("forbidden_mutations"), list) else grammar.get("forbidden_mutations"),
}
def benchmark_roles(family: dict[str, Any]) -> list[str]:
visual_dna = family.get("visual_dna") if isinstance(family.get("visual_dna"), dict) else {}
benchmarks = visual_dna.get("screenshot_benchmarks") if isinstance(visual_dna.get("screenshot_benchmarks"), list) else []
return [str(item.get("role")) for item in benchmarks if isinstance(item, dict) and item.get("role")]
def query_signals(query: str) -> dict[str, Any]:
needs: list[str] = []
tones: list[str] = []
@@ -148,6 +184,9 @@ def family_score(query: str, signals: dict[str, Any], family: dict[str, Any]) ->
if pattern.search(query) and template_id in boosts:
score += boosts[template_id]
reasons.append(reason)
if family.get("claim_level") == "source_inventory_only":
score -= 0.03
reasons.append("source inventory only; requires contract compile before absorbed claim")
avoid_text = normalize_text(semantic_fit.get("avoid_when"))
if avoid_text and token_overlap_score(query, avoid_text) > 0:
score -= 0.3
@@ -297,11 +336,17 @@ def match_templates(query: str, limit: int = 3, page_count: int | None = None, r
matches.append(
{
"template_id": family.get("template_id"),
"status": family.get("status"),
"claim_level": family.get("claim_level"),
"score": round(score, 4),
"reasons": reasons,
"recommended_variants": variants,
"component_hints": family.get("component_candidates", [])[:6],
"asset_strategy_hints": ["chart_when_quantified", "real_image_required", "structured_fallback"],
"family_usage_policy_summary": family_usage_policy_summary(family),
"cjk_policy_summary": cjk_policy_summary(family),
"extension_grammar_summary": extension_grammar_summary(family),
"benchmark_roles": benchmark_roles(family),
}
)
return {"query_signals": signals, "matches": matches}
@@ -309,8 +354,9 @@ def match_templates(query: str, limit: int = 3, page_count: int | None = None, r
def plan_with_template_family(query: str, page_count: int = 10) -> dict[str, Any]:
result = match_templates(query, limit=3, page_count=page_count)
selected = result["matches"][0]["template_id"]
variants = result["matches"][0]["recommended_variants"]
selected_match = result["matches"][0]
selected = selected_match["template_id"]
variants = selected_match["recommended_variants"]
if not variants:
variants = ["cover", "agenda", "context_overview", "comparison", "action_plan", "closing"]
slides = []
@@ -320,6 +366,7 @@ def plan_with_template_family(query: str, page_count: int = 10) -> dict[str, Any
slides.append(
{
"page": index + 1,
"template_family_id": selected,
"template_variant": variant,
"semantic_blocks": blocks,
"component_selection": select_components(blocks),
@@ -334,7 +381,12 @@ def plan_with_template_family(query: str, page_count: int = 10) -> dict[str, Any
"source": "beautiful-html-template-families",
"selected_template_id": selected,
"candidate_template_ids": [item["template_id"] for item in result["matches"]],
"selection_reason": "; ".join(result["matches"][0]["reasons"]),
"selection_reason": "; ".join(selected_match["reasons"]),
"claim_level": selected_match.get("claim_level"),
"family_usage_policy_summary": selected_match.get("family_usage_policy_summary"),
"cjk_policy_summary": selected_match.get("cjk_policy_summary"),
"extension_grammar_summary": selected_match.get("extension_grammar_summary"),
"benchmark_roles": selected_match.get("benchmark_roles"),
},
"slides": slides,
}

View File

@@ -121,6 +121,62 @@ def normalized_formality(value: Any) -> str:
return raw if raw in FORMALITY_VALUES else "medium"
def policy_summary(value: Any, keys: list[str]) -> dict[str, Any]:
if not isinstance(value, dict):
return {}
return {key: value.get(key) for key in keys if value.get(key) not in (None, "", [])}
def family_usage_policy_summary(family: dict[str, Any]) -> dict[str, Any]:
return policy_summary(
family.get("family_usage_policy"),
["closed_visual_system", "cross_family_layout_mix_allowed", "recolor_allowed", "font_substitution_allowed", "decorative_elements_policy"],
)
def cjk_policy_summary(family: dict[str, Any]) -> dict[str, Any]:
return policy_summary(
family.get("cjk_policy"),
["strategy", "display_font_cn", "body_font_cn", "runtime_font_policy", "italic_policy", "letter_spacing_policy", "mixed_run_spacing"],
)
def extension_grammar_summary(family: dict[str, Any]) -> dict[str, Any]:
grammar = family.get("extension_grammar") if isinstance(family.get("extension_grammar"), dict) else {}
return {
"layout_rhythm": grammar.get("layout_rhythm"),
"component_grammar": grammar.get("component_grammar", [])[:6] if isinstance(grammar.get("component_grammar"), list) else grammar.get("component_grammar"),
"decorative_vocabulary": grammar.get("decorative_vocabulary", [])[:6] if isinstance(grammar.get("decorative_vocabulary"), list) else grammar.get("decorative_vocabulary"),
"forbidden_mutations": grammar.get("forbidden_mutations", [])[:6] if isinstance(grammar.get("forbidden_mutations"), list) else grammar.get("forbidden_mutations"),
}
def benchmark_roles(family: dict[str, Any]) -> list[str]:
visual_dna = family.get("visual_dna") if isinstance(family.get("visual_dna"), dict) else {}
benchmarks = visual_dna.get("screenshot_benchmarks") if isinstance(visual_dna.get("screenshot_benchmarks"), list) else []
return [str(item.get("role")) for item in benchmarks if isinstance(item, dict) and item.get("role")]
def family_policy_context(limit: int | None = None) -> list[dict[str, Any]]:
records: list[dict[str, Any]] = []
for family in families()[: limit or None]:
template_id = family.get("template_id")
if not isinstance(template_id, str) or not template_id:
continue
records.append(
{
"source_template_id": template_id,
"status": family.get("status"),
"claim_level": family.get("claim_level"),
"family_usage_policy_summary": family_usage_policy_summary(family),
"cjk_policy_summary": cjk_policy_summary(family),
"extension_grammar_summary": extension_grammar_summary(family),
"benchmark_roles": benchmark_roles(family),
}
)
return records
def template_registry() -> dict[str, Any]:
theme_ids = all_theme_ids()
records: list[dict[str, Any]] = []
@@ -163,6 +219,17 @@ def template_registry() -> dict[str, Any]:
"decorative_elements": visual_dna.get("decorative_motifs") or visual_dna.get("motifs") or [],
},
}
if family:
record.update(
{
"source_template_id": family.get("template_id"),
"claim_level": family.get("claim_level"),
"family_usage_policy_summary": family_usage_policy_summary(family),
"cjk_policy_summary": cjk_policy_summary(family),
"extension_grammar_summary": extension_grammar_summary(family),
"benchmark_roles": benchmark_roles(family),
}
)
record.update(TEMPLATE_OVERRIDES.get(template_id, {}))
records.append(record)
return {"version": "svglide-template-registry/generated-from-beautiful-family-v1", "templates": records}

View File

@@ -38,9 +38,13 @@ PATH_NUMBER_RE = re.compile(r"[-+]?(?:\d+\.?\d*|\.\d+)(?:[eE][-+]?\d+)?")
BASE64URL_RE = re.compile(r"^[A-Za-z0-9_-]+$")
SHA256_HASH_RE = re.compile(r"^sha256:[0-9a-fA-F]{64}$")
FONT_SHORTHAND_RE = re.compile(r"(^|;)\s*font\s*:", re.IGNORECASE)
FONT_STYLE_ITALIC_RE = re.compile(r"font-style\s*:\s*(italic|oblique)\b", re.IGNORECASE)
LETTER_SPACING_RE = re.compile(r"letter-spacing\s*:\s*([^;]+)", re.IGNORECASE)
REMOTE_FONT_DEPENDENCY_RE = re.compile(r"(@font-face|fonts\.googleapis\.com|fonts\.gstatic\.com|https?://)", re.IGNORECASE)
STYLE_IMAGE_OPACITY_RE = re.compile(r"(^|;)\s*opacity\s*:", re.IGNORECASE)
STYLE_STROKE_WIDTH_RE = re.compile(r"(^|;)\s*stroke-width\s*:", re.IGNORECASE)
STYLE_STROKE_DASHARRAY_RE = re.compile(r"(^|;)\s*stroke-dasharray\s*:", re.IGNORECASE)
CJK_TEXT_RE = re.compile(r"[\u3400-\u9fff]")
RGB_RE = re.compile(r"rgba?\(([^)]+)\)", re.IGNORECASE)
FONT_SIZE_RE = re.compile(r"font-size\s*:\s*([0-9.]+)px?", re.IGNORECASE)
KEY_PATH_RE = re.compile(r"(critical|flow|journey|loop|main|path|rail|route|spine|timeline)", re.IGNORECASE)
@@ -1437,6 +1441,18 @@ def validate_styles(root: ET.Element) -> list[dict[str, Any]]:
issues: list[dict[str, Any]] = []
for element in root.iter():
style = get_attr(element, "style") or ""
text = "".join(element.itertext())
has_cjk = bool(CJK_TEXT_RE.search(text))
if style and REMOTE_FONT_DEPENDENCY_RE.search(style):
issues.append(
issue(
"error",
"remote_font_dependency",
"SVG style must not depend on remote font loading",
element,
"Use SVGlide system font roles; keep Google/webfont names only as design intent metadata.",
)
)
if FONT_SHORTHAND_RE.search(style):
issues.append(
issue(
@@ -1447,6 +1463,33 @@ def validate_styles(root: ET.Element) -> list[dict[str, Any]]:
"Use explicit font-size, font-weight, font-family, color, line-height, and text-align properties.",
)
)
font_style = textify(get_attr(element, "font-style") or "")
if has_cjk and (FONT_STYLE_ITALIC_RE.search(style) or normalize_name(font_style) in {"italic", "oblique"}):
issues.append(
issue(
"error",
"cjk_fake_italic",
"CJK text must not use fake italic or oblique styling",
element,
"Use color, weight, underline, or a family-specific emphasis treatment instead of synthetic CJK italic.",
)
)
letter_spacing = get_attr(element, "letter-spacing")
match = LETTER_SPACING_RE.search(style)
if match:
letter_spacing = match.group(1)
if has_cjk and letter_spacing and normalize_name(letter_spacing) not in {"0", "0px", "normal", "unset", "initial"}:
parsed = parse_number(letter_spacing)
if parsed is None or abs(parsed) > 0.01:
issues.append(
issue(
"error",
"cjk_letter_spacing_inherited",
"CJK text must reset inherited Latin tracking/letter-spacing",
element,
"Set letter-spacing:0 for CJK runs; keep tracking only on Latin labels when explicitly needed.",
)
)
name = local_name(element.tag)
if name in {"circle", "ellipse"} and (get_attr(element, "stroke-width") is not None or STYLE_STROKE_WIDTH_RE.search(style)):
issues.append(
@@ -1751,6 +1794,180 @@ def is_template_family_route_plan(plan: dict[str, Any]) -> bool:
return isinstance(selection, dict) and boolish(selection.get("enabled"), default=False)
def beautiful_family_by_id() -> dict[str, dict[str, Any]]:
return {textify(family.get("template_id")).strip(): family for family in beautiful_template_runtime.families() if textify(family.get("template_id")).strip()}
def selected_template_family_id(plan: dict[str, Any]) -> str:
selection = nested_dict(plan.get("template_family_selection"))
return textify(selection.get("selected_template_id") or selection.get("template_id") or selection.get("family_id")).strip()
def selected_template_family(plan: dict[str, Any]) -> dict[str, Any] | None:
selected = selected_template_family_id(plan)
if not selected:
return None
families_by_id = beautiful_family_by_id()
family = families_by_id.get(selected)
if family:
return family
normalized = normalize_name(selected)
for template_id, candidate in families_by_id.items():
if normalize_name(template_id) == normalized:
return candidate
return None
def slide_template_family_id(slide: dict[str, Any]) -> str:
for key in ["template_family_id", "family_id", "template_source_id", "source_template_id"]:
value = textify(slide.get(key)).strip()
if value:
return value
selection = nested_dict(slide.get("template_family_selection"))
return textify(selection.get("selected_template_id") or selection.get("template_id") or selection.get("family_id")).strip()
def family_variant_ids(family: dict[str, Any]) -> set[str]:
variants = family.get("variants") if isinstance(family.get("variants"), list) else []
return {textify(item.get("variant_id")).strip() for item in variants if isinstance(item, dict) and textify(item.get("variant_id")).strip()}
def slide_requires_extension_grammar(slide: dict[str, Any], family: dict[str, Any]) -> bool:
variant = textify(slide.get("template_variant")).strip()
variant_source = normalize_name(slide.get("variant_source") or slide.get("layout_source"))
if variant_source in {"generated_extension", "custom", "from_scratch", "manual_extension", "derived_extension"}:
return True
return variant.startswith("custom_")
def slide_has_extension_grammar_ref(slide: dict[str, Any]) -> bool:
for key in ["extension_grammar_ref", "extension_grammar_applied", "extension_grammar", "template_extension_grammar"]:
value = slide.get(key)
if isinstance(value, bool):
if value:
return True
elif textify(value).strip():
return True
return False
def selection_requests_recolor(plan: dict[str, Any]) -> bool:
selection = nested_dict(plan.get("template_family_selection"))
for key in ["palette_override", "recolor_override", "color_override", "theme_override"]:
value = selection.get(key) or plan.get(key)
if value not in (None, "", {}, []):
return True
return False
def selection_allows_recolor(plan: dict[str, Any]) -> bool:
selection = nested_dict(plan.get("template_family_selection"))
for source in [selection, plan]:
if boolish(source.get("allow_family_recolor") or source.get("brand_palette_override") or source.get("brand_override"), default=False):
return True
reason = textify(source.get("brand_override_reason") or source.get("recolor_reason")).strip()
if reason:
return True
return False
def validate_template_family_policy(plan: dict[str, Any]) -> list[dict[str, Any]]:
issues: list[dict[str, Any]] = []
family = selected_template_family(plan)
selection = nested_dict(plan.get("template_family_selection"))
selected = selected_template_family_id(plan)
if not family:
return issues
if family.get("claim_level") == "source_inventory_only" and selection.get("claim_level") == "svglide_absorbed":
issues.append(
plan_issue(
"error",
"source_inventoried_claim_escalation",
f'template family "{selected}" is source_inventory_only but planner claims svglide_absorbed',
None,
"Keep claim_level=source_inventory_only until the family has real SVGlide absorption provenance.",
)
)
usage = family.get("family_usage_policy") if isinstance(family.get("family_usage_policy"), dict) else {}
if selection_requests_recolor(plan) and not selection_allows_recolor(plan) and usage.get("recolor_allowed") is False:
issues.append(
plan_issue(
"error",
"family_recolor_without_override",
f'template family "{selected}" forbids default recolor without explicit brand override',
None,
"Remove the palette/recolor override or mark an explicit brand override with a reason.",
)
)
cjk_policy = family.get("cjk_policy") if isinstance(family.get("cjk_policy"), dict) else {}
if not cjk_policy.get("mixed_run_spacing"):
issues.append(
plan_issue(
"error",
"cjk_mixed_run_spacing_missing",
f'template family "{selected}" does not declare mixed-run CJK/Latin spacing policy',
None,
"Extract mixed_run_spacing from design.md before using the family.",
)
)
if cjk_policy.get("runtime_font_policy") not in {"system_font_only_no_remote_dependency", None}:
issues.append(
plan_issue(
"error",
"remote_font_dependency",
f'template family "{selected}" declares a runtime font dependency that SVGlide cannot rely on',
None,
"Use system CJK font roles in SVGlide runtime; keep webfont names as design intent only.",
)
)
visual_dna = family.get("visual_dna") if isinstance(family.get("visual_dna"), dict) else {}
benchmarks = visual_dna.get("screenshot_benchmarks") if isinstance(visual_dna.get("screenshot_benchmarks"), list) else []
roles = {textify(item.get("role")) for item in benchmarks if isinstance(item, dict)}
required_roles = {"cover_reference", "mid_deck_reference", "late_deck_reference"}
if roles != required_roles:
issues.append(
plan_issue(
"error",
"missing_screenshot_benchmark_role",
f'template family "{selected}" screenshot_benchmarks must include cover/mid/late roles',
None,
"Use README gallery order and real screenshot paths; do not invent benchmark roles.",
)
)
slides = plan.get("slides") if isinstance(plan.get("slides"), list) else []
for slide in slides:
if not isinstance(slide, dict):
continue
visual_plan = slide_visual_plan(slide)
slide_family_id = slide_template_family_id(visual_plan)
if slide_family_id and slide_family_id != selected and normalize_name(slide_family_id) != normalize_name(selected):
issues.append(
plan_issue(
"error",
"cross_family_layout_mix",
f'slide uses template family "{slide_family_id}" while deck selected "{selected}"',
slide,
"Keep one template family per deck unless the user explicitly starts a new deck.",
)
)
if slide_requires_extension_grammar(visual_plan, family) and not slide_has_extension_grammar_ref(visual_plan):
issues.append(
plan_issue(
"error",
"missing_extension_grammar",
"generated or custom template_family slide must cite the selected family's extension_grammar",
slide,
"Attach extension_grammar_ref/extension_grammar_applied when extending missing layouts.",
)
)
return issues
def slide_visual_plan(slide: dict[str, Any]) -> dict[str, Any]:
visual_plan = slide.get("visual_plan")
if isinstance(visual_plan, dict):
@@ -2460,6 +2677,7 @@ def lint_plan(plan: dict[str, Any], path: str = "<plan>") -> dict[str, Any]:
"Use one of: " + ", ".join(sorted(family_ids)),
)
)
issues.extend(validate_template_family_policy(plan))
deck_preset_id = deck_style_preset_id(plan)
deck_style_system = style_system(plan)
asset_contract_lookup = asset_contracts_by_id(plan)

View File

@@ -178,11 +178,21 @@ def template_registry_bundle() -> list[dict[str, Any]]:
"max_items": item.get("max_items"),
"text_budget": item.get("text_budget"),
"supported_theme_ids": item.get("supported_theme_ids"),
"source_template_id": item.get("source_template_id"),
"claim_level": item.get("claim_level"),
"family_usage_policy_summary": item.get("family_usage_policy_summary"),
"cjk_policy_summary": item.get("cjk_policy_summary"),
"extension_grammar_summary": item.get("extension_grammar_summary"),
"benchmark_roles": item.get("benchmark_roles"),
}
)
return result
def template_family_policy_context_bundle() -> list[dict[str, Any]]:
return beautiful_template_runtime.family_policy_context()
def load_selection_context(project: Path | None = None) -> dict[str, Any]:
if project is None:
return {}
@@ -204,6 +214,7 @@ def load_selection_context(project: Path | None = None) -> dict[str, Any]:
def load_context(project: Path | None = None) -> dict[str, Any]:
context = {
"templates": template_registry_bundle(),
"template_family_policy_context": template_family_policy_context_bundle(),
"themes": theme_registry_bundle(),
"layout_archetypes": read_json(repo_path("skills/lark-slides/references/svglide-layout-archetypes.json")),
"component_registry": beautiful_template_runtime.component_registry(),

View File

@@ -66,6 +66,17 @@ BLOCKED_ASSET_SOURCE_REFS = {"local-generated-preview-asset"}
BLOCKED_ASSET_KINDS = {"generated_image", "ai_image"}
BLOCKED_ASSET_LICENSES = {"preview_unverified"}
INTERNAL_ASSET_SCHEMES = {"internal"}
M15_POLICY_CODES = {
"cross_family_layout_mix",
"missing_extension_grammar",
"remote_font_dependency",
"cjk_fake_italic",
"cjk_letter_spacing_inherited",
"cjk_mixed_run_spacing_missing",
"family_recolor_without_override",
"source_inventoried_claim_escalation",
"missing_screenshot_benchmark_role",
}
ASSET_METADATA_KEYS = {
"asset_id",
"asset_kind",
@@ -272,6 +283,20 @@ def error_count_from_payload(payload: Any) -> int | None:
return raw
def collect_issue_codes(payload: Any) -> set[str]:
codes: set[str] = set()
if isinstance(payload, dict):
code = payload.get("code")
if isinstance(code, str):
codes.add(code)
for value in payload.values():
codes.update(collect_issue_codes(value))
elif isinstance(payload, list):
for item in payload:
codes.update(collect_issue_codes(item))
return codes
def list_waivers(payload: Any) -> list[Any]:
if not isinstance(payload, dict):
return []
@@ -900,6 +925,11 @@ def load_check(project: Path, name: str, rel: Path, *, required: bool, profile:
check["error_count"] = error_count
check["action"] = action
check["waivers"] = waivers
if name == "preflight":
m15_codes = sorted(M15_POLICY_CODES & collect_issue_codes(payload))
if m15_codes:
check["issues"].append(issue("m15_policy_gate_failed", "preflight contains M15 policy blocker(s): " + ", ".join(m15_codes)))
return check
if error_count > 0:
check["issues"].append(issue("check_has_errors", f"summary.error_count is {error_count}"))
return check

View File

@@ -282,6 +282,16 @@ def score_template(signals: dict[str, Any], template: dict[str, Any], *, brief:
scored["rejection_reasons"].append("template_mismatch:summary_not_process")
scored["score"] = score
scored["template_id"] = template_id
for key in [
"source_template_id",
"claim_level",
"family_usage_policy_summary",
"cjk_policy_summary",
"extension_grammar_summary",
"benchmark_roles",
]:
if template.get(key) not in (None, "", [], {}):
scored[key] = template.get(key)
scored["selection_reason"] = list(scored.get("matched_signals", []))[:6]
return scored