diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 056c5f3..26fb183 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,5 +17,5 @@ jobs: with: python-version: "3.12" - run: python -m unittest discover -s tests -v - - run: python skills/beautify-readme/scripts/validate_skill.py - - run: python skills/beautify-readme/scripts/check_readme.py . + - run: python skills/refine-readme/scripts/validate_skill.py + - run: python skills/refine-readme/scripts/check_readme.py . diff --git a/README.md b/README.md index 89cb06c..9bcd029 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,8 @@

Agent Skill · Quick start · - Cover styles · + Directions · + Before / After · What it delivers

@@ -51,21 +52,35 @@ Or run the deterministic helpers directly: ```bash python skills/refine-readme/scripts/inspect_repository.py /path/to/repository +python skills/refine-readme/scripts/plan_directions.py /path/to/repository python skills/refine-readme/scripts/check_readme.py /path/to/repository python skills/refine-readme/scripts/render_cover.py \ --style protocol-grid \ --title "My Project" \ --tagline "One clear promise backed by real proof" \ + --eyebrow "ASYNC VIDEO API" \ + --proof-label "POST /v1/tasks" \ --output assets/readme/cover.svg ``` -## Six project-native cover directions +## From repository evidence to three directions + +Before creating a cover, the Skill resolves five things: audience, one-sentence +value, primary proof, first successful action, and native visual material. It +then proposes three directions that each identify a repository-specific motif, +proof source, construction mode, hero composition, and risk. + +The bundled styles below are direction seeds, not fixed templates. Their palette, +composition, and proof slots are adapted to the project. If removing the project +name would make the result fit an unrelated repository, the direction fails. + +## Six cover direction seeds

Six README Refiner cover directions: Protocol Grid, Product Proof, Research Field, Ink Archive, Modular Build, and Integration Bridge

-| Style | Best for | Visual language | +| Seed | Best for | Visual language | | --- | --- | --- | | Protocol Grid | APIs, SDKs, CLIs, infrastructure | Terminal rhythm, request/response blocks, grids, system paths | | Product Proof | SaaS, web apps, AI tools | Real screenshots or outputs framed by precise SVG typography | @@ -77,6 +92,25 @@ python skills/refine-readme/scripts/render_cover.py \ Generated imagery never owns exact project text. When a style needs an organic subject, image generation creates only the subject or background; deterministic SVG overlays the project name, commands, labels, and factual claims. +User covers contain no README Refiner watermark or branding by default. + +## Real Before / After + +

+ Awesome 3D Prompts README before and after: a text-only 29-case table becomes a visual 300-plus-case gallery with a hero, workflow navigation, result media, and evidence labels +

+ +The public [`BeatAPI/awesome-3d-prompts`](https://github.com/BeatAPI/awesome-3d-prompts) +history provides a durable comparison: the +[`a591c0f` snapshot](https://github.com/BeatAPI/awesome-3d-prompts/blob/a591c0ffee88fb5d529f4da0931465ce37980a25/README.md) +is an 80-line, text-first catalog with 29 accepted cases; the +[`ac37217` snapshot](https://github.com/BeatAPI/awesome-3d-prompts/blob/ac37217b7b723fbe38095503e06e3b818fbb1a85/README.md) +is a 300+ case visual gallery with a hero, workflow navigation, result media, +prompt-fidelity labels, and source attribution. + +[See the evidence and exact comparison](examples/awesome-3d-prompts-before-after.md). +This is a real repository-history reference for the Refiner quality bar, not a +claim that this Skill authored the historical commits. ## Modes @@ -84,7 +118,7 @@ SVG overlays the project name, commands, labels, and factual claims. | --- | --- | | `audit` | Read-only review of clarity, proof, trust, and maintenance cost | | `cover` | Recommend three directions and create cover assets only | -| `beautify` | Apply the complete five-layer workflow and produce a README diff | +| `beautify` | Apply the complete project-native workflow and produce a README diff | | `check` | Run factual and GitHub rendering checks without redesigning | No mode commits, pushes, opens a pull request, or publishes without explicit @@ -116,8 +150,9 @@ assets/readme/architecture.svg optional workflow or system explanation ## Status -This is the first public version. The core workflow, six cover presets, repository -inspector, README checker, and deterministic SVG renderer are available now. +This is the first public version. The project-native direction gate, six cover +seeds, repository inspector, direction planner, README checker, and deterministic +SVG renderer are available now. GitHub-like browser previews, additional project fixtures, and continuous README checks will be added through real repository usage. diff --git a/assets/readme/awesome-3d-prompts-before-after.svg b/assets/readme/awesome-3d-prompts-before-after.svg new file mode 100644 index 0000000..da29cb4 --- /dev/null +++ b/assets/readme/awesome-3d-prompts-before-after.svg @@ -0,0 +1,83 @@ + + Awesome 3D Prompts README before and after + A factual comparison of two public commits: an 80-line text-first catalog with 29 accepted cases becomes a visual gallery with 306 source-backed cases, a hero, workflow navigation, media previews, fidelity labels, and attribution. + + + + + + + + + + + + + + + + + + + + + + REAL REPOSITORY HISTORY · FIXED PUBLIC COMMITS + Awesome GPT-6 Astra 3D Prompts + + + + BEFORE · a591c0f + 29 accepted cases · 80 README lines + Text-first catalog + + Effect Engine Prompt Source + + + + + + + + + + + + + + No hero · no result previews in the README + + + + + AFTER · ac37217 + 306 source-backed cases · visual gallery + + + + + + + + 250 WebM + 56 WebP + 6 workflows + result media + result images + fast navigation + + Hero · previews · fidelity labels · attribution + + + + + Same repository · exact commits linked below · no invented product claims + diff --git a/examples/awesome-3d-prompts-before-after.md b/examples/awesome-3d-prompts-before-after.md new file mode 100644 index 0000000..52daf29 --- /dev/null +++ b/examples/awesome-3d-prompts-before-after.md @@ -0,0 +1,53 @@ +# Awesome 3D Prompts: real README before and after + +This comparison uses two immutable commits from the public +[`BeatAPI/awesome-3d-prompts`](https://github.com/BeatAPI/awesome-3d-prompts) +repository. It is a reference for README Refiner's quality bar, not a claim that +README Refiner authored the historical change. + +## Fixed snapshots + +- **Before:** [`a591c0ffee88fb5d529f4da0931465ce37980a25`](https://github.com/BeatAPI/awesome-3d-prompts/blob/a591c0ffee88fb5d529f4da0931465ce37980a25/README.md) +- **After:** [`ac37217b7b723fbe38095503e06e3b818fbb1a85`](https://github.com/BeatAPI/awesome-3d-prompts/blob/ac37217b7b723fbe38095503e06e3b818fbb1a85/README.md) +- **README diff:** [`a591c0f...ac37217`](https://github.com/BeatAPI/awesome-3d-prompts/compare/a591c0ffee88fb5d529f4da0931465ce37980a25...ac37217b7b723fbe38095503e06e3b818fbb1a85) + +## Evidence ledger + +| Signal | Before | After | +| --- | --- | --- | +| README length | 80 lines | 949 lines | +| Catalog size | 29 accepted cases | 306 source-backed cases | +| Opening visual | None | Repository-owned hero image | +| Result proof | Text tables link to detail pages | Featured entries show visible result media in the README | +| Navigation | Category headings | Six workflow links with counts | +| Trust cues | Source and rights explanation | Source attribution, prompt-fidelity labels, evidence notes, and rights links | +| Media summary | No media summary | 250 WebM videos and 56 WebP images | + +Counts above are read from the two committed READMEs and their committed prompt +data. The comparison does not change or reinterpret the source prompts. + +## What changed in the reading experience + +The earlier README is accurate, but a visitor must read tables and open detail +pages before seeing why the collection is useful. The later README leads with a +clear promise, visible output, workflow-level navigation, and evidence labels. +The underlying source discipline remains intact; the presentation makes that +discipline easier to understand and trust. + +This is the pattern README Refiner should reproduce on other repositories: + +1. preserve repository truth; +2. move the strongest real proof forward; +3. establish a project-native visual system; +4. improve scanning without hiding detail; +5. keep every important claim traceable to source. + +## Reproduce the comparison + +From a clone of the 3D prompt repository: + +```bash +git show a591c0ffee88fb5d529f4da0931465ce37980a25:README.md +git show ac37217b7b723fbe38095503e06e3b818fbb1a85:README.md +git diff a591c0ffee88fb5d529f4da0931465ce37980a25..ac37217b7b723fbe38095503e06e3b818fbb1a85 -- README.md +``` diff --git a/skills/refine-readme/SKILL.md b/skills/refine-readme/SKILL.md index 07813f0..5aa851b 100644 --- a/skills/refine-readme/SKILL.md +++ b/skills/refine-readme/SKILL.md @@ -16,14 +16,14 @@ wants `audit`, `cover`, `beautify`, or `check`. - `audit`: inspect and report only; do not edit files. - `cover`: create visual assets only; do not edit README content or references. -- `beautify`: execute the complete five-layer workflow and propose a README diff. +- `beautify`: execute the complete project-native workflow and propose a README diff. - `check`: validate facts and GitHub rendering without redesigning. Reading repository files does not grant permission to edit, commit, push, open a pull request, or publish. Always preview first. Require separate explicit approval for commit, push, PR, and publication actions. -## Run the five-layer workflow +## Run the project-native workflow 1. Establish repository truth. - Inspect the current README, manifests, package scripts, public routes, @@ -33,7 +33,19 @@ approval for commit, push, PR, and publication actions. - Build a fact ledger that separates confirmed evidence, reasonable inference, user-supplied claims, and unsupported claims. -2. Rebuild the story and Markdown hierarchy. +2. Pass the project-native direction gate. + - Resolve the audience, one-sentence value, primary proof, first successful + action, and native visual material before selecting a style. + - Run `scripts/plan_directions.py ` for three evidence-led + candidates, then sharpen them with repository-specific reasoning. + - If the planner reports a blocked gate, request repository evidence instead + of returning arbitrary zero-evidence styles. + - For every direction, explain why it fits, its project-native motif, its + real proof, construction mode, hero composition, and primary risk. + - Treat bundled styles as seeds and constraints, not immutable templates. + - Read `references/project-native-directions.md` before recommending styles. + +3. Rebuild the story and Markdown hierarchy. - Make the first screen answer what the project is, who it is for, what proof exists, and how to try it. - Move real screenshots, outputs, examples, or a minimal command ahead of @@ -42,20 +54,21 @@ approval for commit, push, PR, and publication actions. precise and scannable. - Read `references/five-layer-workflow.md` for the default section logic. -3. Create the cover and visual identity. +4. Create the cover and visual identity. - In `beautify` mode, create a cover unless the user explicitly opts out. - Default to `1200x400` SVG. Use `1200x320` for compact technical projects and `1200x480` for proof-rich showcases. - - When no style is selected, recommend exactly three suitable presets from - `references/style-catalog.md`, explain each in one sentence, and ask the - user to choose. Auto-select only when the user says to decide automatically. - - Use exactly one style preset. Do not blend multiple visual systems. + - When no direction is selected, recommend exactly three evidence-led + directions and ask the user to choose. Auto-select only when the user says + to decide automatically. + - Choose one coherent direction. Adapt one style seed to the project instead + of applying a fixed template or mixing unrelated decorative traits. - Use `scripts/render_cover.py` for deterministic exact-text SVG covers. - If generated imagery is needed, generate only the subject, texture, or background. Add project names, commands, metrics, and labels through SVG. - Read `references/cover-system.md` before creating or reviewing a cover. -4. Add proof and explanation. +5. Add proof and explanation. - Prefer real screenshots, outputs, input/output comparisons, terminal captures, or diagrams over decorative images. - Use SVG for exact diagrams and coordinated section transitions; use @@ -63,7 +76,7 @@ approval for commit, push, PR, and publication actions. - Keep explanations and commands in Markdown so they remain searchable and copyable. -5. Validate and deliver. +6. Validate and deliver. - Run `scripts/check_readme.py `. - Review factual claims against the ledger and `references/fact-check.md`. - Apply the GitHub rendering rules in `references/github-rendering.md`. @@ -92,8 +105,11 @@ to the run. - The first screen communicates one concrete promise and one real proof. - The cover uses exact, readable text and survives narrow rendering. - The title, palette, motifs, and proof feel native to this repository. +- Removing the project name would not make the visual fit an unrelated project. - Claims, commands, paths, versions, ports, and public interfaces match source. - Images have meaningful alt text and do not replace essential body content. - Relative paths and heading anchors resolve. - The result remains useful with images disabled. - The user can review every change before anything is published. +- Do not place README Refiner branding in user assets by default; attribution is + optional and belongs outside the project hero. diff --git a/skills/refine-readme/references/cover-system.md b/skills/refine-readme/references/cover-system.md index bc9c69f..bbb97d9 100644 --- a/skills/refine-readme/references/cover-system.md +++ b/skills/refine-readme/references/cover-system.md @@ -22,6 +22,8 @@ strong symbolic or typographic identities. Keep all factual text in SVG source. Use real screenshots, terminal output, generated results, or prompt cards inside a deterministic SVG frame. Do not invent dashboard data or polished outputs. +Use a single-board hero only when the proof remains readable at the target +GitHub width. Otherwise make the cover simple and place proof immediately below. ### Hybrid generated subject @@ -37,6 +39,8 @@ place it under exact SVG typography. Preserve a source prompt and editable SVG. - Use no more than one short supporting line in the cover. - Avoid tiny fake UI, dense diagrams, long feature lists, and decorative badges. - Test the cover on light and dark GitHub themes and near 360 CSS pixels wide. +- Keep project attribution separate from tool attribution. User covers contain + no README Refiner branding or watermark by default. ## Required checks diff --git a/skills/refine-readme/references/project-native-directions.md b/skills/refine-readme/references/project-native-directions.md new file mode 100644 index 0000000..0e023d7 --- /dev/null +++ b/skills/refine-readme/references/project-native-directions.md @@ -0,0 +1,44 @@ +# Project-native direction gate + +Do not begin production by asking only which visual preset the user likes. Read +the repository and produce three defensible directions first. + +## Required project brief + +Resolve or explicitly mark unknown: + +1. **Audience** — who reaches this repository and what they already understand. +2. **One-sentence value** — one confirmed outcome, without slogans. +3. **Primary proof** — the strongest real output, screenshot, command, or flow. +4. **First successful action** — the shortest verified path to useful output. +5. **Native visual material** — product UI, terminal grammar, data shape, workflow, + domain object, logo, palette, or other material that genuinely belongs here. + +If the repository contains too little evidence to support a direction, stop at +the gate and request the missing proof. Do not fill all three slots with +zero-evidence presets. + +## Three-direction contract + +Each direction must state: + +- why it fits this repository; +- which native motif it uses; +- which real proof leads the design; +- whether it uses deterministic SVG, a proof composite, or a generated subject; +- whether the hero integrates proof or is followed by a separate proof board; +- its main legibility, factual, or maintenance risk. + +Style presets are seeds and constraints, not finished templates. Adapt their +palette, composition, motif, and proof slots to repository evidence. Never mix +decorative traits from several presets without one coherent art direction. + +## Decision test + +Before producing assets, remove the repository name mentally. If the direction +could fit an unrelated project without changing its motif or proof, redesign it. + +Use an integrated hero only when the proof remains legible near 900 CSS pixels. +Otherwise use a simpler title cover followed immediately by a real proof board. +At 360 CSS pixels, the title and one concrete promise must remain readable even +when the proof itself moves below the cover. diff --git a/skills/refine-readme/references/style-catalog.md b/skills/refine-readme/references/style-catalog.md index d078956..81e632f 100644 --- a/skills/refine-readme/references/style-catalog.md +++ b/skills/refine-readme/references/style-catalog.md @@ -1,7 +1,9 @@ -# Cover style catalog +# Cover direction seeds -Recommend exactly three styles when the user has not selected one. Select one -final style before generating production assets. +Recommend exactly three evidence-led directions when the user has not selected +one. These entries provide useful constraints and production primitives; they +are not six fixed templates. Adapt one seed around a repository-native motif and +real proof before generating production assets. | Style | Best for | Distinguishing anchors | | --- | --- | --- | @@ -13,3 +15,7 @@ final style before generating production assets. | `integration-bridge` | Plugins, MCP, APIs, integrations | Two recognizable endpoints, one restrained connector, derived accent colors | Full machine-readable metadata lives in `../assets/styles/presets.json`. + +Do not expose the internal seed name as user-facing branding inside the cover. +Do not add README Refiner branding, watermarks, or promotional links to project +assets unless the user explicitly requests attribution. diff --git a/skills/refine-readme/scripts/inspect_repository.py b/skills/refine-readme/scripts/inspect_repository.py index 0c301f6..fdeb5ab 100644 --- a/skills/refine-readme/scripts/inspect_repository.py +++ b/skills/refine-readme/scripts/inspect_repository.py @@ -53,6 +53,28 @@ def package_facts(path: Path) -> dict: } +def pyproject_facts(path: Path) -> dict: + text = read_text(path) + if not text: + return {"parse_error": True} + project_match = re.search(r"(?ms)^\[project\]\s*(.*?)(?=^\[|\Z)", text) + project_text = project_match.group(1) if project_match else "" + + def scalar(name: str) -> str | None: + match = re.search(rf'(?m)^{re.escape(name)}\s*=\s*["\']([^"\']+)["\']', project_text) + return match.group(1) if match else None + + scripts_match = re.search(r"(?ms)^\[project\.scripts\]\s*(.*?)(?=^\[|\Z)", text) + scripts_text = scripts_match.group(1) if scripts_match else "" + scripts = sorted(re.findall(r"(?m)^([A-Za-z0-9_.-]+)\s*=", scripts_text)) + return { + "name": scalar("name"), + "description": scalar("description"), + "requires_python": scalar("requires-python"), + "scripts": scripts, + } + + def inventory(root: Path) -> dict: readme = find_readme(root) readme_text = read_text(readme) if readme else "" @@ -65,10 +87,15 @@ def inventory(root: Path) -> dict: for name in MANIFESTS: path = root / name if path.is_file(): - manifests[name] = package_facts(path) if name == "package.json" else "present" + if name == "package.json": + manifests[name] = package_facts(path) + elif name == "pyproject.toml": + manifests[name] = pyproject_facts(path) + else: + manifests[name] = "present" assets: list[str] = [] - for base_name in ("assets", "public", "docs", "media", "screenshots"): + for base_name in ("assets", "_assets", "public", "docs", "media", "screenshots"): base = root / base_name if not base.is_dir(): continue @@ -81,7 +108,10 @@ def inventory(root: Path) -> dict: break package = manifests.get("package.json") if isinstance(manifests.get("package.json"), dict) else {} + pyproject = manifests.get("pyproject.toml") if isinstance(manifests.get("pyproject.toml"), dict) else {} project_name = package.get("name") if package else None + if not project_name and pyproject: + project_name = pyproject.get("name") if not project_name and headings: project_name = re.sub(r"[*_`]", "", headings[0]["text"]) @@ -89,6 +119,7 @@ def inventory(root: Path) -> dict: "root": str(root), "project_name": project_name or root.name, "readme": readme.name if readme else None, + "readme_excerpt": re.sub(r"\s+", " ", readme_text[:2_500]).strip(), "headings": headings, "manifests": manifests, "existing_visual_assets": sorted(assets), diff --git a/skills/refine-readme/scripts/plan_directions.py b/skills/refine-readme/scripts/plan_directions.py new file mode 100644 index 0000000..471907a --- /dev/null +++ b/skills/refine-readme/scripts/plan_directions.py @@ -0,0 +1,148 @@ +#!/usr/bin/env python3 +"""Recommend three evidence-led README directions without editing the repository.""" + +from __future__ import annotations + +import argparse +import json +import re +import sys +from pathlib import Path + + +HERE = Path(__file__).resolve().parent +sys.path.insert(0, str(HERE)) + +from inspect_repository import inventory # noqa: E402 + + +STYLE_RULES = { + "protocol-grid": { + "terms": ("api", "sdk", "cli", "endpoint", "developer", "server", "infrastructure", "async", "asynchronous", "task", "request", "response"), + "why": "The repository exposes a technical interface that can become the visual grammar.", + "motif": "one real request, its task lifecycle, and the verified output", + "proof": "a copyable command or real request/response", + }, + "product-proof": { + "terms": ("app", "dashboard", "web", "studio", "generator", "video", "image", "product", "gallery", "preview"), + "why": "The result is easier to trust when real product output leads the visual system.", + "motif": "one real product frame or output, not a generic device mockup", + "proof": "a real screenshot, generated result, or before/after", + }, + "research-field": { + "terms": ("research", "model", "dataset", "benchmark", "paper", "analysis", "knowledge", "evidence"), + "why": "The repository is better framed through its method and evidence than through decorative UI.", + "motif": "one repository-specific method, signal, or data relationship", + "proof": "a verified method, dataset, benchmark, or result", + }, + "ink-archive": { + "terms": ("database", "storage", "archive", "index", "query", "record", "catalog", "collection"), + "why": "Records, queries, or indexed material provide a native restrained visual language.", + "motif": "one real record, query, index, or catalog structure", + "proof": "a real query, record, or indexed output", + }, + "modular-build": { + "terms": ("builder", "workflow", "automation", "pipeline", "template", "tutorial", "compose"), + "why": "The project value is created through an understandable sequence of building blocks.", + "motif": "the smallest real sequence from input to completed output", + "proof": "a verified workflow, module path, or completed result", + }, + "integration-bridge": { + "terms": ("plugin", "integration", "mcp", "dify", "connector", "webhook", "provider"), + "why": "The project connects two recognizable systems through one concrete action.", + "motif": "the two real endpoints and the action that crosses between them", + "proof": "a verified integration flow or successful action", + }, +} + + +def repository_text(data: dict) -> str: + pieces = [str(data.get("project_name", "")), str(data.get("readme_excerpt", ""))] + for heading in data.get("headings", []): + pieces.append(str(heading.get("text", ""))) + for manifest in data.get("manifests", {}).values(): + if isinstance(manifest, dict): + pieces.extend(str(manifest.get(key, "")) for key in ("name", "description")) + pieces.extend(data.get("existing_visual_assets", [])) + return " ".join(pieces).lower() + + +def recommend(data: dict) -> dict: + text = repository_text(data) + tokens = set(re.findall(r"[a-z0-9]+", text)) + visual_markers = ("screenshot", "demo", "output", "result", "preview", "hero", "gallery") + has_visual_proof = any( + marker in asset.lower() + for asset in data.get("existing_visual_assets", []) + for marker in visual_markers + ) + + scored: list[tuple[int, str, list[str]]] = [] + for style, rule in STYLE_RULES.items(): + matched_terms = sorted(term for term in rule["terms"] if term in tokens) + score = len(matched_terms) * 3 + if style == "product-proof" and has_visual_proof: + score += 5 + scored.append((score, style, matched_terms)) + scored.sort(key=lambda item: (-item[0], item[1])) + + evidence_backed = [item for item in scored if item[0] > 0] + directions = [] + if evidence_backed: + for score, style, matched_terms in evidence_backed[:3]: + rule = STYLE_RULES[style] + directions.append({ + "style_seed": style, + "evidence_score": score, + "matched_terms": matched_terms, + "why_it_fits": rule["why"], + "project_native_motif": rule["motif"], + "primary_proof": rule["proof"], + "construction_mode": "proof-composite" if has_visual_proof else "deterministic-svg", + "decision_status": "candidate; verify against repository facts before production", + }) + + gate_status = "ready" if len(directions) == 3 else "blocked" + gate_reason = ( + "Repository evidence supports three candidate directions." + if gate_status == "ready" + else "Fewer than three evidence-backed directions were found. Add or identify a README, manifest description, public interface, workflow, or real visual proof before selecting a direction." + ) + return { + "repository": data.get("root"), + "project_name": data.get("project_name"), + "direction_gate": {"status": gate_status, "reason": gate_reason}, + "project_brief": { + "audience": "verify from current docs, package metadata, and user evidence", + "one_sentence_value": "derive from confirmed behavior, not the repository tagline alone", + "primary_proof": "select the strongest real output before designing the hero", + "first_successful_action": "identify the shortest verified path to a useful result", + "native_visual_material": data.get("existing_visual_assets", [])[:12], + }, + "directions": directions, + "selection_rule": "Choose the direction with the strongest repository-specific motif and proof, not the highest score alone.", + } + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("repository", nargs="?", default=".") + parser.add_argument("--output") + args = parser.parse_args() + + root = Path(args.repository).expanduser().resolve() + if not root.is_dir(): + parser.error(f"repository is not a directory: {root}") + payload = recommend(inventory(root)) + rendered = json.dumps(payload, indent=2, ensure_ascii=False) + "\n" + if args.output: + output = Path(args.output).expanduser().resolve() + output.parent.mkdir(parents=True, exist_ok=True) + output.write_text(rendered, encoding="utf-8") + else: + print(rendered, end="") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/skills/refine-readme/scripts/render_cover.py b/skills/refine-readme/scripts/render_cover.py index c7745ea..2aa0a03 100644 --- a/skills/refine-readme/scripts/render_cover.py +++ b/skills/refine-readme/scripts/render_cover.py @@ -5,6 +5,7 @@ import argparse import json +import re import textwrap from html import escape from pathlib import Path @@ -12,17 +13,33 @@ HERE = Path(__file__).resolve().parent PRESETS_PATH = HERE.parent / "assets" / "styles" / "presets.json" +SAFE_COLOR = re.compile( + r"^(?:#[0-9a-fA-F]{3,8}|[A-Za-z]+|(?:rgb|rgba|hsl|hsla|oklch)\([0-9.,% /+-]+\))$" +) def load_presets() -> dict: return json.loads(PRESETS_PATH.read_text(encoding="utf-8")) -def motif(style: str, accent: str, support: str, foreground: str) -> str: +def validate_color(value: str) -> str: + if not SAFE_COLOR.fullmatch(value): + raise ValueError("accent must be a plain CSS color value") + return value + + +def motif( + style: str, + accent: str, + support: str, + foreground: str, + proof_label: str, +) -> str: + safe_proof_label = escape(textwrap.shorten(proof_label, width=42, placeholder="…")) if style == "protocol-grid": return f''' - POST /v1/tasks + {safe_proof_label} REQUEST @@ -31,7 +48,7 @@ def motif(style: str, accent: str, support: str, foreground: str) -> str: OUTPUT - truth → design → proof → check + REQUEST → TASK → VERIFIED OUTPUT ''' if style == "product-proof": return f''' @@ -74,7 +91,15 @@ def motif(style: str, accent: str, support: str, foreground: str) -> str: ''' -def render(style_id: str, title: str, tagline: str, accent_override: str | None = None) -> str: +def render( + style_id: str, + title: str, + tagline: str, + accent_override: str | None = None, + eyebrow: str | None = None, + badge: str | None = None, + proof_label: str = "PROJECT PROOF", +) -> str: presets = load_presets() if style_id not in presets: raise KeyError(f"unknown style: {style_id}; choose from {', '.join(sorted(presets))}") @@ -82,12 +107,28 @@ def render(style_id: str, title: str, tagline: str, accent_override: str | None width, height = (int(value) for value in preset["canvas"].split("x")) background = preset["background"] foreground = preset["foreground"] - accent = accent_override or preset["accent"] + accent = validate_color(accent_override) if accent_override else preset["accent"] support = preset["support"] safe_title = escape(title) safe_tagline = escape(textwrap.shorten(tagline, width=78, placeholder="…")) - style_name = escape(preset["name"]) + safe_eyebrow = escape(textwrap.shorten(eyebrow, width=48, placeholder="…")) if eyebrow else "" + raw_badge = textwrap.shorten(badge, width=24, placeholder="…") if badge else "" + safe_badge = escape(raw_badge) font_size = 76 if len(title) <= 18 else 60 if len(title) <= 30 else 48 + eyebrow_svg = ( + f'{safe_eyebrow}' + if safe_eyebrow + else "" + ) + badge_width = min(260, max(150, 36 + len(raw_badge) * 9)) + badge_text_x = 72 + badge_width / 2 + badge_svg = ( + f''' + {safe_badge}''' + if safe_badge + else "" + ) return f''' {safe_title} @@ -104,12 +145,11 @@ def render(style_id: str, title: str, tagline: str, accent_override: str | None - README REFINER · {style_name.upper()} + {eyebrow_svg} {safe_title} {safe_tagline} - - BEAUTIFUL · TRUE - {motif(style_id, accent, support, foreground)} + {badge_svg} + {motif(style_id, accent, support, foreground, proof_label)} ''' @@ -119,6 +159,9 @@ def main() -> int: parser.add_argument("--title") parser.add_argument("--tagline", default="A clear, polished, GitHub-ready README") parser.add_argument("--accent", help="Optional CSS color override") + parser.add_argument("--eyebrow", help="Optional project-owned category or product label") + parser.add_argument("--badge", help="Optional short verified callout; omitted by default") + parser.add_argument("--proof-label", default="PROJECT PROOF", help="Short project-native label used by the motif") parser.add_argument("--output") parser.add_argument("--list-styles", action="store_true") args = parser.parse_args() @@ -130,8 +173,16 @@ def main() -> int: if not args.style or not args.title or not args.output: parser.error("--style, --title, and --output are required unless --list-styles is used") try: - svg = render(args.style, args.title, args.tagline, args.accent) - except KeyError as error: + svg = render( + args.style, + args.title, + args.tagline, + args.accent, + args.eyebrow, + args.badge, + args.proof_label, + ) + except (KeyError, ValueError) as error: parser.error(str(error)) output = Path(args.output).expanduser().resolve() output.parent.mkdir(parents=True, exist_ok=True) diff --git a/skills/refine-readme/scripts/validate_skill.py b/skills/refine-readme/scripts/validate_skill.py index 9c62067..d066aad 100644 --- a/skills/refine-readme/scripts/validate_skill.py +++ b/skills/refine-readme/scripts/validate_skill.py @@ -14,6 +14,7 @@ "agents/openai.yaml", "references/five-layer-workflow.md", "references/cover-system.md", + "references/project-native-directions.md", "references/style-catalog.md", "references/fact-check.md", "references/github-rendering.md", diff --git a/tests/test_scripts.py b/tests/test_scripts.py index 2457cdd..ef2df6d 100644 --- a/tests/test_scripts.py +++ b/tests/test_scripts.py @@ -27,6 +27,32 @@ def test_all_cover_presets_render_exact_title(self): self.assertIn("Exact Project", svg) self.assertIn('role="img"', svg) self.assertNotIn("{{", svg) + self.assertNotIn("README REFINER", svg) + self.assertNotIn("BEAUTIFUL · TRUE", svg) + + def test_cover_uses_only_explicit_project_labels(self): + renderer = load("render_cover") + svg = renderer.render( + "protocol-grid", + "BeatAPI for Dify", + "Create and monitor async video tasks", + eyebrow="DIFY PLUGIN", + badge="2 VERIFIED TOOLS", + proof_label="CREATE → POLL", + ) + self.assertIn("DIFY PLUGIN", svg) + self.assertIn("2 VERIFIED TOOLS", svg) + self.assertIn("CREATE → POLL", svg) + + def test_cover_rejects_markup_in_color_override(self): + renderer = load("render_cover") + with self.assertRaises(ValueError): + renderer.render( + "protocol-grid", + "Demo", + "Verified tagline", + accent_override='red" onload="alert(1)', + ) def test_inspector_reads_package_scripts(self): inspector = load("inspect_repository") @@ -39,6 +65,63 @@ def test_inspector_reads_package_scripts(self): result = inspector.inventory(root) self.assertEqual(result["project_name"], "demo") self.assertEqual(result["manifests"]["package.json"]["scripts"], ["test"]) + self.assertEqual(result["readme_excerpt"], "# Demo") + + def test_inspector_reads_pyproject_and_private_asset_folder(self): + inspector = load("inspect_repository") + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + (root / "README.md").write_text("# Python Demo\n", encoding="utf-8") + (root / "pyproject.toml").write_text( + '[project]\nname = "python-demo"\ndescription = "A useful CLI"\n' + 'requires-python = ">=3.11"\n\n[project.scripts]\ndemo = "demo:main"\n', + encoding="utf-8", + ) + (root / "_assets").mkdir() + (root / "_assets" / "preview.webp").write_bytes(b"RIFF") + result = inspector.inventory(root) + self.assertEqual(result["project_name"], "python-demo") + self.assertEqual(result["manifests"]["pyproject.toml"]["scripts"], ["demo"]) + self.assertEqual(result["existing_visual_assets"], ["_assets/preview.webp"]) + + def test_direction_planner_returns_three_evidence_led_candidates(self): + planner = load("plan_directions") + result = planner.recommend({ + "root": "/tmp/beatapi-dify-plugin", + "project_name": "beatapi-dify-plugin", + "headings": [{"text": "Dify plugin for an asynchronous API"}], + "manifests": {}, + "existing_visual_assets": ["assets/demo-output.webp"], + }) + self.assertEqual(result["direction_gate"]["status"], "ready") + self.assertEqual(len(result["directions"]), 3) + self.assertEqual(result["directions"][0]["style_seed"], "integration-bridge") + self.assertEqual(result["directions"][0]["construction_mode"], "proof-composite") + + def test_direction_planner_blocks_when_repository_has_no_style_evidence(self): + planner = load("plan_directions") + result = planner.recommend({ + "root": "/tmp/unknown", + "project_name": "unknown", + "headings": [], + "manifests": {}, + "existing_visual_assets": [], + }) + self.assertEqual(result["direction_gate"]["status"], "blocked") + self.assertEqual(result["directions"], []) + + def test_direction_planner_does_not_pad_with_zero_evidence_styles(self): + planner = load("plan_directions") + result = planner.recommend({ + "root": "/tmp/api-only", + "project_name": "api-only", + "headings": [], + "manifests": {}, + "existing_visual_assets": [], + }) + self.assertEqual(result["direction_gate"]["status"], "blocked") + self.assertEqual(len(result["directions"]), 1) + self.assertTrue(all(item["evidence_score"] > 0 for item in result["directions"])) def test_checker_reports_missing_local_path(self): checker = load("check_readme")