Add named client profiles (#61)
* feat: add client profiles * ci: pin shell to bash for docs_sync step The docs_sync step runs two commands in a multi-line block. Without a pinned shell, the windows-latest matrix leg uses pwsh, where a non-zero exit from the first command may not abort the block, masking a gate failure. Pin shell: bash to match the icons and summary steps.
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Regression tests for docs links and profile-surface verification."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
VERIFY = ROOT / "scripts" / "verify-docs-sync.py"
|
||||
|
||||
|
||||
def load_verify_module():
|
||||
sys.dont_write_bytecode = True
|
||||
spec = importlib.util.spec_from_file_location("diagram_design_verify_docs", VERIFY)
|
||||
if spec is None or spec.loader is None:
|
||||
raise AssertionError("could not load verify-docs-sync.py")
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
def main() -> int:
|
||||
verify = load_verify_module()
|
||||
with tempfile.TemporaryDirectory(prefix="verify-docs-sync-") as temp_dir:
|
||||
skill = Path(temp_dir)
|
||||
references = skill / "references"
|
||||
references.mkdir()
|
||||
(references / "present.md").write_text("# Present\n", encoding="utf-8")
|
||||
|
||||
errors: list[str] = []
|
||||
verify.check_skill_reference_links(
|
||||
errors,
|
||||
"See [present](references/present.md) and [section](references/present.md#part).",
|
||||
skill,
|
||||
)
|
||||
if errors:
|
||||
raise AssertionError(f"valid reference link failed: {errors}")
|
||||
|
||||
errors = []
|
||||
verify.check_skill_reference_links(
|
||||
errors,
|
||||
"See [missing](references/missing.md).",
|
||||
skill,
|
||||
)
|
||||
expected = "SKILL.md links to missing reference 'references/missing.md'"
|
||||
if errors != [expected]:
|
||||
raise AssertionError(f"broken reference was not reported: {errors}")
|
||||
|
||||
errors = []
|
||||
verify.check_skill_reference_links(
|
||||
errors,
|
||||
"See [README](../../README.md), [asset](assets/example.html), and https://example.com.",
|
||||
skill,
|
||||
)
|
||||
if errors:
|
||||
raise AssertionError(f"non-reference links should be ignored: {errors}")
|
||||
|
||||
root = Path(temp_dir) / "repo"
|
||||
profile_reference = root / "skills/diagram-design/references/profiles.md"
|
||||
command = root / "commands/profile.md"
|
||||
prompt = root / "prompts/profile.md"
|
||||
for path in (profile_reference, command, prompt):
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
profile_reference.write_text("# Profiles\n", encoding="utf-8")
|
||||
command.write_text("Follow references/profiles.md.\n", encoding="utf-8")
|
||||
prompt.write_text("Follow references/profiles.md.\n", encoding="utf-8")
|
||||
|
||||
errors = []
|
||||
verify.check_profile_surfaces(errors, root)
|
||||
if errors:
|
||||
raise AssertionError(f"valid profile surfaces failed: {errors}")
|
||||
|
||||
prompt.unlink()
|
||||
errors = []
|
||||
verify.check_profile_surfaces(errors, root)
|
||||
expected = "profile surface is missing: prompts/profile.md"
|
||||
if errors != [expected]:
|
||||
raise AssertionError(f"missing Pi prompt was not reported: {errors}")
|
||||
|
||||
prompt.write_text("Stale standalone instructions.\n", encoding="utf-8")
|
||||
errors = []
|
||||
verify.check_profile_surfaces(errors, root)
|
||||
expected = (
|
||||
"profile surface does not route to references/profiles.md: prompts/profile.md"
|
||||
)
|
||||
if errors != [expected]:
|
||||
raise AssertionError(f"stale Pi prompt was not reported: {errors}")
|
||||
|
||||
print(
|
||||
"PASS: docs sync checks reference links and Claude/Pi profile-surface parity"
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Verify that routing and browsing surfaces stay in sync with the skill.
|
||||
|
||||
Three drift classes, each of which has shipped before:
|
||||
Five drift classes, each of which has shipped before:
|
||||
|
||||
1. The SKILL.md frontmatter description is the only text an agent sees before
|
||||
deciding to load the skill — every visual type in the selection table must
|
||||
@@ -9,6 +9,8 @@ Three drift classes, each of which has shipped before:
|
||||
2. The gallery (assets/index.html) must reach every shipped example, and every
|
||||
gallery tab must point at a file that exists.
|
||||
3. Every concrete file named in README.md's architecture tree must exist.
|
||||
4. Every relative references/*.md link in SKILL.md must resolve.
|
||||
5. Claude and Pi profile surfaces must both route to the profile reference.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -29,6 +31,10 @@ DESCRIPTION_ALIASES = {
|
||||
"line chart": "line",
|
||||
"scatter plot": "scatter",
|
||||
}
|
||||
PROFILE_SURFACES = (
|
||||
Path("commands/profile.md"),
|
||||
Path("prompts/profile.md"),
|
||||
)
|
||||
|
||||
|
||||
def normalized(text: str) -> str:
|
||||
@@ -114,17 +120,57 @@ def check_readme_tree(errors: list[str]) -> None:
|
||||
errors.append(f"README architecture tree names {token!r} but no such file exists")
|
||||
|
||||
|
||||
def skill_reference_links(markdown: str) -> list[str]:
|
||||
"""Return direct relative links from SKILL.md into references/."""
|
||||
return re.findall(
|
||||
r"\]\((references/[A-Za-z0-9][A-Za-z0-9_.-]*\.md)(?:#[^)]*)?\)",
|
||||
markdown,
|
||||
)
|
||||
|
||||
|
||||
def check_skill_reference_links(
|
||||
errors: list[str], markdown: str, skill_directory: Path
|
||||
) -> None:
|
||||
for target in sorted(set(skill_reference_links(markdown))):
|
||||
if not (skill_directory / target).is_file():
|
||||
errors.append(f"SKILL.md links to missing reference {target!r}")
|
||||
|
||||
|
||||
def check_profile_surfaces(errors: list[str], root: Path) -> None:
|
||||
reference = root / "skills/diagram-design/references/profiles.md"
|
||||
if not reference.is_file():
|
||||
errors.append("profile source of truth is missing: skills/diagram-design/references/profiles.md")
|
||||
for relative in PROFILE_SURFACES:
|
||||
path = root / relative
|
||||
if not path.is_file():
|
||||
errors.append(f"profile surface is missing: {relative.as_posix()}")
|
||||
continue
|
||||
if "references/profiles.md" not in path.read_text(encoding="utf-8"):
|
||||
errors.append(
|
||||
f"profile surface does not route to references/profiles.md: {relative.as_posix()}"
|
||||
)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
errors: list[str] = []
|
||||
check_description(errors)
|
||||
check_gallery(errors)
|
||||
check_readme_tree(errors)
|
||||
check_skill_reference_links(
|
||||
errors,
|
||||
SKILL.read_text(encoding="utf-8"),
|
||||
SKILL.parent,
|
||||
)
|
||||
check_profile_surfaces(errors, ROOT)
|
||||
if errors:
|
||||
print("FAIL docs sync")
|
||||
for error in errors:
|
||||
print(f" - {error}")
|
||||
return 1
|
||||
print("OK docs sync: description hooks, gallery reachability, README tree")
|
||||
print(
|
||||
"OK docs sync: description hooks, gallery reachability, README tree, "
|
||||
"reference links, profile surfaces"
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user