mirror of
https://github.com/larksuite/cli.git
synced 2026-07-09 18:34:03 +08:00
feat: harden svg slides generation receipts
This commit is contained in:
12
skills/lark-slides/prompt-sources/svg-slides/README.md
Normal file
12
skills/lark-slides/prompt-sources/svg-slides/README.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# SVG Slides Prompt Sources
|
||||
|
||||
This directory is the maintainer-only source for the SVG Slides runtime references.
|
||||
|
||||
Do not read these files while generating or editing a deck. Runtime agents should read `skills/lark-slides/references/svg-slides/` instead.
|
||||
|
||||
When changing prompt behavior:
|
||||
|
||||
1. Edit `full.debranded.md` and `split-manifest.json` here.
|
||||
2. Run `node skills/lark-slides/scripts/svg_slides_extract_references.mjs`.
|
||||
3. Run the SVG Slides tests and source coverage check.
|
||||
|
||||
@@ -72,21 +72,21 @@ When you do present it, call `show_form` ONCE with `meta.form_purpose: "outline_
|
||||
Slide count rule for this outline: the proposed outline is the actual slide sequence, not a chapter list. Use the user's explicit page count when given. Otherwise, default to 8-12 slides for normal requests. Do not plan fewer than 8 slides unless the user explicitly asks for a short / concise deck. Broad topic-only requests such as F1 introductions, financial analysis, product comparisons, or design guides still need 8-12 substantive slides with concrete material, not 5-6 generic chapters.
|
||||
|
||||
### Phase 6 — Write slide_content.md
|
||||
Write a `slide_content.md` structural outline to the project directory, **following the brief's `narrative_spine` for the narrative arc and each slide's role, and its `depth` directive for how much material each slide carries**: the key material (data points, claims, quotes) with source references. This is the content plan, NOT final wording — exact text, layout, and visuals are decided when writing each slide. It is also delivered to the user so they can reference sections when requesting changes.
|
||||
What it should NOT lock in: exact final sentences, image file paths, or chart layout details.
|
||||
Write a `slide_content.md` structural outline to the project directory, **following the brief's `narrative_spine` for the narrative arc and each slide's role, and its `depth` directive for how much material each slide carries**: the key material (data points, claims, quotes) with source references. For every slide, include `slide_role`, `key_message`, `visual_job`, `primary_visual_required`, `visual_subject`, `source_strategy`, and `expected_asset_kind`; cover, section-divider, closing, and key-showcase slides require an expressive primary visual plan.
|
||||
What it should NOT lock in: exact final sentences, image file paths, or chart layout details. It SHOULD lock the conceptual visual subject and why the visual supports the slide's key message, so asset preparation cannot become decorative afterthought.
|
||||
|
||||
### Phase 7 — Lock the visual direction & plan visuals
|
||||
The design brief's `visual_system` is AUTHORITATIVE for the look — do NOT override it with your own taste. Translate it (resolved in Phase 4) into the concrete `style_instruction` you pass to {{.ToolSlideOutline}}:
|
||||
- `aesthetic_direction`: the visual_system's design language + mood, verbatim in spirit.
|
||||
- `color_palette`: realize the visual_system's color system (its hues + roles), not your own.
|
||||
- `typography`: MATCH the visual_system's typography — keep its font **category and treatment** (serif vs sans-serif vs rounded vs mono, weight, UPPERCASE + letter-spacing) exactly. When mapping to fonts, choose from `<runtime_font_candidates>` when present; otherwise use the Font Palette in `<svg_reference>`. Pick a font in the SAME category (e.g. if the visual_system specifies a sans-serif uppercase display, pick a sans-serif display font — do NOT substitute a serif like Playfair; do NOT flip serif↔sans). Never re-pick fonts from your own editorial intuition; never the banned generic fonts.
|
||||
This becomes the deck's locked style — carry its `aesthetic_direction`, `color_palette`, and `typography` consistently across EVERY slide.
|
||||
Then plan visuals per slide — images AND charts together: how many images each needs and what aspect ratio, and for every slide whose point rests on a real quantitative data series (trend, multi-category comparison, part-to-whole split, distribution, 2D positioning) a chart. These are generated as assets BEFORE slide_edit, the same as images: once {{.ToolSlideOutline}} has created the project, call {{.ToolGenerateSvgChart}} for every planned chart (dispatch all together in one turn); slide_edit then embeds each returned `.svg` by `<rect slide:role="chart" href="...">`. A real data series goes through this tool — never hand-draw it from primitives. (See <visuals> and <chart_workflow>.)
|
||||
- `typography`: MATCH the visual_system's typography — keep its font **category and treatment** (serif vs sans-serif vs rounded vs mono, weight, UPPERCASE + letter-spacing) exactly. When mapping to fonts, choose from `<runtime_font_candidates>` when present; otherwise use the Font Palette in `<svg_reference>`. Pick a font in the SAME category (do NOT substitute serif↔sans or rounded↔formal). Write `receipts/typography_plan.json` with `version`, `font_category`, `display_font`, `body_font`, `label_font`, `source`, `selection_reason`, `forbidden_substitutions`, and `runtime_candidates_used`; never re-pick fonts from your own editorial intuition or banned generic fonts.
|
||||
This becomes the deck's locked style — carry its `aesthetic_direction`, `color_palette`, `typography`, and `typography_plan` consistently across EVERY slide.
|
||||
Then plan visuals per slide — images AND charts together: how many images each needs, what aspect ratio, which named entities require web image search, and which pages are cover / section-divider / closing / key-showcase. Write `receipts/asset_readiness.json` with a deck-level `visual_identity_fingerprint`, required search entities, prepared local assets, asset `purpose` / `asset_kind` / `expressive_role` / `content_relevance` / `format`, and per-slide `slide_role` / `visual_tier` / `primary_visual_required` / `primary_visual` / `layout_family` / `text_overlay` / `mask_required` / `mask_strategy` before slide_edit; in multi-deck/team runs, compare sibling fingerprints and do not reuse the same style family + composition signature. Once {{.ToolSlideOutline}} has created the project, call {{.ToolGenerateSvgChart}} for every planned chart (dispatch all together in one turn); slide_edit then embeds each returned `.svg` by `<rect slide:role="chart" href="...">`. A real data series goes through this tool — never hand-draw it from primitives. (See <visuals> and <chart_workflow>.)
|
||||
|
||||
### Phase 8 — Generate & deliver
|
||||
1. **{{.ToolSlideOutline}}** — pass the confirmed outline (main_title, pages, and the style_instruction locked in Phase 7). Creates the project directory, `outline.json`, style, and one empty `.svg` per slide. The language of your arguments sets the slide language. IMPORTANT: it overwrites ALL slide files — never call it again after slides are written (use {{.ToolSlideOrganize}} to add/delete pages later).
|
||||
2. **{{.ToolActivateSlidesEdit}}** — call immediately after slide_outline, before any slide_edit. Pass `project_dir`. This switches to a faster model optimized for slide writing.
|
||||
3. **{{.ToolSlideEdit}}** — write each slide as a COMPLETE SVG document following `<svg_reference>`. In `content_thinking`, state the layout intent, which visual assets you'll use, AND the animation decision for this slide (its build order, or `static`) per `<animation>`. Compose freely (no canned templates). Slides display incrementally as each completes. Add a per-slide build sequence and/or the deck's one page transition where it earns its place (see `<animation>` for when / how much; the elements are defined in `<svg_reference>`).
|
||||
3. **{{.ToolSlideEdit}}** — write each slide as a COMPLETE SVG document following `<svg_reference>` only after the slide's searched/generated local assets exist and are recorded in `asset_readiness.json`, and the locked font stacks are recorded in `typography_plan.json`. In `content_thinking`, state `slide_role`, key message, primary visual path, why the visual supports the message, text-overlay / mask strategy, layout intent, visual assets, typography role, and the animation decision for this slide (its build order, or `static`) per `<animation>`. Compose freely (no canned templates). Slides display incrementally as each completes. Add a per-slide build sequence and/or the deck's one page transition where it earns its place (see `<animation>` for when / how much; the elements are defined in `<svg_reference>`).
|
||||
4. **{{.ToolFinishSlidesEdit}}** — call after all slides are written; restores the default model.
|
||||
5. **Deliver** — the deck is complete; the UI shows it automatically (do not re-summarize slide content). Share the `slide_content.md` path and remind the user they can edit in the editor or request changes in chat.
|
||||
|
||||
@@ -108,15 +108,15 @@ Modifying structure after creation: add pages via {{.ToolSlideOrganize}} "add" (
|
||||
</content_quality>
|
||||
|
||||
<visuals>
|
||||
Visuals re-engage attention and carry meaning. Plan them deliberately; don't decorate.
|
||||
Visuals re-engage attention and carry meaning. Plan them deliberately; don't decorate. Key pages need images that carry the idea or emotion, not just any asset.
|
||||
- **Image sourcing priority**:
|
||||
1. **Generation (default)** — exact aspect ratios, palette-consistent, any ratio on demand. Best for abstract concepts, backgrounds, conceptual illustrations, non-standard ratios. Describe the concrete subject first, then add the deck's palette/mood as style qualifiers.
|
||||
2. **Search** ({{.ToolAssignImageSearchAgent}}) — ONLY for specific identifiable real-world entities (a named product, landmark, company). Do not search for logos.
|
||||
2. **Search** ({{.ToolAssignImageSearchAgent}}) — REQUIRED for specific identifiable real-world entities (a named product, landmark, company, person, place, artwork, paper, or venue). Do not search for logos; record the query/outcome in `asset_readiness.json` even when search fails and generation fallback is used.
|
||||
3. **Search + generation refinement** — when search has the right subject but wrong ratio/tone, use it as an image-to-image reference.
|
||||
- NEVER crop to force a ratio — generate at the exact ratio. Every content image should be unique across slides; backgrounds may repeat for consistency.
|
||||
- NEVER crop to force a ratio — generate at the exact ratio. Every content image should be unique across slides; backgrounds may repeat for consistency. Cover, section-divider, closing, and key-showcase slides require a strong primary visual; abstract geometry, icons, pale textures, and pure decorative backgrounds do not satisfy that requirement.
|
||||
- **Aspect ratio**: informational images (charts, diagrams, screenshots, infographics) MUST preserve their original ratio — extract dimensions from the filename pattern `image_w{W}_h{H}_...` and size the SVG `<image>` to match. Decorative photos may be composed freely.
|
||||
- **SVG elements** (see `<svg_reference>` for full attributes): place an image with `<image slide:role="image" slide:shape-type="image" href="..." x y width height>` (a single `<image>` element — never wrapped in `<g>`); set a full-bleed slide background image with `<rect slide:role="background" fill="url(/abs/path.jpg)">`.
|
||||
- **Cover/closing**: prefer generation for style consistency (search only for a specific subject). Generated images must contain NO baked-in text — typography is rendered by the slide on top. Match the image's composition to the chosen cover layout (full-bleed background vs. a positioned image zone vs. no image).
|
||||
- **Hero/key pages**: cover, section-divider, closing, and key-showcase pages must use a primary visual as the layout core (full-bleed hero, large image zone, dominant screenshot/document capture, or editorial crop). Use PNG/JPG/WebP for photographic, product, people, store, screenshot, or document-capture assets; SVG is for charts, icons, line decorations, and self-drawn diagrams. If text sits on a hero image, add a concrete `slide:role="text-scrim"` or `slide:role="image-mask"` overlay, gradient mask, or safe text zone so the image has impact and the text remains readable.
|
||||
</visuals>
|
||||
|
||||
<about_slides_outline>
|
||||
@@ -898,7 +898,7 @@ The deliverable is one **Style Deconstruction** document with 7 design dimension
|
||||
## Core principles
|
||||
|
||||
1. **Anchor to the user's explicit asks, then design a coherent direction for the topic.** If the user stated a palette / mood / font feel, honor it exactly and build the rest around it. Otherwise, choose a distinctive, topic-appropriate direction — do NOT default to a generic corporate look.
|
||||
2. **Commit to ONE distinctive, deconstructable style.** Pick a clear visual language (e.g. editorial poster, brand system, magazine layout, cinematic photography treatment) and deconstruct it concretely. Avoid a vague mash-up.
|
||||
2. **Commit to ONE distinctive, deconstructable style.** Pick a clear visual language (e.g. editorial poster, brand system, magazine layout, cinematic photography treatment) and deconstruct it concretely. Avoid a vague mash-up; in multi-deck/team runs, make this style intentionally different from sibling decks and name a stable `visual_identity_fingerprint`.
|
||||
3. **Deconstruct to buildable granularity.** Each dimension must be concrete enough to directly guide implementation (hex values, font categories, ratios) — not vague adjectives.
|
||||
4. **Visual style only — never content decisions.** A Style Deconstruction describes "what this design looks like" (color, type, material, decoration), NOT "how content is organized" (how much info per slide, density, information architecture). The same visual style can carry wildly different content densities — a black-white-red minimalist style is one big image per page on a product site, but dense charts and data tables on a financial review. Information architecture is decided by the content itself, not constrained by the visual style.
|
||||
5. **Aim ABOVE the obvious default.** Whatever treatment first comes to mind for a topic is the training-data median — the on-the-nose cliché that reads as generic AI slop. Treat your first instinct as the floor to rise above, not the answer. Sophistication comes from **restraint and intention** — a confident, slightly-unexpected palette; editorial typography as the hero; deliberate negative space; real material/print references; precise composition and alignment — **never from applied "effects" or manufactured atmosphere**. Glows, spotlights, ambient haze, and gratuitous gradients are not design; they are the absence of it — a page's mood must come from its color, type, and composition, not from a light effect layered on top. When a direction feels like the expected look for this topic, push to something more specific and more restrained — that is the line between *designed* and *generated*.
|
||||
@@ -962,12 +962,12 @@ This describes visual composition technique ONLY, NOT content density or informa
|
||||
|
||||
Do NOT write density / architecture constraints in this dimension (no "one data point per slide" / "lots of whitespace"). Those are information-architecture decisions, decided by the content, not part of visual style.
|
||||
|
||||
#### Dimension 4: Imagery treatment
|
||||
#### Dimension 4: Imagery treatment and imagery_system
|
||||
- **Image type**: photo / illustration / icon / vector / 3D / chart
|
||||
- **Color treatment**: original / desaturated / monochrome / duotone
|
||||
- **Texture**: halftone / blur / grain / none
|
||||
- **Cropping**: regular crop / shaped crop / bleed / cut-out
|
||||
- **Relationship to text**: image-text separated / overlaid / image as background
|
||||
- **Relationship to text**: image-text separated / overlaid / image as background; also define an `imagery_system` with `hero_treatment`, `key_page_visual_rule`, `small_image_format_preference` (png/jpg/webp for photos, products, people, stores, screenshots, document captures), `image_text_relationship`, `mask_strategy`, and `primary_visual_do_not_use` (icons, abstract geometry, low-opacity texture, decorative patterns)
|
||||
|
||||
#### Dimension 5: Material & texture
|
||||
- **Surface quality**: clean flat / paper texture / noise / metallic / matte, etc.
|
||||
@@ -1003,8 +1003,8 @@ Output one Markdown document (no code fences), in the deck's language (Chinese w
|
||||
[fill per Dimension 2 — give concrete Chinese font names]
|
||||
### 3. Layout language
|
||||
[fill per Dimension 3]
|
||||
### 4. Imagery treatment
|
||||
[fill per Dimension 4]
|
||||
### 4. Imagery treatment and imagery_system
|
||||
[fill per Dimension 4, including the explicit `imagery_system` fields]
|
||||
### 5. Material & texture
|
||||
[fill per Dimension 5]
|
||||
### 6. Decoration language
|
||||
@@ -1017,7 +1017,7 @@ Output one Markdown document (no code fences), in the deck's language (Chinese w
|
||||
## Quality check
|
||||
|
||||
Verify all items before output:
|
||||
- [ ] All 7 dimensions filled, no gaps
|
||||
- [ ] All 7 dimensions filled, no gaps, including the explicit `imagery_system` fields under Dimension 4
|
||||
- [ ] The visual direction honors the user's explicit color / mood / font asks from the conversation (if any)
|
||||
- [ ] Color system has concrete hex values, not a vague "warm tone"
|
||||
- [ ] Typography has concrete categories (serif / sans / handwriting) + concrete Chinese font names, not "a nice font"
|
||||
@@ -1288,7 +1288,7 @@ A text-only slide with decorative shapes signals skipped preparation. It is rare
|
||||
|
||||
## Image usage
|
||||
- NEVER reference a non-existent or non-local image. Always use absolute paths for images, fonts, and other resources.
|
||||
- Use ONLY the image path(s) prepared for the slide (to avoid duplicates). Place content images with concrete subjects (UI mockups, illustrations) as `<image slide:role="shape" slide:shape-type="image">` in a split or side layout — do not fade them to low opacity and use as full-screen backgrounds, which creates ghost-like visual noise behind foreground content.
|
||||
- Use ONLY the image path(s) prepared for the slide and recorded in `receipts/asset_readiness.json` (to avoid duplicates), including any declared `primary_visual`. Place content images with concrete subjects (photos, products, people, stores, screenshots, document captures, UI mockups, illustrations) as `<image slide:role="image" slide:shape-type="image">`; small photographic assets should be PNG/JPG/WebP. SVG is for charts, icons, line decorations, and self-drawn diagrams, not a substitute for searched/generated bitmap imagery.
|
||||
|
||||
## Incremental processing
|
||||
- Slides are written and displayed as soon as each one is complete.
|
||||
@@ -1299,7 +1299,7 @@ A text-only slide with decorative shapes signals skipped preparation. It is rare
|
||||
<layout_and_design>
|
||||
Compose every slide's layout from scratch to fit its specific content and the deck's aesthetic direction — follow "Design Thinking", "Aesthetic Guidelines", and "Layout Freedom" in `<svg_reference>`. Do NOT rotate through a fixed menu of canned patterns, and do NOT apply formulaic "diagram" templates — that produces the template-stamped feel we are avoiding. Favor unexpected, asymmetric, content-specific composition: overlap, diagonal flow, grid-breaking elements, a single dominant hero element, generous negative space. Vary the structural arrangement between adjacent slides while keeping the deck's background, card-surface style, and decoration density CONSTANT across the whole deck.
|
||||
|
||||
Use visual elements (shapes, lines, icons, accent bars, gradients) to break up text and build hierarchy; apply the accent color sparingly for emphasis; maintain white space and contrast. When text sits on a background image, overlay the image with a semi-transparent shape first so the text stays readable.
|
||||
Use visual elements (shapes, lines, icons, accent bars, gradients) to break up text and build hierarchy; apply the accent color sparingly for emphasis; maintain white space and contrast. When text sits on a background image, add a concrete readable overlay (`slide:role="text-scrim"`), image mask (`slide:role="image-mask"`), gradient, or safe text zone first so the text stays readable.
|
||||
</layout_and_design>
|
||||
|
||||
<prohibited_practices>
|
||||
@@ -1317,7 +1317,7 @@ Use visual elements (shapes, lines, icons, accent bars, gradients) to break up t
|
||||
|
||||
<thinking_process_instructions>
|
||||
Before writing the SVG, use the `content_thinking` parameter to document:
|
||||
1. **Visual assets**: which images/charts you will use (list file paths). If none are available, the slide is missing preparation — go prepare visuals first.
|
||||
1. **Visual assets**: the slide_role, primary visual path when required, which images/charts you will use (list file paths and the asset_readiness entry), why the primary visual supports the key message, and whether text overlays require a mask/scrim. If none are available, the slide is missing preparation — go search/generate/download visuals first.
|
||||
2. **Layout**: what composition best fits this content, and how it differs from adjacent slides.
|
||||
3. **Key message**: the ONE takeaway, and how typography and spacing emphasize it.
|
||||
4. **Data visualization**: can any content be shown as a chart or large stat number instead of text?
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"version": "svg-slides.split-manifest.v1",
|
||||
"source": "skills/lark-slides/references/svg-slides/source/full.debranded.md",
|
||||
"source": "skills/lark-slides/prompt-sources/svg-slides/full.debranded.md",
|
||||
"source_export": "/Users/bytedance/Documents/Codex/2026-07-01/https-bytedance-larkoffice-com-docx-kncld7xr5ohwonxhksncz3lxnvd/outputs/lark_doc_KnCLd7xr5ohWONxhKsncZ3Lxnvd/full.debranded.md",
|
||||
"source_role": "provenance_and_coverage_authority_not_default_runtime_context",
|
||||
"source_role": "maintainer_prompt_source_not_runtime_generation_context",
|
||||
"sections": [
|
||||
{
|
||||
"id": "title",
|
||||
@@ -1,6 +1,8 @@
|
||||
# SVG Slides Local Generation
|
||||
|
||||
This reference family is generated from `source/full.debranded.md` and organized by runtime unit.
|
||||
This directory is the runtime reference contract for local SVG Slides generation.
|
||||
|
||||
Do not use maintainer prompt-source files as generation context. Runtime agents should follow the read routes below.
|
||||
|
||||
## Read Routes
|
||||
|
||||
@@ -9,9 +11,11 @@ This reference family is generated from `source/full.debranded.md` and organized
|
||||
| Understand the main SVG Slides workflow | `system-prompt.md` | `svg-reference.md`, `tools/slides-edit.md` |
|
||||
| Author or repair SVG protocol content | `svg-reference.md` | `tools/slides-edit.md`, `tools/compute-custom-shape-bbox.md` |
|
||||
| Resolve deck design direction | `resolve-design-brief/README.md` | `deck-design-reference-catalog.md` |
|
||||
| Choose or audit typography | `resolve-design-brief/style-director-prompt.md` | `system-prompt.md`, `svg-reference.md`, `validation/typography-plan.md` |
|
||||
| Generate or revise charts | `generate-svg-chart/README.md` | matching files under `generate-svg-chart/snippets/` |
|
||||
| Continue or convert an existing deck | `tools/slides-convert.md` | `tools/slide-organize.md`, `tools/slides-edit.md` |
|
||||
| Audit source coverage | `validation/source-coverage.md` | `source/split-manifest.json`, `source/full.debranded.md` |
|
||||
| Prove asset search/readiness | `validation/asset-readiness.md` | `../../../scripts/svg_slides_asset_readiness.mjs`, `../../../scripts/svg_slides_visual_identity_check.mjs` |
|
||||
| Prove typography planning | `validation/typography-plan.md` | `../../../scripts/svg_slides_typography_plan.mjs` |
|
||||
| Prove prompt reads for a generated deck | `validation/prompt-read-trace.md` | `../../scripts/svg_slides_prompt_trace.mjs` |
|
||||
|
||||
## Boundary
|
||||
@@ -22,10 +26,16 @@ Local SVG Slides generation and validation produce local artifacts. Publishing t
|
||||
|
||||
Every generated local bundle must include `receipts/prompt_read_trace.json` before `manifest.json` may set `publish_ready=true`.
|
||||
|
||||
The trace records the prompt/reference Markdown files read for the generation task, including path, role, reason, source lines, and SHA-256.
|
||||
The trace records the runtime prompt/reference Markdown files read for the generation task, including path, role, reason, and SHA-256.
|
||||
|
||||
## Source Coverage
|
||||
## Asset Readiness Gate
|
||||
|
||||
- Covers manifest sections: title
|
||||
- Source lines: 1-1
|
||||
- Coverage mode: routing entry; executable content lives in the linked runtime-unit files.
|
||||
Every generated local bundle must include `receipts/asset_readiness.json` before `manifest.json` may set `publish_ready=true`.
|
||||
|
||||
The receipt records the deck's visual identity fingerprint, required image-search entities, prepared local assets, and per-slide layout families.
|
||||
|
||||
## Typography Plan Gate
|
||||
|
||||
Every generated local bundle must include `receipts/typography_plan.json` before `manifest.json` may set `publish_ready=true`.
|
||||
|
||||
The receipt records the deck's selected font category, display/body/label stacks, source, and rejected substitutions. Slide SVG text must use only those planned stacks.
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
## Context
|
||||
- Role: reference
|
||||
- Purpose: Narrative archetypes and depth rubric consumed by the design brief prompt.
|
||||
- Source lines: 1082-1234
|
||||
## Content
|
||||
# Deck design reference catalog
|
||||
|
||||
@@ -157,9 +156,3 @@ THREE QUICK DISAMBIGUATIONS (where decks wrongly converge to MID)
|
||||
2. Expert vs lay on the SAME topic (ai-hardtech-pitch vs ai-101): both about AI — one is eval tables + failure-mode rates for technical partners, the other bans benchmark charts entirely. Audience, not topic, sets altitude.
|
||||
3. Pitch is NOT low-detail everywhere: stage demo-day is C1 (one sentence/slide), but the Series B/C IC pre-read is A-altitude self-read (cohort triangles, Rule-of-40, 20-page-memo-equivalent density). Same category, opposite depth — delivery_mode + audience decide.
|
||||
```
|
||||
## Source Coverage
|
||||
|
||||
- Covers manifest sections: deck_design_reference_catalog
|
||||
- Source lines: 1082-1234
|
||||
- Coverage mode: extracted runtime-unit content, preserving executable prompt or contract boundaries.
|
||||
- Omitted source-only artifacts: copied implementation filenames only.
|
||||
|
||||
@@ -9,7 +9,3 @@ Before generating or revising any chart, read these files in order:
|
||||
5. Snippets whose frontmatter `chart_types` include the selected `chart_type`
|
||||
|
||||
If snippet routing is uncertain, read `full.md`.
|
||||
|
||||
## Source Slice
|
||||
|
||||
- Source lines: 1357-2356
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
## Context
|
||||
- Role: prompt
|
||||
- Purpose: Defines the chart sub-agent response contract.
|
||||
- Source lines: 1436-1487
|
||||
## Content
|
||||
③ 契约模板 chart contract template:
|
||||
|
||||
@@ -56,6 +55,3 @@ Hard rules:
|
||||
|
||||
**Examples are illustrative, not templates.** Every numeric coordinate, bar count, slice count, scale factor, and category in the snippet examples is shaped for *that example's* data, NOT yours. Re-derive all geometry from YOUR `data` and item count in the `<design>` block (`Scale_Math` plus `Coordinates`). Never copy an example's coordinates or category count; match the layout strategy, then compute your own numbers.
|
||||
```
|
||||
## Source Slice
|
||||
|
||||
- Source lines: 1436-1487
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
## Context
|
||||
- Role: reference
|
||||
- Purpose: Defines chart craft rules, grid, palette roles, typography, labels, and invariants.
|
||||
- Source lines: 1489-1586
|
||||
## Content
|
||||
④ 设计简报 \_envelope.md(始终注入;§A 工艺原则 / §B 画布网格 Box Model / §C 色板角色 / §D 排版 / §E 线条 / §F 放置词汇 / §G 不变量。[fs-\*] 字号、scaffold 网格常数、[token] 色板均 Go 侧按 width/accent/theme 注入,此处为规则文本,示例几何按 tier L 常数锚定):
|
||||
|
||||
@@ -102,6 +101,3 @@ When skeletons name a placement strategy, these are the geometric rules:
|
||||
7. Gridlines + tick marks + data labels simultaneously; with direct labels, gridlines and the Y-axis go.
|
||||
8. A bordered legend box; a title or subtitle inside the SVG (the host owns headings); a **global / full-canvas background fill** — the backing stays transparent so the chart inherits the host slide background (a *local* background shape — an emphasis band, a shaded forecast/threshold zone, a callout card in a palette color — is welcome when it strengthens the message); external resources, `<script>`, `<foreignObject>`.
|
||||
````
|
||||
## Source Slice
|
||||
|
||||
- Source lines: 1489-1586
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
## Context
|
||||
- Role: prompt_bundle
|
||||
- Purpose: Complete generate_svg_chart bundle for audit or uncertain snippet routing.
|
||||
- Source lines: 1357-2356
|
||||
## Required Read Path
|
||||
|
||||
1. `input-schema.md`
|
||||
@@ -1010,9 +1009,3 @@ Draw connectors **before** the bar rects (document order) so bars overlap them.
|
||||
stroke="[context]" stroke-width="1" stroke-dasharray="2,2"/>
|
||||
```
|
||||
````
|
||||
## Source Coverage
|
||||
|
||||
- Covers manifest sections: generate_svg_chart_tool
|
||||
- Source lines: 1357-2356
|
||||
- Coverage mode: extracted runtime-unit content, preserving executable prompt or contract boundaries.
|
||||
- Omitted source-only artifacts: copied implementation filenames only.
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
## Context
|
||||
- Role: schema
|
||||
- Purpose: Defines parameters, chart routing, composition gate, and output path rules.
|
||||
- Source lines: 1367-1434
|
||||
## Content
|
||||
② 入参 schema(generate_svg_chart 参数逐个;chart_type 路由规则 = 选型方法论唯一真相源,见 chart service implementation):
|
||||
|
||||
@@ -72,6 +71,3 @@ OPTIONAL. Use ONLY for revising an existing chart. Provide specific visual or st
|
||||
--- reference_design_path (optional, string) ---
|
||||
OPTIONAL. Provide this if and only if 'revision_instruction' is provided. Path to the previous design.md file to be used as the baseline for revision.
|
||||
```
|
||||
## Source Slice
|
||||
|
||||
- Source lines: 1367-1434
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
## Context
|
||||
- Role: snippet
|
||||
- Purpose: Chart design snippet for chart types: hundred-percent-stacked-column, marimekko
|
||||
- Source lines: 1591-1617
|
||||
## Content
|
||||
---
|
||||
title: 100% indicator
|
||||
@@ -29,6 +28,3 @@ Skip on regular `stacked-column` (the per-column total IS the takeaway; use
|
||||
100% = $530B
|
||||
</text>
|
||||
```
|
||||
## Source Slice
|
||||
|
||||
- Source lines: 1591-1617
|
||||
|
||||
@@ -4,30 +4,25 @@ Load snippets whose frontmatter `chart_types` include the selected `chart_type`.
|
||||
|
||||
## Snippet Index
|
||||
|
||||
- `100-percent-indicator.md` (1591-1617): hundred-percent-stacked-column, marimekko
|
||||
- `above-cluster-group-title.md` (1618-1642): clustered-column
|
||||
- `axis-break-marker.md` (1643-1700): column, stacked-column, bar, stacked-bar, line, stacked-area, waterfall, clustered-column, diverging-bar, combination, small-multiples
|
||||
- `bottom-multi-column-legend.md` (1701-1735): pie, doughnut
|
||||
- `cagr-arrow.md` (1736-1773): column, line, indexed-line, stacked-area, small-multiples, combination, clustered-column
|
||||
- `category-span-bracket.md` (1774-1809): column, stacked-column, hundred-percent-stacked-column, bar, stacked-bar, clustered-column, line, indexed-line, stacked-area, scatter, bubble, waterfall, marimekko, diverging-bar, small-multiples, combination
|
||||
- `column-divider.md` (1810-1834): clustered-column, marimekko
|
||||
- `delta-badge.md` (1835-1874): column, stacked-column, hundred-percent-stacked-column, bar, stacked-bar, clustered-column, line, indexed-line, stacked-area, scatter, bubble, waterfall, marimekko, pie, doughnut, diverging-bar, small-multiples, combination
|
||||
- `ellipse-callout.md` (1875-1919): column, stacked-column, hundred-percent-stacked-column, bar, stacked-bar, clustered-column, line, indexed-line, stacked-area, scatter, bubble, waterfall, marimekko, pie, doughnut, diverging-bar, small-multiples, combination
|
||||
- `endpoint-difference-bracket.md` (1920-1946): column, stacked-column, bar, stacked-bar, line, indexed-line, stacked-area, waterfall, diverging-bar, combination, clustered-column
|
||||
- `error-bar.md` (1947-1981): column, bar, line, scatter, bubble, combination
|
||||
- `mini-legend-pair.md` (1982-2014): line, indexed-line, stacked-area, scatter, bubble, combination
|
||||
- `quadrant-guides.md` (2015-2062): scatter, bubble
|
||||
- `range-band.md` (2063-2087): line, indexed-line, stacked-area, combination
|
||||
- `secondary-metric-overlay.md` (2088-2125): column, stacked-column, hundred-percent-stacked-column, diverging-bar, clustered-column
|
||||
- `series-connector.md` (2126-2153): stacked-column, hundred-percent-stacked-column, stacked-bar, marimekko, combination
|
||||
- `slice-breakout.md` (2154-2193): pie, doughnut
|
||||
- `total-difference-arrow.md` (2194-2240): stacked-column, hundred-percent-stacked-column, stacked-area, stacked-bar
|
||||
- `triangle-pointer.md` (2241-2267): column, bar, line, stacked-area, bubble, waterfall, combination
|
||||
- `value-line.md` (2268-2299): column, stacked-column, hundred-percent-stacked-column, bar, stacked-bar, line, indexed-line, stacked-area, scatter, bubble, waterfall, diverging-bar, small-multiples, combination, clustered-column
|
||||
- `vertical-bracket-measurement.md` (2300-2329): stacked-column, hundred-percent-stacked-column, stacked-bar, waterfall, diverging-bar, clustered-column, marimekko
|
||||
- `waterfall-connector.md` (2330-2353): waterfall
|
||||
|
||||
## Source Slice
|
||||
|
||||
- Source lines: 1590-2354
|
||||
- Snippet entries include their own source line ranges for traceability
|
||||
- `100-percent-indicator.md`: hundred-percent-stacked-column, marimekko
|
||||
- `above-cluster-group-title.md`: clustered-column
|
||||
- `axis-break-marker.md`: column, stacked-column, bar, stacked-bar, line, stacked-area, waterfall, clustered-column, diverging-bar, combination, small-multiples
|
||||
- `bottom-multi-column-legend.md`: pie, doughnut
|
||||
- `cagr-arrow.md`: column, line, indexed-line, stacked-area, small-multiples, combination, clustered-column
|
||||
- `category-span-bracket.md`: column, stacked-column, hundred-percent-stacked-column, bar, stacked-bar, clustered-column, line, indexed-line, stacked-area, scatter, bubble, waterfall, marimekko, diverging-bar, small-multiples, combination
|
||||
- `column-divider.md`: clustered-column, marimekko
|
||||
- `delta-badge.md`: column, stacked-column, hundred-percent-stacked-column, bar, stacked-bar, clustered-column, line, indexed-line, stacked-area, scatter, bubble, waterfall, marimekko, pie, doughnut, diverging-bar, small-multiples, combination
|
||||
- `ellipse-callout.md`: column, stacked-column, hundred-percent-stacked-column, bar, stacked-bar, clustered-column, line, indexed-line, stacked-area, scatter, bubble, waterfall, marimekko, pie, doughnut, diverging-bar, small-multiples, combination
|
||||
- `endpoint-difference-bracket.md`: column, stacked-column, bar, stacked-bar, line, indexed-line, stacked-area, waterfall, diverging-bar, combination, clustered-column
|
||||
- `error-bar.md`: column, bar, line, scatter, bubble, combination
|
||||
- `mini-legend-pair.md`: line, indexed-line, stacked-area, scatter, bubble, combination
|
||||
- `quadrant-guides.md`: scatter, bubble
|
||||
- `range-band.md`: line, indexed-line, stacked-area, combination
|
||||
- `secondary-metric-overlay.md`: column, stacked-column, hundred-percent-stacked-column, diverging-bar, clustered-column
|
||||
- `series-connector.md`: stacked-column, hundred-percent-stacked-column, stacked-bar, marimekko, combination
|
||||
- `slice-breakout.md`: pie, doughnut
|
||||
- `total-difference-arrow.md`: stacked-column, hundred-percent-stacked-column, stacked-area, stacked-bar
|
||||
- `triangle-pointer.md`: column, bar, line, stacked-area, bubble, waterfall, combination
|
||||
- `value-line.md`: column, stacked-column, hundred-percent-stacked-column, bar, stacked-bar, line, indexed-line, stacked-area, scatter, bubble, waterfall, diverging-bar, small-multiples, combination, clustered-column
|
||||
- `vertical-bracket-measurement.md`: stacked-column, hundred-percent-stacked-column, stacked-bar, waterfall, diverging-bar, clustered-column, marimekko
|
||||
- `waterfall-connector.md`: waterfall
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
## Context
|
||||
- Role: snippet
|
||||
- Purpose: Chart design snippet for chart types: clustered-column
|
||||
- Source lines: 1618-1642
|
||||
## Content
|
||||
---
|
||||
title: Above-cluster group title
|
||||
@@ -27,6 +26,3 @@ Prefer this over a separate legend when the groups are visually contiguous
|
||||
Replace "Regular format" with your group label. -->
|
||||
<text x="320" y="110" font-size="[fs-meta]" font-weight="bold" fill="[ink]" text-anchor="middle">Regular format</text>
|
||||
```
|
||||
## Source Slice
|
||||
|
||||
- Source lines: 1618-1642
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
## Context
|
||||
- Role: snippet
|
||||
- Purpose: Chart design snippet for chart types: column, stacked-column, bar, stacked-bar, line, stacked-area, waterfall, clustered-column, diverging-bar, combination, small-multiples
|
||||
- Source lines: 1643-1700
|
||||
## Content
|
||||
---
|
||||
title: Axis break marker
|
||||
@@ -60,6 +59,3 @@ to e.g. ~70 / ~115 / ~160px (monotonic in value), each with its own value label.
|
||||
<polygon points="200,440 300,440 309,456 300,472 200,472" fill="[context]"/>
|
||||
<polygon points="305,440 314,456 305,472 420,472 420,440" fill="[context]"/>
|
||||
```
|
||||
## Source Slice
|
||||
|
||||
- Source lines: 1643-1700
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
## Context
|
||||
- Role: snippet
|
||||
- Purpose: Chart design snippet for chart types: pie, doughnut
|
||||
- Source lines: 1701-1735
|
||||
## Content
|
||||
---
|
||||
title: Bottom multi-column legend
|
||||
@@ -37,6 +36,3 @@ reserved footnote line at footnote_y (tier L: 592).
|
||||
If you need a 5th entry, drop to a 2-row × 3-col grid with row y=540 and y=568,
|
||||
each row anchored at x=56 / 312 / 568 (3 cols wide). Anything past 5 entries
|
||||
means the pie has too many slices — fold smaller ones into "Other" first.
|
||||
## Source Slice
|
||||
|
||||
- Source lines: 1701-1735
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
## Context
|
||||
- Role: snippet
|
||||
- Purpose: Chart design snippet for chart types: column, line, indexed-line, stacked-area, small-multiples, combination, clustered-column
|
||||
- Source lines: 1736-1773
|
||||
## Content
|
||||
---
|
||||
title: CAGR arrow
|
||||
@@ -40,6 +39,3 @@ annotation, not a series line.
|
||||
CAGR +6.4%
|
||||
</text>
|
||||
```
|
||||
## Source Slice
|
||||
|
||||
- Source lines: 1736-1773
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
## Context
|
||||
- Role: snippet
|
||||
- Purpose: Chart design snippet for chart types: column, stacked-column, hundred-percent-stacked-column, bar, stacked-bar, clustered-column, line, indexed-line, stacked-area, scatter, bubble, waterfall, marimekko, diverging-bar, small-multiples, combination
|
||||
- Source lines: 1774-1809
|
||||
## Content
|
||||
---
|
||||
title: Category-span bracket
|
||||
@@ -38,6 +37,3 @@ with a label outside. Use for category-spanning numeric claims ("+90M jobs acros
|
||||
<tspan x="138" dy="1.2em">needed by 2030</tspan>
|
||||
</text>
|
||||
```
|
||||
## Source Slice
|
||||
|
||||
- Source lines: 1774-1809
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
## Context
|
||||
- Role: snippet
|
||||
- Purpose: Chart design snippet for chart types: clustered-column, marimekko
|
||||
- Source lines: 1810-1834
|
||||
## Content
|
||||
---
|
||||
title: Column divider
|
||||
@@ -27,6 +26,3 @@ column (it's a separator inside a filled rect, not on top of the host background
|
||||
<line x1="324" y1="48" x2="324" y2="480"
|
||||
stroke="[ink]" stroke-width="1"/>
|
||||
```
|
||||
## Source Slice
|
||||
|
||||
- Source lines: 1810-1834
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
## Context
|
||||
- Role: snippet
|
||||
- Purpose: Chart design snippet for chart types: column, stacked-column, hundred-percent-stacked-column, bar, stacked-bar, clustered-column, line, indexed-line, stacked-area, scatter, bubble, waterfall, marimekko, pie, doughnut, diverging-bar, small-multiples, combination
|
||||
- Source lines: 1835-1874
|
||||
## Content
|
||||
---
|
||||
title: Delta badge
|
||||
@@ -42,6 +41,3 @@ not a data mark. Never carry `rx` onto bars, columns, or slices (envelope "Anti-
|
||||
<rect x="680" y="260" width="44" height="20" rx="4" ry="4" fill="[context]"/>
|
||||
<text x="702" y="274" font-size="[fs-label]" font-weight="bold" fill="#ffffff" text-anchor="middle">−55%</text>
|
||||
```
|
||||
## Source Slice
|
||||
|
||||
- Source lines: 1835-1874
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
## Context
|
||||
- Role: snippet
|
||||
- Purpose: Chart design snippet for chart types: column, stacked-column, hundred-percent-stacked-column, bar, stacked-bar, clustered-column, line, indexed-line, stacked-area, scatter, bubble, waterfall, marimekko, pie, doughnut, diverging-bar, small-multiples, combination
|
||||
- Source lines: 1875-1919
|
||||
## Content
|
||||
---
|
||||
title: Ellipse callout
|
||||
@@ -47,6 +46,3 @@ only under the envelope §C gate — a "−90% error rate" is GOOD news and stay
|
||||
<tspan x="600" dy="1.2em" font-size="[fs-label]" font-weight="bold">159</tspan>
|
||||
</text>
|
||||
```
|
||||
## Source Slice
|
||||
|
||||
- Source lines: 1875-1919
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
## Context
|
||||
- Role: snippet
|
||||
- Purpose: Chart design snippet for chart types: column, stacked-column, bar, stacked-bar, line, indexed-line, stacked-area, waterfall, diverging-bar, combination, clustered-column
|
||||
- Source lines: 1920-1946
|
||||
## Content
|
||||
---
|
||||
title: Endpoint difference bracket
|
||||
@@ -29,6 +28,3 @@ use **Category-span bracket** instead.
|
||||
<polygon points="690,256 698,260 690,264" fill="[ink]"/>
|
||||
<text x="702" y="264" font-size="[fs-label]" font-weight="bold" fill="[ink]">+18%</text>
|
||||
```
|
||||
## Source Slice
|
||||
|
||||
- Source lines: 1920-1946
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
## Context
|
||||
- Role: snippet
|
||||
- Purpose: Chart design snippet for chart types: column, bar, line, scatter, bubble, combination
|
||||
- Source lines: 1947-1981
|
||||
## Content
|
||||
---
|
||||
title: Error bar
|
||||
@@ -37,6 +36,3 @@ horizontally and the caps are vertical lines.
|
||||
<line x1="236" y1="255" x2="244" y2="255" stroke="[ink]" stroke-width="1"/>
|
||||
<line x1="236" y1="305" x2="244" y2="305" stroke="[ink]" stroke-width="1"/>
|
||||
```
|
||||
## Source Slice
|
||||
|
||||
- Source lines: 1947-1981
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
## Context
|
||||
- Role: snippet
|
||||
- Purpose: Chart design snippet for chart types: line, indexed-line, stacked-area, scatter, bubble, combination
|
||||
- Source lines: 1982-2014
|
||||
## Content
|
||||
---
|
||||
title: Mini-legend pair
|
||||
@@ -35,6 +34,3 @@ not categorical fanout.
|
||||
<circle cx="192" cy="40" r="3" fill="[accent]"/>
|
||||
<text x="208" y="44" font-size="[fs-meta]" fill="[ink]">Fact</text>
|
||||
```
|
||||
## Source Slice
|
||||
|
||||
- Source lines: 1982-2014
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
## Context
|
||||
- Role: snippet
|
||||
- Purpose: Chart design snippet for chart types: scatter, bubble
|
||||
- Source lines: 2015-2062
|
||||
## Content
|
||||
---
|
||||
title: Quadrant guides
|
||||
@@ -50,6 +49,3 @@ midpoint.
|
||||
<text x="894" y="532" font-size="[fs-meta]" font-weight="bold" fill="[axis]" text-anchor="end">Cash cows</text>
|
||||
<text x="58" y="532" font-size="[fs-meta]" font-weight="bold" fill="[axis]" text-anchor="start">Dogs</text>
|
||||
```
|
||||
## Source Slice
|
||||
|
||||
- Source lines: 2015-2062
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
## Context
|
||||
- Role: snippet
|
||||
- Purpose: Chart design snippet for chart types: line, indexed-line, stacked-area, combination
|
||||
- Source lines: 2063-2087
|
||||
## Content
|
||||
---
|
||||
title: Range band
|
||||
@@ -27,6 +26,3 @@ Fill with `[accent]` at low opacity (`fill-opacity="0.15"`) — a faint accent e
|
||||
Replace x_center / range values per your data. -->
|
||||
<rect x="206" y="140" width="20" height="190" fill="[accent]" fill-opacity="0.15" stroke="none"/>
|
||||
```
|
||||
## Source Slice
|
||||
|
||||
- Source lines: 2063-2087
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
## Context
|
||||
- Role: snippet
|
||||
- Purpose: Chart design snippet for chart types: column, stacked-column, hundred-percent-stacked-column, diverging-bar, clustered-column
|
||||
- Source lines: 2088-2125
|
||||
## Content
|
||||
---
|
||||
title: Secondary-metric overlay
|
||||
@@ -40,6 +39,3 @@ For a single average / target instead of a per-category secondary metric, use
|
||||
<text x="400" y="296" font-size="[fs-label]" fill="[ink]" text-anchor="middle">−1pts</text>
|
||||
<text x="520" y="332" font-size="[fs-label]" fill="[ink]" text-anchor="middle">+2pts</text>
|
||||
```
|
||||
## Source Slice
|
||||
|
||||
- Source lines: 2088-2125
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
## Context
|
||||
- Role: snippet
|
||||
- Purpose: Chart design snippet for chart types: stacked-column, hundred-percent-stacked-column, stacked-bar, marimekko, combination
|
||||
- Source lines: 2126-2153
|
||||
## Content
|
||||
---
|
||||
title: Series connector
|
||||
@@ -30,6 +29,3 @@ hero is a per-stack TOTAL (not a per-segment trend across stacks), use
|
||||
<line x1="240" y1="300" x2="320" y2="280"
|
||||
stroke="[context]" stroke-width="1"/>
|
||||
```
|
||||
## Source Slice
|
||||
|
||||
- Source lines: 2126-2153
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
## Context
|
||||
- Role: snippet
|
||||
- Purpose: Chart design snippet for chart types: pie, doughnut
|
||||
- Source lines: 2154-2193
|
||||
## Content
|
||||
---
|
||||
title: Slice breakout
|
||||
@@ -42,6 +41,3 @@ Wrap the slice path in `<g transform="translate(Δx, Δy)">`.
|
||||
fill="[accent]" stroke="[bg]" stroke-width="2"/>
|
||||
</g>
|
||||
```
|
||||
## Source Slice
|
||||
|
||||
- Source lines: 2154-2193
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
## Context
|
||||
- Role: snippet
|
||||
- Purpose: Chart design snippet for chart types: stacked-column, hundred-percent-stacked-column, stacked-area, stacked-bar
|
||||
- Source lines: 2194-2240
|
||||
## Content
|
||||
---
|
||||
title: Total difference arrow
|
||||
@@ -49,6 +48,3 @@ spans rightward between two row ends instead of upward between two column tops.
|
||||
<polygon points="528,128 520,124 520,132" fill="[accent]"/>
|
||||
<text x="380" y="118" font-size="[fs-label]" font-weight="bold" fill="[accent]" text-anchor="middle">+25%</text>
|
||||
```
|
||||
## Source Slice
|
||||
|
||||
- Source lines: 2194-2240
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
## Context
|
||||
- Role: snippet
|
||||
- Purpose: Chart design snippet for chart types: column, bar, line, stacked-area, bubble, waterfall, combination
|
||||
- Source lines: 2241-2267
|
||||
## Content
|
||||
---
|
||||
title: Triangle pointer
|
||||
@@ -29,6 +28,3 @@ to the right of the chart's right edge).
|
||||
<!-- Pointing UP at (480, 250). Pairs with an Ellipse callout above a line point. -->
|
||||
<polygon points="480,250 472,258 488,258" fill="[accent]"/>
|
||||
```
|
||||
## Source Slice
|
||||
|
||||
- Source lines: 2241-2267
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
## Context
|
||||
- Role: snippet
|
||||
- Purpose: Chart design snippet for chart types: column, stacked-column, hundred-percent-stacked-column, bar, stacked-bar, line, indexed-line, stacked-area, scatter, bubble, waterfall, diverging-bar, small-multiples, combination, clustered-column
|
||||
- Source lines: 2268-2299
|
||||
## Content
|
||||
---
|
||||
title: Value line
|
||||
@@ -34,6 +33,3 @@ separate snippet needed.
|
||||
stroke="[context]" stroke-width="1" stroke-dasharray="4,3"/>
|
||||
<text x="904" y="324" font-size="[fs-meta]" fill="[context]">Avg 4.2</text>
|
||||
```
|
||||
## Source Slice
|
||||
|
||||
- Source lines: 2268-2299
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
## Context
|
||||
- Role: snippet
|
||||
- Purpose: Chart design snippet for chart types: stacked-column, hundred-percent-stacked-column, stacked-bar, waterfall, diverging-bar, clustered-column, marimekko
|
||||
- Source lines: 2300-2329
|
||||
## Content
|
||||
---
|
||||
title: Vertical bracket measurement
|
||||
@@ -32,6 +31,3 @@ spanning *multiple* categories; this one measures *one* column's vertical span.
|
||||
<polygon points="196,314 204,314 200,306" fill="[accent]"/>
|
||||
<text x="194" y="268" font-size="[fs-hero]" font-weight="bold" fill="[accent]" text-anchor="end">54%</text>
|
||||
```
|
||||
## Source Slice
|
||||
|
||||
- Source lines: 2300-2329
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
## Context
|
||||
- Role: snippet
|
||||
- Purpose: Chart design snippet for chart types: waterfall
|
||||
- Source lines: 2330-2353
|
||||
## Content
|
||||
---
|
||||
title: Waterfall connector
|
||||
@@ -27,6 +26,3 @@ Draw connectors **before** the bar rects (document order) so bars overlap them.
|
||||
<line x1="200" y1="224" x2="240" y2="224"
|
||||
stroke="[context]" stroke-width="1" stroke-dasharray="2,2"/>
|
||||
```
|
||||
## Source Slice
|
||||
|
||||
- Source lines: 2330-2353
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
## Context
|
||||
- Role: reference
|
||||
- Purpose: Describes generated chart outputs.
|
||||
- Source lines: 1357-1365
|
||||
## Content
|
||||
## generate_svg_chart
|
||||
|
||||
@@ -13,6 +12,3 @@
|
||||
```text
|
||||
Generate ONE SVG chart as a standalone file. Output viewBox is fixed at 960×600. Returns file_path (where the SVG was written), drive_token, and design_path (auditable design.md sidecar).
|
||||
```
|
||||
## Source Slice
|
||||
|
||||
- Source lines: 1357-1365
|
||||
|
||||
@@ -8,7 +8,3 @@ Before resolving a deck design brief, read these files in order:
|
||||
4. `../deck-design-reference-catalog.md`
|
||||
|
||||
If routing is uncertain, read `full.md`.
|
||||
|
||||
## Source Slice
|
||||
|
||||
- Source lines: 867-1080
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
## Context
|
||||
- Role: prompt
|
||||
- Purpose: Produces narrative_spine, depth, and tone as strict JSON.
|
||||
- Source lines: 1031-1080
|
||||
## Content
|
||||
内容设计 System Prompt
|
||||
|
||||
@@ -53,6 +52,3 @@ Any page count, length, or structural ask the user already specified are FIXED.
|
||||
"tone": "voice / persona / emotional register"
|
||||
}
|
||||
```
|
||||
## Source Slice
|
||||
|
||||
- Source lines: 1031-1080
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
## Context
|
||||
- Role: prompt_bundle
|
||||
- Purpose: Complete resolve_design_brief prompt bundle for audit or uncertain routing.
|
||||
- Source lines: 867-1080
|
||||
## Required Read Path
|
||||
|
||||
1. `tool-contract.md`
|
||||
@@ -45,7 +44,7 @@ The deliverable is one **Style Deconstruction** document with 7 design dimension
|
||||
## Core principles
|
||||
|
||||
1. **Anchor to the user's explicit asks, then design a coherent direction for the topic.** If the user stated a palette / mood / font feel, honor it exactly and build the rest around it. Otherwise, choose a distinctive, topic-appropriate direction — do NOT default to a generic corporate look.
|
||||
2. **Commit to ONE distinctive, deconstructable style.** Pick a clear visual language (e.g. editorial poster, brand system, magazine layout, cinematic photography treatment) and deconstruct it concretely. Avoid a vague mash-up.
|
||||
2. **Commit to ONE distinctive, deconstructable style.** Pick a clear visual language (e.g. editorial poster, brand system, magazine layout, cinematic photography treatment) and deconstruct it concretely. Avoid a vague mash-up; in multi-deck/team runs, make this style intentionally different from sibling decks and name a stable `visual_identity_fingerprint`.
|
||||
3. **Deconstruct to buildable granularity.** Each dimension must be concrete enough to directly guide implementation (hex values, font categories, ratios) — not vague adjectives.
|
||||
4. **Visual style only — never content decisions.** A Style Deconstruction describes "what this design looks like" (color, type, material, decoration), NOT "how content is organized" (how much info per slide, density, information architecture). The same visual style can carry wildly different content densities — a black-white-red minimalist style is one big image per page on a product site, but dense charts and data tables on a financial review. Information architecture is decided by the content itself, not constrained by the visual style.
|
||||
5. **Aim ABOVE the obvious default.** Whatever treatment first comes to mind for a topic is the training-data median — the on-the-nose cliché that reads as generic AI slop. Treat your first instinct as the floor to rise above, not the answer. Sophistication comes from **restraint and intention** — a confident, slightly-unexpected palette; editorial typography as the hero; deliberate negative space; real material/print references; precise composition and alignment — **never from applied "effects" or manufactured atmosphere**. Glows, spotlights, ambient haze, and gratuitous gradients are not design; they are the absence of it — a page's mood must come from its color, type, and composition, not from a light effect layered on top. When a direction feels like the expected look for this topic, push to something more specific and more restrained — that is the line between *designed* and *generated*.
|
||||
@@ -109,12 +108,12 @@ This describes visual composition technique ONLY, NOT content density or informa
|
||||
|
||||
Do NOT write density / architecture constraints in this dimension (no "one data point per slide" / "lots of whitespace"). Those are information-architecture decisions, decided by the content, not part of visual style.
|
||||
|
||||
#### Dimension 4: Imagery treatment
|
||||
#### Dimension 4: Imagery treatment and imagery_system
|
||||
- **Image type**: photo / illustration / icon / vector / 3D / chart
|
||||
- **Color treatment**: original / desaturated / monochrome / duotone
|
||||
- **Texture**: halftone / blur / grain / none
|
||||
- **Cropping**: regular crop / shaped crop / bleed / cut-out
|
||||
- **Relationship to text**: image-text separated / overlaid / image as background
|
||||
- **Relationship to text**: image-text separated / overlaid / image as background; also define an `imagery_system` with `hero_treatment`, `key_page_visual_rule`, `small_image_format_preference` (png/jpg/webp for photos, products, people, stores, screenshots, document captures), `image_text_relationship`, `mask_strategy`, and `primary_visual_do_not_use` (icons, abstract geometry, low-opacity texture, decorative patterns)
|
||||
|
||||
#### Dimension 5: Material & texture
|
||||
- **Surface quality**: clean flat / paper texture / noise / metallic / matte, etc.
|
||||
@@ -150,8 +149,8 @@ Output one Markdown document (no code fences), in the deck's language (Chinese w
|
||||
[fill per Dimension 2 — give concrete Chinese font names]
|
||||
### 3. Layout language
|
||||
[fill per Dimension 3]
|
||||
### 4. Imagery treatment
|
||||
[fill per Dimension 4]
|
||||
### 4. Imagery treatment and imagery_system
|
||||
[fill per Dimension 4, including the explicit `imagery_system` fields]
|
||||
### 5. Material & texture
|
||||
[fill per Dimension 5]
|
||||
### 6. Decoration language
|
||||
@@ -164,7 +163,7 @@ Output one Markdown document (no code fences), in the deck's language (Chinese w
|
||||
## Quality check
|
||||
|
||||
Verify all items before output:
|
||||
- [ ] All 7 dimensions filled, no gaps
|
||||
- [ ] All 7 dimensions filled, no gaps, including the explicit `imagery_system` fields under Dimension 4
|
||||
- [ ] The visual direction honors the user's explicit color / mood / font asks from the conversation (if any)
|
||||
- [ ] Color system has concrete hex values, not a vague "warm tone"
|
||||
- [ ] Typography has concrete categories (serif / sans / handwriting) + concrete Chinese font names, not "a nice font"
|
||||
@@ -224,9 +223,3 @@ Any page count, length, or structural ask the user already specified are FIXED.
|
||||
"tone": "voice / persona / emotional register"
|
||||
}
|
||||
```
|
||||
## Source Coverage
|
||||
|
||||
- Covers manifest sections: resolve_design_brief
|
||||
- Source lines: 867-1080
|
||||
- Coverage mode: extracted runtime-unit content, preserving executable prompt or contract boundaries.
|
||||
- Omitted source-only artifacts: copied implementation filenames only.
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
## Context
|
||||
- Role: prompt
|
||||
- Purpose: Produces the visual-system style deconstruction.
|
||||
- Source lines: 885-1029
|
||||
## Content
|
||||
样式设计System Prompt
|
||||
|
||||
@@ -20,7 +19,7 @@ The deliverable is one **Style Deconstruction** document with 7 design dimension
|
||||
## Core principles
|
||||
|
||||
1. **Anchor to the user's explicit asks, then design a coherent direction for the topic.** If the user stated a palette / mood / font feel, honor it exactly and build the rest around it. Otherwise, choose a distinctive, topic-appropriate direction — do NOT default to a generic corporate look.
|
||||
2. **Commit to ONE distinctive, deconstructable style.** Pick a clear visual language (e.g. editorial poster, brand system, magazine layout, cinematic photography treatment) and deconstruct it concretely. Avoid a vague mash-up.
|
||||
2. **Commit to ONE distinctive, deconstructable style.** Pick a clear visual language (e.g. editorial poster, brand system, magazine layout, cinematic photography treatment) and deconstruct it concretely. Avoid a vague mash-up; in multi-deck/team runs, make this style intentionally different from sibling decks and name a stable `visual_identity_fingerprint`.
|
||||
3. **Deconstruct to buildable granularity.** Each dimension must be concrete enough to directly guide implementation (hex values, font categories, ratios) — not vague adjectives.
|
||||
4. **Visual style only — never content decisions.** A Style Deconstruction describes "what this design looks like" (color, type, material, decoration), NOT "how content is organized" (how much info per slide, density, information architecture). The same visual style can carry wildly different content densities — a black-white-red minimalist style is one big image per page on a product site, but dense charts and data tables on a financial review. Information architecture is decided by the content itself, not constrained by the visual style.
|
||||
5. **Aim ABOVE the obvious default.** Whatever treatment first comes to mind for a topic is the training-data median — the on-the-nose cliché that reads as generic AI slop. Treat your first instinct as the floor to rise above, not the answer. Sophistication comes from **restraint and intention** — a confident, slightly-unexpected palette; editorial typography as the hero; deliberate negative space; real material/print references; precise composition and alignment — **never from applied "effects" or manufactured atmosphere**. Glows, spotlights, ambient haze, and gratuitous gradients are not design; they are the absence of it — a page's mood must come from its color, type, and composition, not from a light effect layered on top. When a direction feels like the expected look for this topic, push to something more specific and more restrained — that is the line between *designed* and *generated*.
|
||||
@@ -84,12 +83,12 @@ This describes visual composition technique ONLY, NOT content density or informa
|
||||
|
||||
Do NOT write density / architecture constraints in this dimension (no "one data point per slide" / "lots of whitespace"). Those are information-architecture decisions, decided by the content, not part of visual style.
|
||||
|
||||
#### Dimension 4: Imagery treatment
|
||||
#### Dimension 4: Imagery treatment and imagery_system
|
||||
- **Image type**: photo / illustration / icon / vector / 3D / chart
|
||||
- **Color treatment**: original / desaturated / monochrome / duotone
|
||||
- **Texture**: halftone / blur / grain / none
|
||||
- **Cropping**: regular crop / shaped crop / bleed / cut-out
|
||||
- **Relationship to text**: image-text separated / overlaid / image as background
|
||||
- **Relationship to text**: image-text separated / overlaid / image as background; also define an `imagery_system` with `hero_treatment`, `key_page_visual_rule`, `small_image_format_preference` (png/jpg/webp for photos, products, people, stores, screenshots, document captures), `image_text_relationship`, `mask_strategy`, and `primary_visual_do_not_use` (icons, abstract geometry, low-opacity texture, decorative patterns)
|
||||
|
||||
#### Dimension 5: Material & texture
|
||||
- **Surface quality**: clean flat / paper texture / noise / metallic / matte, etc.
|
||||
@@ -125,8 +124,8 @@ Output one Markdown document (no code fences), in the deck's language (Chinese w
|
||||
[fill per Dimension 2 — give concrete Chinese font names]
|
||||
### 3. Layout language
|
||||
[fill per Dimension 3]
|
||||
### 4. Imagery treatment
|
||||
[fill per Dimension 4]
|
||||
### 4. Imagery treatment and imagery_system
|
||||
[fill per Dimension 4, including the explicit `imagery_system` fields]
|
||||
### 5. Material & texture
|
||||
[fill per Dimension 5]
|
||||
### 6. Decoration language
|
||||
@@ -139,7 +138,7 @@ Output one Markdown document (no code fences), in the deck's language (Chinese w
|
||||
## Quality check
|
||||
|
||||
Verify all items before output:
|
||||
- [ ] All 7 dimensions filled, no gaps
|
||||
- [ ] All 7 dimensions filled, no gaps, including the explicit `imagery_system` fields under Dimension 4
|
||||
- [ ] The visual direction honors the user's explicit color / mood / font asks from the conversation (if any)
|
||||
- [ ] Color system has concrete hex values, not a vague "warm tone"
|
||||
- [ ] Typography has concrete categories (serif / sans / handwriting) + concrete Chinese font names, not "a nice font"
|
||||
@@ -149,6 +148,3 @@ Verify all items before output:
|
||||
- [ ] **No crossing into content decisions**: no "how much info per slide", "lots of whitespace", "single-column layout" or other density / architecture constraints. Layout language describes visual technique (alignment, zoning, borders) only, never content sparsity
|
||||
|
||||
```
|
||||
## Source Slice
|
||||
|
||||
- Source lines: 885-1029
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
## Context
|
||||
- Role: tool_contract
|
||||
- Purpose: Defines the design brief tool timing, outputs, and required inputs.
|
||||
- Source lines: 867-883
|
||||
## Content
|
||||
## resolve_design_brief
|
||||
|
||||
@@ -21,6 +20,3 @@ Inputs:
|
||||
- visual_style_query (required): an array of 1-3 short visual-direction phrases, each "<topic> + <material type / sub-direction>" (English works best), e.g. ["Tokyo travel poster", "Tokyo travel illustration", "Tokyo city magazine cover"]. Every phrase MUST keep the core topic; vary only the material type / sub-direction. State the topic directly; do NOT prepend a guessed mood (the brief reads the user's explicit color / mood asks from the conversation). Drives the visual_system.
|
||||
- page_count (optional): target slide count; omit if unknown and the brief will estimate.
|
||||
```
|
||||
## Source Slice
|
||||
|
||||
- Source lines: 867-883
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
## Context
|
||||
- Role: reference
|
||||
- Purpose: Protocol schema and design rules for SVG Slides documents.
|
||||
- Source lines: 198-865
|
||||
## Content
|
||||
# SVG reference(协议 schema + 设计规范 / svg_reference)
|
||||
|
||||
@@ -672,9 +671,3 @@ Slide with animation (private `<slide:animations>` + one `<slide:transition>` as
|
||||
</canonical_examples>
|
||||
</svg_reference>
|
||||
````
|
||||
## Source Coverage
|
||||
|
||||
- Covers manifest sections: svg_reference
|
||||
- Source lines: 198-865
|
||||
- Coverage mode: extracted runtime-unit content, preserving executable prompt or contract boundaries.
|
||||
- Omitted source-only artifacts: copied implementation filenames only.
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
## Context
|
||||
- Role: prompt
|
||||
- Purpose: Main conductor prompt for SVG Slides workflow.
|
||||
- Source lines: 3-196
|
||||
## Content
|
||||
# System prompt(编排 / mode_system_prompt_svg)
|
||||
|
||||
@@ -76,21 +75,21 @@ When you do present it, call `show_form` ONCE with `meta.form_purpose: "outline_
|
||||
Slide count rule for this outline: the proposed outline is the actual slide sequence, not a chapter list. Use the user's explicit page count when given. Otherwise, default to 8-12 slides for normal requests. Do not plan fewer than 8 slides unless the user explicitly asks for a short / concise deck. Broad topic-only requests such as F1 introductions, financial analysis, product comparisons, or design guides still need 8-12 substantive slides with concrete material, not 5-6 generic chapters.
|
||||
|
||||
### Phase 6 — Write slide_content.md
|
||||
Write a `slide_content.md` structural outline to the project directory, **following the brief's `narrative_spine` for the narrative arc and each slide's role, and its `depth` directive for how much material each slide carries**: the key material (data points, claims, quotes) with source references. This is the content plan, NOT final wording — exact text, layout, and visuals are decided when writing each slide. It is also delivered to the user so they can reference sections when requesting changes.
|
||||
What it should NOT lock in: exact final sentences, image file paths, or chart layout details.
|
||||
Write a `slide_content.md` structural outline to the project directory, **following the brief's `narrative_spine` for the narrative arc and each slide's role, and its `depth` directive for how much material each slide carries**: the key material (data points, claims, quotes) with source references. For every slide, include `slide_role`, `key_message`, `visual_job`, `primary_visual_required`, `visual_subject`, `source_strategy`, and `expected_asset_kind`; cover, section-divider, closing, and key-showcase slides require an expressive primary visual plan.
|
||||
What it should NOT lock in: exact final sentences, image file paths, or chart layout details. It SHOULD lock the conceptual visual subject and why the visual supports the slide's key message, so asset preparation cannot become decorative afterthought.
|
||||
|
||||
### Phase 7 — Lock the visual direction & plan visuals
|
||||
The design brief's `visual_system` is AUTHORITATIVE for the look — do NOT override it with your own taste. Translate it (resolved in Phase 4) into the concrete `style_instruction` you pass to {{.ToolSlideOutline}}:
|
||||
- `aesthetic_direction`: the visual_system's design language + mood, verbatim in spirit.
|
||||
- `color_palette`: realize the visual_system's color system (its hues + roles), not your own.
|
||||
- `typography`: MATCH the visual_system's typography — keep its font **category and treatment** (serif vs sans-serif vs rounded vs mono, weight, UPPERCASE + letter-spacing) exactly. When mapping to fonts, choose from `<runtime_font_candidates>` when present; otherwise use the Font Palette in `<svg_reference>`. Pick a font in the SAME category (e.g. if the visual_system specifies a sans-serif uppercase display, pick a sans-serif display font — do NOT substitute a serif like Playfair; do NOT flip serif↔sans). Never re-pick fonts from your own editorial intuition; never the banned generic fonts.
|
||||
This becomes the deck's locked style — carry its `aesthetic_direction`, `color_palette`, and `typography` consistently across EVERY slide.
|
||||
Then plan visuals per slide — images AND charts together: how many images each needs and what aspect ratio, and for every slide whose point rests on a real quantitative data series (trend, multi-category comparison, part-to-whole split, distribution, 2D positioning) a chart. These are generated as assets BEFORE slide_edit, the same as images: once {{.ToolSlideOutline}} has created the project, call {{.ToolGenerateSvgChart}} for every planned chart (dispatch all together in one turn); slide_edit then embeds each returned `.svg` by `<rect slide:role="chart" href="...">`. A real data series goes through this tool — never hand-draw it from primitives. (See <visuals> and <chart_workflow>.)
|
||||
- `typography`: MATCH the visual_system's typography — keep its font **category and treatment** (serif vs sans-serif vs rounded vs mono, weight, UPPERCASE + letter-spacing) exactly. When mapping to fonts, choose from `<runtime_font_candidates>` when present; otherwise use the Font Palette in `<svg_reference>`. Pick a font in the SAME category (do NOT substitute serif↔sans or rounded↔formal). Write `receipts/typography_plan.json` with `version`, `font_category`, `display_font`, `body_font`, `label_font`, `source`, `selection_reason`, `forbidden_substitutions`, and `runtime_candidates_used`; never re-pick fonts from your own editorial intuition or banned generic fonts.
|
||||
This becomes the deck's locked style — carry its `aesthetic_direction`, `color_palette`, `typography`, and `typography_plan` consistently across EVERY slide.
|
||||
Then plan visuals per slide — images AND charts together: how many images each needs, what aspect ratio, which named entities require web image search, and which pages are cover / section-divider / closing / key-showcase. Write `receipts/asset_readiness.json` with a deck-level `visual_identity_fingerprint`, required search entities, prepared local assets, asset `purpose` / `asset_kind` / `expressive_role` / `content_relevance` / `format`, and per-slide `slide_role` / `visual_tier` / `primary_visual_required` / `primary_visual` / `layout_family` / `text_overlay` / `mask_required` / `mask_strategy` before slide_edit; in multi-deck/team runs, compare sibling fingerprints and do not reuse the same style family + composition signature. Once {{.ToolSlideOutline}} has created the project, call {{.ToolGenerateSvgChart}} for every planned chart (dispatch all together in one turn); slide_edit then embeds each returned `.svg` by `<rect slide:role="chart" href="...">`. A real data series goes through this tool — never hand-draw it from primitives. (See <visuals> and <chart_workflow>.)
|
||||
|
||||
### Phase 8 — Generate & deliver
|
||||
1. **{{.ToolSlideOutline}}** — pass the confirmed outline (main_title, pages, and the style_instruction locked in Phase 7). Creates the project directory, `outline.json`, style, and one empty `.svg` per slide. The language of your arguments sets the slide language. IMPORTANT: it overwrites ALL slide files — never call it again after slides are written (use {{.ToolSlideOrganize}} to add/delete pages later).
|
||||
2. **{{.ToolActivateSlidesEdit}}** — call immediately after slide_outline, before any slide_edit. Pass `project_dir`. This switches to a faster model optimized for slide writing.
|
||||
3. **{{.ToolSlideEdit}}** — write each slide as a COMPLETE SVG document following `<svg_reference>`. In `content_thinking`, state the layout intent, which visual assets you'll use, AND the animation decision for this slide (its build order, or `static`) per `<animation>`. Compose freely (no canned templates). Slides display incrementally as each completes. Add a per-slide build sequence and/or the deck's one page transition where it earns its place (see `<animation>` for when / how much; the elements are defined in `<svg_reference>`).
|
||||
3. **{{.ToolSlideEdit}}** — write each slide as a COMPLETE SVG document following `<svg_reference>` only after the slide's searched/generated local assets exist and are recorded in `asset_readiness.json`, and the locked font stacks are recorded in `typography_plan.json`. In `content_thinking`, state `slide_role`, key message, primary visual path, why the visual supports the message, text-overlay / mask strategy, layout intent, visual assets, typography role, and the animation decision for this slide (its build order, or `static`) per `<animation>`. Compose freely (no canned templates). Slides display incrementally as each completes. Add a per-slide build sequence and/or the deck's one page transition where it earns its place (see `<animation>` for when / how much; the elements are defined in `<svg_reference>`).
|
||||
4. **{{.ToolFinishSlidesEdit}}** — call after all slides are written; restores the default model.
|
||||
5. **Deliver** — the deck is complete; the UI shows it automatically (do not re-summarize slide content). Share the `slide_content.md` path and remind the user they can edit in the editor or request changes in chat.
|
||||
|
||||
@@ -112,15 +111,15 @@ Modifying structure after creation: add pages via {{.ToolSlideOrganize}} "add" (
|
||||
</content_quality>
|
||||
|
||||
<visuals>
|
||||
Visuals re-engage attention and carry meaning. Plan them deliberately; don't decorate.
|
||||
Visuals re-engage attention and carry meaning. Plan them deliberately; don't decorate. Key pages need images that carry the idea or emotion, not just any asset.
|
||||
- **Image sourcing priority**:
|
||||
1. **Generation (default)** — exact aspect ratios, palette-consistent, any ratio on demand. Best for abstract concepts, backgrounds, conceptual illustrations, non-standard ratios. Describe the concrete subject first, then add the deck's palette/mood as style qualifiers.
|
||||
2. **Search** ({{.ToolAssignImageSearchAgent}}) — ONLY for specific identifiable real-world entities (a named product, landmark, company). Do not search for logos.
|
||||
2. **Search** ({{.ToolAssignImageSearchAgent}}) — REQUIRED for specific identifiable real-world entities (a named product, landmark, company, person, place, artwork, paper, or venue). Do not search for logos; record the query/outcome in `asset_readiness.json` even when search fails and generation fallback is used.
|
||||
3. **Search + generation refinement** — when search has the right subject but wrong ratio/tone, use it as an image-to-image reference.
|
||||
- NEVER crop to force a ratio — generate at the exact ratio. Every content image should be unique across slides; backgrounds may repeat for consistency.
|
||||
- NEVER crop to force a ratio — generate at the exact ratio. Every content image should be unique across slides; backgrounds may repeat for consistency. Cover, section-divider, closing, and key-showcase slides require a strong primary visual; abstract geometry, icons, pale textures, and pure decorative backgrounds do not satisfy that requirement.
|
||||
- **Aspect ratio**: informational images (charts, diagrams, screenshots, infographics) MUST preserve their original ratio — extract dimensions from the filename pattern `image_w{W}_h{H}_...` and size the SVG `<image>` to match. Decorative photos may be composed freely.
|
||||
- **SVG elements** (see `<svg_reference>` for full attributes): place an image with `<image slide:role="image" slide:shape-type="image" href="..." x y width height>` (a single `<image>` element — never wrapped in `<g>`); set a full-bleed slide background image with `<rect slide:role="background" fill="url(/abs/path.jpg)">`.
|
||||
- **Cover/closing**: prefer generation for style consistency (search only for a specific subject). Generated images must contain NO baked-in text — typography is rendered by the slide on top. Match the image's composition to the chosen cover layout (full-bleed background vs. a positioned image zone vs. no image).
|
||||
- **Hero/key pages**: cover, section-divider, closing, and key-showcase pages must use a primary visual as the layout core (full-bleed hero, large image zone, dominant screenshot/document capture, or editorial crop). Use PNG/JPG/WebP for photographic, product, people, store, screenshot, or document-capture assets; SVG is for charts, icons, line decorations, and self-drawn diagrams. If text sits on a hero image, add a concrete `slide:role="text-scrim"` or `slide:role="image-mask"` overlay, gradient mask, or safe text zone so the image has impact and the text remains readable.
|
||||
</visuals>
|
||||
|
||||
<about_slides_outline>
|
||||
@@ -198,9 +197,3 @@ When slide tools fail: retry once. If the retry also fails, consider the task fa
|
||||
</user_communication_guidelines>
|
||||
</about_slides>
|
||||
````
|
||||
## Source Coverage
|
||||
|
||||
- Covers manifest sections: system_prompt_workflow
|
||||
- Source lines: 3-196
|
||||
- Coverage mode: extracted runtime-unit content, preserving executable prompt or contract boundaries.
|
||||
- Omitted source-only artifacts: copied implementation filenames only.
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
## Context
|
||||
- Role: tool_contract
|
||||
- Purpose: Switches into the fast slide-writing mode after outline creation.
|
||||
- Source lines: 1256-1262
|
||||
## Content
|
||||
## activate_slides_edit
|
||||
|
||||
@@ -11,9 +10,3 @@
|
||||
```text
|
||||
Activate slide edit mode. Call this AFTER slide_outline and BEFORE slide_edit. This switches to a faster model optimized for writing slides. Pass project_dir.
|
||||
```
|
||||
## Source Coverage
|
||||
|
||||
- Covers manifest sections: activate_slides_edit_tool
|
||||
- Source lines: 1256-1262
|
||||
- Coverage mode: extracted runtime-unit content, preserving executable prompt or contract boundaries.
|
||||
- Omitted source-only artifacts: copied implementation filenames only.
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
## Context
|
||||
- Role: tool_contract
|
||||
- Purpose: Computes custom SVG path bounds before authoring custom shapes.
|
||||
- Source lines: 1349-1355
|
||||
## Content
|
||||
## compute_custom_shape_bbox
|
||||
|
||||
@@ -11,9 +10,3 @@ SVG 专属:算 custom path 真实包围盒。工具描述
|
||||
```text
|
||||
Compute the exact bounding box of one or more SVG custom-shape paths. You CANNOT eyeball a path's real size, so before writing any <path slide:role="shape" slide:shape-type="custom"> call this with each path's `d`. For each path it returns the true width/height, a normalized `d` (shifted to the (0,0) origin) and an (offsetX, offsetY). Author the element as: <path slide:shape-type="custom" d="<returned d>" slide:width="<width>" slide:height="<height>" transform="translate(<offsetX>,<offsetY>)" .../> — never set slide:width/slide:height to the slide/canvas size.
|
||||
```
|
||||
## Source Coverage
|
||||
|
||||
- Covers manifest sections: compute_custom_shape_bbox_tool
|
||||
- Source lines: 1349-1355
|
||||
- Coverage mode: extracted runtime-unit content, preserving executable prompt or contract boundaries.
|
||||
- Omitted source-only artifacts: copied implementation filenames only.
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
## Context
|
||||
- Role: tool_contract
|
||||
- Purpose: Leaves edit mode and checks for placeholder slides.
|
||||
- Source lines: 1333-1339
|
||||
## Content
|
||||
## finish_slides_edit
|
||||
|
||||
@@ -11,9 +10,3 @@
|
||||
```text
|
||||
Finish slide edit mode. Call this AFTER all slide_edit calls are completed. This restores the original model. The tool will verify that all slides have been edited — if any placeholder slides remain, the call will fail and you must edit them first.
|
||||
```
|
||||
## Source Coverage
|
||||
|
||||
- Covers manifest sections: finish_slides_edit_tool
|
||||
- Source lines: 1333-1339
|
||||
- Coverage mode: extracted runtime-unit content, preserving executable prompt or contract boundaries.
|
||||
- Omitted source-only artifacts: copied implementation filenames only.
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
## Context
|
||||
- Role: tool_contract
|
||||
- Purpose: Adds or deletes pages after outline creation.
|
||||
- Source lines: 1341-1347
|
||||
## Content
|
||||
## slide_organize
|
||||
|
||||
@@ -11,9 +10,3 @@
|
||||
```text
|
||||
Add or delete slide pages in an existing presentation project. Use this instead of calling slide_outline again when you need to modify the page structure after the initial outline is created. Operations are executed in order.
|
||||
```
|
||||
## Source Coverage
|
||||
|
||||
- Covers manifest sections: slide_organize_tool
|
||||
- Source lines: 1341-1347
|
||||
- Coverage mode: extracted runtime-unit content, preserving executable prompt or contract boundaries.
|
||||
- Omitted source-only artifacts: copied implementation filenames only.
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
## Context
|
||||
- Role: tool_contract
|
||||
- Purpose: Creates project structure, outline metadata, style settings, and empty slide files.
|
||||
- Source lines: 1236-1254
|
||||
## Content
|
||||
## slide_outline
|
||||
|
||||
@@ -23,9 +22,3 @@ Create the project structure with outline metadata, style settings, and empty sl
|
||||
- Use to define the presentation structure and style before writing individual slides
|
||||
</recommended_usage>
|
||||
```
|
||||
## Source Coverage
|
||||
|
||||
- Covers manifest sections: slide_outline_tool
|
||||
- Source lines: 1236-1254
|
||||
- Coverage mode: extracted runtime-unit content, preserving executable prompt or contract boundaries.
|
||||
- Omitted source-only artifacts: copied implementation filenames only.
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
## Context
|
||||
- Role: tool_contract
|
||||
- Purpose: Converts uploaded PPTX files into editable slides format.
|
||||
- Source lines: 2358-2395
|
||||
## Content
|
||||
## slides_convert
|
||||
|
||||
@@ -42,9 +41,3 @@ IMPORTANT:
|
||||
- The original PPTX content is preserved exactly in the converted .slides file
|
||||
- If user wants to use PPTX as a STYLE REFERENCE for new slides, use slides_parse_template instead
|
||||
```
|
||||
## Source Coverage
|
||||
|
||||
- Covers manifest sections: slides_convert_tool
|
||||
- Source lines: 2358-2395
|
||||
- Coverage mode: extracted runtime-unit content, preserving executable prompt or contract boundaries.
|
||||
- Omitted source-only artifacts: copied implementation filenames only.
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
## Context
|
||||
- Role: tool_contract
|
||||
- Purpose: Writes SVG page documents and carries slide-edit hard gates.
|
||||
- Source lines: 1264-1331
|
||||
## Content
|
||||
## slides_edit
|
||||
|
||||
@@ -31,7 +30,7 @@ A text-only slide with decorative shapes signals skipped preparation. It is rare
|
||||
|
||||
## Image usage
|
||||
- NEVER reference a non-existent or non-local image. Always use absolute paths for images, fonts, and other resources.
|
||||
- Use ONLY the image path(s) prepared for the slide (to avoid duplicates). Place content images with concrete subjects (UI mockups, illustrations) as `<image slide:role="shape" slide:shape-type="image">` in a split or side layout — do not fade them to low opacity and use as full-screen backgrounds, which creates ghost-like visual noise behind foreground content.
|
||||
- Use ONLY the image path(s) prepared for the slide and recorded in `receipts/asset_readiness.json` (to avoid duplicates), including any declared `primary_visual`. Place content images with concrete subjects (photos, products, people, stores, screenshots, document captures, UI mockups, illustrations) as `<image slide:role="image" slide:shape-type="image">`; small photographic assets should be PNG/JPG/WebP. SVG is for charts, icons, line decorations, and self-drawn diagrams, not a substitute for searched/generated bitmap imagery.
|
||||
|
||||
## Incremental processing
|
||||
- Slides are written and displayed as soon as each one is complete.
|
||||
@@ -42,7 +41,7 @@ A text-only slide with decorative shapes signals skipped preparation. It is rare
|
||||
<layout_and_design>
|
||||
Compose every slide's layout from scratch to fit its specific content and the deck's aesthetic direction — follow "Design Thinking", "Aesthetic Guidelines", and "Layout Freedom" in `<svg_reference>`. Do NOT rotate through a fixed menu of canned patterns, and do NOT apply formulaic "diagram" templates — that produces the template-stamped feel we are avoiding. Favor unexpected, asymmetric, content-specific composition: overlap, diagonal flow, grid-breaking elements, a single dominant hero element, generous negative space. Vary the structural arrangement between adjacent slides while keeping the deck's background, card-surface style, and decoration density CONSTANT across the whole deck.
|
||||
|
||||
Use visual elements (shapes, lines, icons, accent bars, gradients) to break up text and build hierarchy; apply the accent color sparingly for emphasis; maintain white space and contrast. When text sits on a background image, overlay the image with a semi-transparent shape first so the text stays readable.
|
||||
Use visual elements (shapes, lines, icons, accent bars, gradients) to break up text and build hierarchy; apply the accent color sparingly for emphasis; maintain white space and contrast. When text sits on a background image, add a concrete readable overlay (`slide:role="text-scrim"`), image mask (`slide:role="image-mask"`), gradient, or safe text zone first so the text stays readable.
|
||||
</layout_and_design>
|
||||
|
||||
<prohibited_practices>
|
||||
@@ -60,7 +59,7 @@ Use visual elements (shapes, lines, icons, accent bars, gradients) to break up t
|
||||
|
||||
<thinking_process_instructions>
|
||||
Before writing the SVG, use the `content_thinking` parameter to document:
|
||||
1. **Visual assets**: which images/charts you will use (list file paths). If none are available, the slide is missing preparation — go prepare visuals first.
|
||||
1. **Visual assets**: the slide_role, primary visual path when required, which images/charts you will use (list file paths and the asset_readiness entry), why the primary visual supports the key message, and whether text overlays require a mask/scrim. If none are available, the slide is missing preparation — go search/generate/download visuals first.
|
||||
2. **Layout**: what composition best fits this content, and how it differs from adjacent slides.
|
||||
3. **Key message**: the ONE takeaway, and how typography and spacing emphasize it.
|
||||
4. **Data visualization**: can any content be shown as a chart or large stat number instead of text?
|
||||
@@ -72,9 +71,3 @@ Before writing the SVG, use the `content_thinking` parameter to document:
|
||||
- Stay consistent with the style_instruction provided in slide_outline.
|
||||
</quality_standards>
|
||||
```
|
||||
## Source Coverage
|
||||
|
||||
- Covers manifest sections: slides_edit_tool, svg_document_rules, image_usage, incremental_processing
|
||||
- Source lines: 1264-1331
|
||||
- Coverage mode: extracted runtime-unit content, preserving executable prompt or contract boundaries.
|
||||
- Omitted source-only artifacts: copied implementation filenames only.
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
## Context
|
||||
- Role: tool_contract
|
||||
- Purpose: Parses and preprocesses XML templates for template-based generation.
|
||||
- Source lines: 2397-2420
|
||||
## Content
|
||||
## slides_parse_template
|
||||
|
||||
@@ -28,9 +27,3 @@ OUTPUT:
|
||||
|
||||
After parsing, read tmpl.xml to understand the template's layout patterns, then replicate those layouts when writing SML with slide_edit.
|
||||
```
|
||||
## Source Coverage
|
||||
|
||||
- Covers manifest sections: slides_parse_template_tool
|
||||
- Source lines: 2397-2420
|
||||
- Coverage mode: extracted runtime-unit content, preserving executable prompt or contract boundaries.
|
||||
- Omitted source-only artifacts: copied implementation filenames only.
|
||||
|
||||
@@ -8,8 +8,7 @@ Use this directory for local validation and provenance audit routing.
|
||||
- Protocol validation script: `../../../scripts/validate_svg_deck.mjs`
|
||||
- Bundle manifest script: `../../../scripts/svg_slides_bundle.mjs`
|
||||
- Browser text bounds script: `../../../scripts/svg_slides_browser_text_bounds.mjs`
|
||||
- Asset readiness script: `../../../scripts/svg_slides_asset_readiness.mjs`
|
||||
- Visual identity comparison script: `../../../scripts/svg_slides_visual_identity_check.mjs`
|
||||
- Typography plan script: `../../../scripts/svg_slides_typography_plan.mjs`
|
||||
- Prompt read trace script: `../../../scripts/svg_slides_prompt_trace.mjs`
|
||||
|
||||
## Source Slice
|
||||
|
||||
- Source lines: derived from local validation scripts, not a manifest section
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
# Asset Readiness
|
||||
|
||||
Every generated SVG Slides bundle must write `receipts/asset_readiness.json` before `manifest.json` may set `publish_ready=true`.
|
||||
|
||||
The receipt proves that visual assets were planned and prepared before slide authoring. It closes two failure modes:
|
||||
|
||||
- slides that pass SVG protocol validation but contain only decorative shapes instead of searched or generated visuals
|
||||
- multiple decks in one run that reuse the same generic visual system and layout vocabulary
|
||||
|
||||
## Required Receipt
|
||||
|
||||
```json
|
||||
{
|
||||
"version": "svg-slides.asset-readiness.v1",
|
||||
"visual_identity_fingerprint": {
|
||||
"style_family": "technical-paper-terminal",
|
||||
"composition_signature": "asymmetric-code-panels",
|
||||
"imagery_treatment": "paper screenshots and architecture diagrams",
|
||||
"palette_signature": "navy-cyan-orange"
|
||||
},
|
||||
"required_search_entities": ["Named company or product when applicable"],
|
||||
"assets": [
|
||||
{
|
||||
"slide": "slides/slide_01.svg",
|
||||
"purpose": "primary hero visual",
|
||||
"asset_kind": "photograph | product_image | person_image | storefront | screenshot | document_capture | illustration | chart | icon | decoration",
|
||||
"expressive_role": "what emotion, proof, or narrative function this asset carries",
|
||||
"content_relevance": "why this exact visual belongs to this slide's key message",
|
||||
"format": "png",
|
||||
"source_type": "web_search",
|
||||
"status": "ready",
|
||||
"query": "specific query used",
|
||||
"source_url": "https://example.com/source",
|
||||
"local_path": "/absolute/path/to/local/image.png",
|
||||
"assignment_reason": "why this asset belongs on this slide"
|
||||
}
|
||||
],
|
||||
"slides": [
|
||||
{
|
||||
"file": "slides/slide_01.svg",
|
||||
"slide_role": "cover | section-divider | content | key-showcase | closing",
|
||||
"visual_tier": "hero | supporting | chart | none",
|
||||
"primary_visual_required": true,
|
||||
"primary_visual": "/absolute/path/to/local/image.png",
|
||||
"layout_family": "hero-image-left-code-right",
|
||||
"text_overlay": true,
|
||||
"mask_required": true,
|
||||
"mask_strategy": "dark-gradient-left",
|
||||
"visual_assets": ["/absolute/path/to/local/image.png"]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Rules
|
||||
|
||||
- For specific real-world entities (named companies, products, people, places, venues, artworks, or papers), record them in `required_search_entities` and use `source_type: "web_search"` evidence for each entity.
|
||||
- Search failures must still be explicit assets with `status: "not_found"`, `query`, and `failure_reason`; then use `generated` fallback assets where appropriate.
|
||||
- Abstract concepts can use `generated` assets, but they still require a concrete subject, local file path, and assignment reason.
|
||||
- Every slide must declare `slide_role` and `layout_family`. `cover`, `section-divider`, `closing`, and `key-showcase` slides must set `primary_visual_required: true` and provide a `primary_visual`.
|
||||
- Primary/hero assets must declare `asset_kind`, `expressive_role`, `content_relevance`, and `format`; charts, icons, decorative patterns, textures, and abstract geometry do not satisfy a primary visual requirement.
|
||||
- Photographic/product/person/storefront/screenshot/document-capture assets should use PNG/JPG/JPEG/WebP. SVG is allowed for those only with `format_exception_reason`.
|
||||
- If slide text overlays a hero image (`text_overlay: true` and `mask_required: true`), the slide SVG must include a readable mask/scrim/safe-zone structure such as `slide:role="text-scrim"`, `slide:role="image-mask"`, a gradient overlay, or a semi-transparent overlay rect.
|
||||
- Every `ready` visual asset must have an existing local path, be assigned to a slide's `visual_assets`, and be embedded by that slide SVG. Remote-only URLs are not publish-ready.
|
||||
- Every slide must declare a `layout_family`; multi-deck/team runs must compare `visual_identity_fingerprint` and layout-family overlap with `../../../scripts/svg_slides_visual_identity_check.mjs`.
|
||||
|
||||
## Validation Commands
|
||||
|
||||
```bash
|
||||
node skills/lark-slides/scripts/svg_slides_asset_readiness.mjs <deck-dir>
|
||||
node skills/lark-slides/scripts/svg_slides_visual_identity_check.mjs <deck-a> <deck-b> [<deck-c>...]
|
||||
```
|
||||
@@ -17,7 +17,6 @@ The receipt proves the generator read the required prompt and reference Markdown
|
||||
"path": "system-prompt.md",
|
||||
"role": "prompt",
|
||||
"reason": "main workflow",
|
||||
"source_lines": "3-196",
|
||||
"sha256": "..."
|
||||
}
|
||||
],
|
||||
@@ -32,7 +31,3 @@ The receipt proves the generator read the required prompt and reference Markdown
|
||||
- Missing required reads fail validation.
|
||||
- A bundle without `receipts/prompt_read_trace.json` must not be marked `publish_ready=true`.
|
||||
- If chart snippet routing is uncertain, record `generate-svg-chart/full.md` in `fallbacks` and in `read_order`.
|
||||
|
||||
## Source Slice
|
||||
|
||||
- Source lines: generated validation contract for prompt read provenance
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
# Source Coverage Audit
|
||||
# Runtime Reference Coverage Audit
|
||||
|
||||
Coverage is declared by `source/split-manifest.json`. Each manifest section must be covered by the exact target Markdown path named in its `target` field.
|
||||
Coverage is a maintainer audit, not deck-generation context.
|
||||
|
||||
The split manifest lives under `skills/lark-slides/prompt-sources/svg-slides/`. Runtime agents should not read that directory while generating decks.
|
||||
|
||||
Rules:
|
||||
|
||||
- Scan Markdown files recursively under `skills/lark-slides/references/svg-slides`.
|
||||
- Ignore `source/` because it is provenance input, not a coverage target.
|
||||
- Each covered section id must appear in exactly one target file.
|
||||
- Each manifest section's target file must exist in the runtime reference tree.
|
||||
- Runtime Markdown must not expose prompt-source paths, source-line metadata, or source-coverage blocks.
|
||||
- `generate-svg-chart/snippets/` must contain exactly 22 snippet Markdown files excluding `README.md`.
|
||||
- Component files may use `## Source Slice`; coverage authority remains the target file's `## Source Coverage` block.
|
||||
|
||||
## Source Slice
|
||||
|
||||
- Source lines: coverage policy for the extracted tree
|
||||
- The source manifest remains the authority for maintainer coverage.
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
# Typography Plan
|
||||
|
||||
Every generated SVG Slides bundle must write `receipts/typography_plan.json` before `manifest.json` may set `publish_ready=true`.
|
||||
|
||||
The receipt makes deck-level font choice explicit. It prevents a generator from silently substituting fonts during slide authoring after the design brief chose a visual direction.
|
||||
|
||||
## Required Receipt
|
||||
|
||||
```json
|
||||
{
|
||||
"version": "svg-slides.typography-plan.v1",
|
||||
"font_category": "clean sans display and body",
|
||||
"display_font": "DM Sans, 黑体, sans-serif",
|
||||
"body_font": "DM Sans, 黑体, sans-serif",
|
||||
"label_font": "DM Sans, 黑体, sans-serif",
|
||||
"source": "visual_system.typography",
|
||||
"selection_reason": "Why this category and stack match the deck's topic, audience, and tone.",
|
||||
"forbidden_substitutions": ["serif display for a sans visual system", "calligraphy body"],
|
||||
"runtime_candidates_used": true
|
||||
}
|
||||
```
|
||||
|
||||
## Rules
|
||||
|
||||
- The design brief's `visual_system.typography` is authoritative. Keep its category and treatment: serif vs sans-serif vs rounded vs mono, weight, case, and letter spacing.
|
||||
- Select explicit `display_font`, `body_font`, and `label_font` stacks before slide authoring. Use `<runtime_font_candidates>` when provided; otherwise use the Font Palette in `svg-reference.md`.
|
||||
- Each stack must start with a Latin font and end with a generic fallback such as `serif`, `sans-serif`, `monospace`, or `cursive`.
|
||||
- Body text must not use calligraphy, cursive, handwriting, or decorative script fonts.
|
||||
- `forbidden_substitutions` must name the tempting but wrong swaps for this deck, such as flipping serif and sans-serif or replacing a technical mono treatment with a cute rounded font.
|
||||
- All slide SVG text must use only the planned display/body/label stacks. If a new stack is needed, update `typography_plan.json` first and keep the category rationale explicit.
|
||||
|
||||
## Validation Command
|
||||
|
||||
```bash
|
||||
node skills/lark-slides/scripts/svg_slides_typography_plan.mjs <deck-dir>
|
||||
```
|
||||
284
skills/lark-slides/scripts/svg_slides_asset_readiness.mjs
Normal file
284
skills/lark-slides/scripts/svg_slides_asset_readiness.mjs
Normal file
@@ -0,0 +1,284 @@
|
||||
#!/usr/bin/env node
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
function fail(message, code = 2) {
|
||||
console.error(message);
|
||||
process.exit(code);
|
||||
}
|
||||
|
||||
function readJson(filePath) {
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
||||
} catch (error) {
|
||||
fail(`Invalid asset readiness JSON: ${error.message}`, 1);
|
||||
}
|
||||
}
|
||||
|
||||
function stringValue(value) {
|
||||
return typeof value === "string" ? value.trim() : "";
|
||||
}
|
||||
|
||||
function normalize(value) {
|
||||
return stringValue(value).toLowerCase();
|
||||
}
|
||||
|
||||
function pathExists(filePath) {
|
||||
return fs.existsSync(path.resolve(filePath));
|
||||
}
|
||||
|
||||
function resolveDeckPath(root, relOrAbs) {
|
||||
if (path.isAbsolute(relOrAbs)) return relOrAbs;
|
||||
return path.join(root, relOrAbs);
|
||||
}
|
||||
|
||||
function slideContainsAsset(slideSource, root, localPath) {
|
||||
const abs = path.resolve(localPath);
|
||||
const rel = path.relative(root, abs).split(path.sep).join("/");
|
||||
return slideSource.includes(localPath) || slideSource.includes(abs) || (rel && slideSource.includes(rel));
|
||||
}
|
||||
|
||||
function assetFormat(asset) {
|
||||
const explicit = normalize(asset.format);
|
||||
if (explicit) return explicit.replace(/^\./, "");
|
||||
const localPath = stringValue(asset.local_path);
|
||||
const extension = localPath ? path.extname(localPath).replace(/^\./, "") : "";
|
||||
return extension.toLowerCase();
|
||||
}
|
||||
|
||||
function slideHasReadableImageMask(slideSource) {
|
||||
return /slide:role=["'](?:image-mask|text-scrim|text-safe-zone)["']/i.test(slideSource)
|
||||
|| /<linearGradient\b/i.test(slideSource)
|
||||
|| /<rect\b[^>]+fill=["']rgba\([^)]*,\s*0\.[2-9][0-9]*\)["']/i.test(slideSource)
|
||||
|| /<rect\b[^>]+fill-opacity=["']0\.[2-9][0-9]*["']/i.test(slideSource)
|
||||
|| /<rect\b[^>]+opacity=["']0\.[2-9][0-9]*["']/i.test(slideSource);
|
||||
}
|
||||
|
||||
function assetMentionsEntity(asset, entity) {
|
||||
const needle = normalize(entity);
|
||||
if (!needle) return false;
|
||||
const haystack = [
|
||||
asset.entity,
|
||||
...(Array.isArray(asset.entities) ? asset.entities : []),
|
||||
asset.subject,
|
||||
asset.purpose,
|
||||
asset.query,
|
||||
asset.source_url,
|
||||
asset.local_path
|
||||
].map(normalize).join(" ");
|
||||
return haystack.includes(needle);
|
||||
}
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const deckArg = args.find((arg) => !arg.startsWith("--"));
|
||||
const json = args.includes("--json");
|
||||
if (!deckArg) {
|
||||
fail("Usage: node skills/lark-slides/scripts/svg_slides_asset_readiness.mjs <deck-dir> [--json]");
|
||||
}
|
||||
|
||||
const root = path.resolve(deckArg);
|
||||
const receiptPath = path.join(root, "receipts", "asset_readiness.json");
|
||||
if (!fs.existsSync(receiptPath)) {
|
||||
fail(`Missing asset readiness receipt: ${receiptPath}`, 1);
|
||||
}
|
||||
|
||||
const receipt = readJson(receiptPath);
|
||||
const errors = [];
|
||||
if (receipt.version !== "svg-slides.asset-readiness.v1") {
|
||||
errors.push(`invalid asset readiness version: ${receipt.version}`);
|
||||
}
|
||||
|
||||
const identity = receipt.visual_identity_fingerprint || {};
|
||||
for (const field of ["style_family", "composition_signature", "imagery_treatment", "palette_signature"]) {
|
||||
if (!stringValue(identity[field])) {
|
||||
errors.push(`visual_identity_fingerprint.${field} is required`);
|
||||
}
|
||||
}
|
||||
|
||||
const assets = Array.isArray(receipt.assets) ? receipt.assets : [];
|
||||
if (assets.length === 0) {
|
||||
errors.push("asset readiness must list at least one prepared visual asset");
|
||||
}
|
||||
|
||||
const allowedTypes = new Set(["web_search", "generated", "user_provided", "provided", "chart", "screenshot"]);
|
||||
const allowedStatuses = new Set(["ready", "not_found", "skipped_with_reason"]);
|
||||
const photographicAssetKinds = new Set([
|
||||
"photograph",
|
||||
"photo",
|
||||
"product_image",
|
||||
"person_image",
|
||||
"storefront",
|
||||
"store_image",
|
||||
"scene_photo",
|
||||
"screenshot",
|
||||
"document_capture"
|
||||
]);
|
||||
const rasterFormats = new Set(["png", "jpg", "jpeg", "webp"]);
|
||||
const bannedPrimaryVisualKinds = new Set(["icon", "chart", "decoration", "abstract_geometry", "texture", "pattern"]);
|
||||
const assetsByResolvedPath = new Map();
|
||||
for (const [index, asset] of assets.entries()) {
|
||||
const label = `asset #${index + 1}`;
|
||||
if (!stringValue(asset.slide)) errors.push(`${label} missing slide`);
|
||||
if (!stringValue(asset.purpose)) errors.push(`${label} missing purpose`);
|
||||
if (!allowedTypes.has(asset.source_type)) {
|
||||
errors.push(`${label} has invalid source_type: ${asset.source_type}`);
|
||||
}
|
||||
if (!allowedStatuses.has(asset.status)) {
|
||||
errors.push(`${label} has invalid status: ${asset.status}`);
|
||||
}
|
||||
if (!stringValue(asset.assignment_reason)) {
|
||||
errors.push(`${label} missing assignment_reason`);
|
||||
}
|
||||
if (asset.source_type === "web_search" && !stringValue(asset.query)) {
|
||||
errors.push(`${label} web_search asset missing query`);
|
||||
}
|
||||
if (asset.status === "ready") {
|
||||
if (!stringValue(asset.local_path)) {
|
||||
errors.push(`${label} ready asset missing local_path`);
|
||||
} else if (!pathExists(asset.local_path)) {
|
||||
errors.push(`${label} asset local_path does not exist: ${asset.local_path}`);
|
||||
} else {
|
||||
assetsByResolvedPath.set(path.resolve(asset.local_path), { asset, label });
|
||||
}
|
||||
if (asset.source_type === "web_search" && !stringValue(asset.source_url)) {
|
||||
errors.push(`${label} web_search ready asset missing source_url`);
|
||||
}
|
||||
const purpose = normalize(asset.purpose);
|
||||
const kind = normalize(asset.asset_kind || asset.kind);
|
||||
const isPrimaryOrHero = purpose.includes("primary") || purpose.includes("hero");
|
||||
if (isPrimaryOrHero) {
|
||||
if (!stringValue(asset.expressive_role)) {
|
||||
errors.push(`${label} primary/hero asset missing expressive_role`);
|
||||
}
|
||||
if (!stringValue(asset.content_relevance)) {
|
||||
errors.push(`${label} primary/hero asset missing content_relevance`);
|
||||
}
|
||||
if (asset.source_type === "chart" || bannedPrimaryVisualKinds.has(kind)) {
|
||||
errors.push(`${label} primary/hero visual cannot be a chart, icon, decoration, texture, pattern, or abstract geometry`);
|
||||
}
|
||||
}
|
||||
if (photographicAssetKinds.has(kind) && assetFormat(asset) === "svg" && !stringValue(asset.format_exception_reason)) {
|
||||
errors.push(`${label} photographic asset should use png, jpg, jpeg, or webp unless format_exception_reason is provided`);
|
||||
}
|
||||
if (photographicAssetKinds.has(kind) && assetFormat(asset) && !rasterFormats.has(assetFormat(asset)) && assetFormat(asset) !== "svg") {
|
||||
errors.push(`${label} photographic asset has unsupported raster format: ${assetFormat(asset)}`);
|
||||
}
|
||||
}
|
||||
if (asset.status === "not_found" && !stringValue(asset.failure_reason)) {
|
||||
errors.push(`${label} not_found asset missing failure_reason`);
|
||||
}
|
||||
}
|
||||
|
||||
const requiredEntities = Array.isArray(receipt.required_search_entities)
|
||||
? receipt.required_search_entities.map(stringValue).filter(Boolean)
|
||||
: [];
|
||||
const missingSearch = requiredEntities.filter((entity) => !assets.some((asset) => (
|
||||
asset.source_type === "web_search"
|
||||
&& stringValue(asset.query)
|
||||
&& assetMentionsEntity(asset, entity)
|
||||
)));
|
||||
if (missingSearch.length > 0) {
|
||||
errors.push(`missing web search evidence for required entities: ${missingSearch.join(", ")}`);
|
||||
}
|
||||
|
||||
const slides = Array.isArray(receipt.slides) ? receipt.slides : [];
|
||||
const assignedVisualAssets = new Set();
|
||||
const allowedSlideRoles = new Set(["cover", "section-divider", "content", "key-showcase", "closing"]);
|
||||
const primaryVisualRoles = new Set(["cover", "section-divider", "closing", "key-showcase"]);
|
||||
if (slides.length === 0) {
|
||||
errors.push("asset readiness must list slide-level visual planning");
|
||||
}
|
||||
for (const [index, slide] of slides.entries()) {
|
||||
const label = `slide #${index + 1}`;
|
||||
if (!stringValue(slide.file)) errors.push(`${label} missing file`);
|
||||
const slideRole = normalize(slide.slide_role);
|
||||
if (!slideRole) {
|
||||
errors.push(`${label} missing slide_role`);
|
||||
} else if (!allowedSlideRoles.has(slideRole)) {
|
||||
errors.push(`${label} invalid slide_role: ${slide.slide_role}`);
|
||||
}
|
||||
if (!stringValue(slide.layout_family)) errors.push(`${label} missing layout_family`);
|
||||
const slidePath = stringValue(slide.file) ? resolveDeckPath(root, slide.file) : "";
|
||||
const slideExists = Boolean(slidePath) && fs.existsSync(slidePath);
|
||||
const slideSource = slideExists ? fs.readFileSync(slidePath, "utf8") : "";
|
||||
if (slidePath && !slideExists) {
|
||||
errors.push(`${label} slide file does not exist: ${slide.file}`);
|
||||
}
|
||||
if (!Array.isArray(slide.visual_assets)) {
|
||||
errors.push(`${label} visual_assets must be an array`);
|
||||
continue;
|
||||
}
|
||||
for (const localPath of slide.visual_assets) {
|
||||
if (stringValue(localPath) && !pathExists(localPath)) {
|
||||
errors.push(`${label} visual asset path does not exist: ${localPath}`);
|
||||
} else if (stringValue(localPath) && slideExists && !slideContainsAsset(slideSource, root, localPath)) {
|
||||
errors.push(`${label} visual asset is not embedded in slide SVG: ${localPath}`);
|
||||
}
|
||||
if (stringValue(localPath)) {
|
||||
assignedVisualAssets.add(path.resolve(localPath));
|
||||
}
|
||||
}
|
||||
const requiresPrimaryVisual = primaryVisualRoles.has(slideRole) || slide.primary_visual_required === true;
|
||||
const primaryVisual = stringValue(slide.primary_visual);
|
||||
if (primaryVisualRoles.has(slideRole) && slide.primary_visual_required !== true) {
|
||||
errors.push(`${label} ${slideRole} must set primary_visual_required=true`);
|
||||
}
|
||||
if (requiresPrimaryVisual && !primaryVisual) {
|
||||
errors.push(`${label} primary_visual is required for ${slideRole || "declared primary-visual slide"}`);
|
||||
}
|
||||
if (primaryVisual) {
|
||||
const resolvedPrimary = path.resolve(primaryVisual);
|
||||
if (!pathExists(primaryVisual)) {
|
||||
errors.push(`${label} primary_visual path does not exist: ${primaryVisual}`);
|
||||
}
|
||||
if (Array.isArray(slide.visual_assets) && !slide.visual_assets.some((localPath) => path.resolve(stringValue(localPath)) === resolvedPrimary)) {
|
||||
errors.push(`${label} primary_visual must also appear in visual_assets: ${primaryVisual}`);
|
||||
}
|
||||
if (slideExists && !slideContainsAsset(slideSource, root, primaryVisual)) {
|
||||
errors.push(`${label} primary_visual is not embedded in slide SVG: ${primaryVisual}`);
|
||||
}
|
||||
const primaryAsset = assetsByResolvedPath.get(resolvedPrimary);
|
||||
if (primaryAsset) {
|
||||
const purpose = normalize(primaryAsset.asset.purpose);
|
||||
const kind = normalize(primaryAsset.asset.asset_kind || primaryAsset.asset.kind);
|
||||
if (!purpose.includes("primary") && !purpose.includes("hero")) {
|
||||
errors.push(`${primaryAsset.label} used as primary_visual but purpose is not primary/hero`);
|
||||
}
|
||||
if (primaryAsset.asset.source_type === "chart" || bannedPrimaryVisualKinds.has(kind)) {
|
||||
errors.push(`${primaryAsset.label} used as primary_visual cannot be a chart, icon, decoration, texture, pattern, or abstract geometry`);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (slide.text_overlay === true && slide.mask_required === true) {
|
||||
if (!stringValue(slide.mask_strategy)) {
|
||||
errors.push(`${label} text overlay requires mask_strategy`);
|
||||
}
|
||||
if (slideExists && !slideHasReadableImageMask(slideSource)) {
|
||||
errors.push(`${label} text overlay requires a readable image mask or text scrim in the SVG`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const [index, asset] of assets.entries()) {
|
||||
if (asset.status !== "ready" || !stringValue(asset.local_path)) continue;
|
||||
if (!assignedVisualAssets.has(path.resolve(asset.local_path))) {
|
||||
errors.push(`asset #${index + 1} ready asset is not assigned to any slide: ${asset.local_path}`);
|
||||
}
|
||||
}
|
||||
|
||||
const report = {
|
||||
ok: errors.length === 0,
|
||||
receipt: receiptPath,
|
||||
assetCount: assets.length,
|
||||
slideCount: slides.length,
|
||||
requiredSearchEntities: requiredEntities,
|
||||
errors
|
||||
};
|
||||
|
||||
if (errors.length > 0) {
|
||||
if (json) console.error(JSON.stringify(report, null, 2));
|
||||
else for (const error of errors) console.error(error);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(JSON.stringify(report, null, 2));
|
||||
301
skills/lark-slides/scripts/svg_slides_asset_readiness_test.mjs
Normal file
301
skills/lark-slides/scripts/svg_slides_asset_readiness_test.mjs
Normal file
@@ -0,0 +1,301 @@
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import test from "node:test";
|
||||
|
||||
const script = path.resolve("skills/lark-slides/scripts/svg_slides_asset_readiness.mjs");
|
||||
|
||||
function tempDeck() {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "svg-slides-assets-"));
|
||||
fs.mkdirSync(path.join(root, "slides"), { recursive: true });
|
||||
fs.mkdirSync(path.join(root, "resources", "images"), { recursive: true });
|
||||
const imagePath = path.join(root, "resources", "images", "hero.png");
|
||||
fs.writeFileSync(imagePath, "fake image bytes");
|
||||
fs.writeFileSync(path.join(root, "slides", "slide_01.svg"), `<svg xmlns="http://www.w3.org/2000/svg" xmlns:slide="https://slides.bytedance.com/ns" slide:role="slide" id="slide_01" viewBox="0 0 960 540">
|
||||
<rect slide:role="background" x="0" y="0" width="960" height="540" fill="rgba(255,255,255,1)"/>
|
||||
<image slide:role="image" slide:shape-type="image" href="${imagePath}" x="80" y="80" width="320" height="180"/>
|
||||
</svg>`);
|
||||
return root;
|
||||
}
|
||||
|
||||
function writeReceipt(root, receipt) {
|
||||
fs.mkdirSync(path.join(root, "receipts"), { recursive: true });
|
||||
fs.writeFileSync(path.join(root, "receipts", "asset_readiness.json"), `${JSON.stringify(receipt, null, 2)}\n`);
|
||||
}
|
||||
|
||||
function validReceipt(root, overrides = {}) {
|
||||
const imagePath = path.join(root, "resources", "images", "hero.png");
|
||||
return {
|
||||
version: "svg-slides.asset-readiness.v1",
|
||||
visual_identity_fingerprint: {
|
||||
style_family: "technical-paper-terminal",
|
||||
composition_signature: "asymmetric-code-panels",
|
||||
imagery_treatment: "searched paper screenshot plus generated diagrams",
|
||||
palette_signature: "navy-cyan-orange"
|
||||
},
|
||||
required_search_entities: [],
|
||||
assets: [
|
||||
{
|
||||
slide: "slides/slide_01.svg",
|
||||
purpose: "primary hero visual",
|
||||
asset_kind: "illustration",
|
||||
expressive_role: "sets the deck's central technical mood",
|
||||
content_relevance: "visualizes the abstract technical concept discussed on the cover",
|
||||
format: "png",
|
||||
source_type: "generated",
|
||||
status: "ready",
|
||||
local_path: imagePath,
|
||||
assignment_reason: "abstract technical concept needs a concrete visual anchor"
|
||||
}
|
||||
],
|
||||
slides: [
|
||||
{
|
||||
file: "slides/slide_01.svg",
|
||||
slide_role: "cover",
|
||||
visual_tier: "hero",
|
||||
primary_visual_required: true,
|
||||
primary_visual: imagePath,
|
||||
layout_family: "hero-image-left-code-right",
|
||||
text_overlay: false,
|
||||
mask_required: false,
|
||||
visual_assets: [imagePath]
|
||||
}
|
||||
],
|
||||
...overrides
|
||||
};
|
||||
}
|
||||
|
||||
function run(root) {
|
||||
return spawnSync("node", [script, root], { encoding: "utf8" });
|
||||
}
|
||||
|
||||
test("asset readiness accepts a generated-image deck with visual identity", () => {
|
||||
const root = tempDeck();
|
||||
writeReceipt(root, validReceipt(root));
|
||||
const result = run(root);
|
||||
assert.equal(result.status, 0, result.stderr || result.stdout);
|
||||
const report = JSON.parse(result.stdout);
|
||||
assert.equal(report.ok, true);
|
||||
assert.equal(report.assetCount, 1);
|
||||
});
|
||||
|
||||
test("asset readiness rejects missing receipt", () => {
|
||||
const root = tempDeck();
|
||||
const result = run(root);
|
||||
assert.equal(result.status, 1);
|
||||
assert.match(result.stderr, /Missing asset readiness receipt/);
|
||||
});
|
||||
|
||||
test("asset readiness rejects named entities without web search evidence", () => {
|
||||
const root = tempDeck();
|
||||
writeReceipt(root, validReceipt(root, {
|
||||
required_search_entities: ["CHAGEE", "MIXUE"],
|
||||
assets: [
|
||||
{
|
||||
slide: "slides/slide_01.svg",
|
||||
purpose: "brand universe",
|
||||
source_type: "generated",
|
||||
status: "ready",
|
||||
local_path: path.join(root, "resources", "images", "hero.png"),
|
||||
assignment_reason: "fallback without search should not satisfy named entity evidence"
|
||||
}
|
||||
]
|
||||
}));
|
||||
const result = run(root);
|
||||
assert.equal(result.status, 1);
|
||||
assert.match(result.stderr, /missing web search evidence for required entities: CHAGEE, MIXUE/);
|
||||
});
|
||||
|
||||
test("asset readiness rejects missing local image files", () => {
|
||||
const root = tempDeck();
|
||||
writeReceipt(root, validReceipt(root, {
|
||||
assets: [
|
||||
{
|
||||
slide: "slides/slide_01.svg",
|
||||
purpose: "hero visual",
|
||||
source_type: "web_search",
|
||||
status: "ready",
|
||||
query: "DeepSeek V4 paper",
|
||||
source_url: "https://example.com/image",
|
||||
local_path: path.join(root, "resources", "images", "missing.png"),
|
||||
assignment_reason: "named source"
|
||||
}
|
||||
]
|
||||
}));
|
||||
const result = run(root);
|
||||
assert.equal(result.status, 1);
|
||||
assert.match(result.stderr, /asset local_path does not exist/);
|
||||
});
|
||||
|
||||
test("asset readiness rejects visual assets that are not embedded in the target slide SVG", () => {
|
||||
const root = tempDeck();
|
||||
fs.writeFileSync(path.join(root, "slides", "slide_01.svg"), `<svg xmlns="http://www.w3.org/2000/svg" xmlns:slide="https://slides.bytedance.com/ns" slide:role="slide" id="slide_01" viewBox="0 0 960 540">
|
||||
<rect slide:role="background" x="0" y="0" width="960" height="540" fill="rgba(255,255,255,1)"/>
|
||||
</svg>`);
|
||||
writeReceipt(root, validReceipt(root));
|
||||
const result = run(root);
|
||||
assert.equal(result.status, 1);
|
||||
assert.match(result.stderr, /visual asset is not embedded in slide SVG/);
|
||||
});
|
||||
|
||||
test("asset readiness rejects ready assets that are not assigned to any slide", () => {
|
||||
const root = tempDeck();
|
||||
writeReceipt(root, validReceipt(root, {
|
||||
slides: [
|
||||
{
|
||||
file: "slides/slide_01.svg",
|
||||
slide_role: "cover",
|
||||
visual_tier: "hero",
|
||||
primary_visual_required: true,
|
||||
primary_visual: path.join(root, "resources", "images", "hero.png"),
|
||||
layout_family: "hero-image-left-code-right",
|
||||
visual_assets: []
|
||||
}
|
||||
]
|
||||
}));
|
||||
const result = run(root);
|
||||
assert.equal(result.status, 1);
|
||||
assert.match(result.stderr, /ready asset is not assigned to any slide/);
|
||||
});
|
||||
|
||||
for (const slideRole of ["cover", "section-divider", "closing", "key-showcase"]) {
|
||||
test(`asset readiness rejects ${slideRole} without primary visual`, () => {
|
||||
const root = tempDeck();
|
||||
writeReceipt(root, validReceipt(root, {
|
||||
slides: [
|
||||
{
|
||||
file: "slides/slide_01.svg",
|
||||
slide_role: slideRole,
|
||||
visual_tier: "hero",
|
||||
primary_visual_required: true,
|
||||
layout_family: "hero-image-left-code-right",
|
||||
visual_assets: [path.join(root, "resources", "images", "hero.png")]
|
||||
}
|
||||
]
|
||||
}));
|
||||
const result = run(root);
|
||||
assert.equal(result.status, 1);
|
||||
assert.match(result.stderr, /primary_visual is required/);
|
||||
});
|
||||
}
|
||||
|
||||
test("asset readiness rejects slides without a slide role", () => {
|
||||
const root = tempDeck();
|
||||
writeReceipt(root, validReceipt(root, {
|
||||
slides: [
|
||||
{
|
||||
file: "slides/slide_01.svg",
|
||||
layout_family: "hero-image-left-code-right",
|
||||
visual_assets: [path.join(root, "resources", "images", "hero.png")]
|
||||
}
|
||||
]
|
||||
}));
|
||||
const result = run(root);
|
||||
assert.equal(result.status, 1);
|
||||
assert.match(result.stderr, /missing slide_role/);
|
||||
});
|
||||
|
||||
test("asset readiness rejects primary visuals that are not embedded in the target slide SVG", () => {
|
||||
const root = tempDeck();
|
||||
fs.writeFileSync(path.join(root, "slides", "slide_01.svg"), `<svg xmlns="http://www.w3.org/2000/svg" xmlns:slide="https://slides.bytedance.com/ns" slide:role="slide" id="slide_01" viewBox="0 0 960 540">
|
||||
<rect slide:role="background" x="0" y="0" width="960" height="540" fill="rgba(255,255,255,1)"/>
|
||||
</svg>`);
|
||||
writeReceipt(root, validReceipt(root));
|
||||
const result = run(root);
|
||||
assert.equal(result.status, 1);
|
||||
assert.match(result.stderr, /primary_visual is not embedded in slide SVG/);
|
||||
});
|
||||
|
||||
test("asset readiness rejects hero text overlays without a mask or scrim", () => {
|
||||
const root = tempDeck();
|
||||
writeReceipt(root, validReceipt(root, {
|
||||
slides: [
|
||||
{
|
||||
file: "slides/slide_01.svg",
|
||||
slide_role: "cover",
|
||||
visual_tier: "hero",
|
||||
primary_visual_required: true,
|
||||
primary_visual: path.join(root, "resources", "images", "hero.png"),
|
||||
layout_family: "full-bleed-hero",
|
||||
text_overlay: true,
|
||||
mask_required: true,
|
||||
mask_strategy: "dark-gradient-left",
|
||||
visual_assets: [path.join(root, "resources", "images", "hero.png")]
|
||||
}
|
||||
]
|
||||
}));
|
||||
const result = run(root);
|
||||
assert.equal(result.status, 1);
|
||||
assert.match(result.stderr, /text overlay requires a readable image mask/);
|
||||
});
|
||||
|
||||
test("asset readiness accepts hero text overlays with a mask or scrim", () => {
|
||||
const root = tempDeck();
|
||||
const imagePath = path.join(root, "resources", "images", "hero.png");
|
||||
fs.writeFileSync(path.join(root, "slides", "slide_01.svg"), `<svg xmlns="http://www.w3.org/2000/svg" xmlns:slide="https://slides.bytedance.com/ns" slide:role="slide" id="slide_01" viewBox="0 0 960 540">
|
||||
<rect slide:role="background" x="0" y="0" width="960" height="540" fill="rgba(255,255,255,1)"/>
|
||||
<image slide:role="image" slide:shape-type="image" href="${imagePath}" x="0" y="0" width="960" height="540"/>
|
||||
<rect slide:role="text-scrim" x="0" y="0" width="520" height="540" fill="rgba(0,0,0,0.45)"/>
|
||||
</svg>`);
|
||||
writeReceipt(root, validReceipt(root, {
|
||||
slides: [
|
||||
{
|
||||
file: "slides/slide_01.svg",
|
||||
slide_role: "cover",
|
||||
visual_tier: "hero",
|
||||
primary_visual_required: true,
|
||||
primary_visual: imagePath,
|
||||
layout_family: "full-bleed-hero-with-mask",
|
||||
text_overlay: true,
|
||||
mask_required: true,
|
||||
mask_strategy: "text-scrim-left",
|
||||
visual_assets: [imagePath]
|
||||
}
|
||||
]
|
||||
}));
|
||||
const result = run(root);
|
||||
assert.equal(result.status, 0, result.stderr || result.stdout);
|
||||
});
|
||||
|
||||
test("asset readiness rejects photographic assets stored as SVG without an exception reason", () => {
|
||||
const root = tempDeck();
|
||||
const svgPath = path.join(root, "resources", "images", "storefront.svg");
|
||||
fs.writeFileSync(svgPath, "<svg></svg>");
|
||||
fs.writeFileSync(path.join(root, "slides", "slide_01.svg"), `<svg xmlns="http://www.w3.org/2000/svg" xmlns:slide="https://slides.bytedance.com/ns" slide:role="slide" id="slide_01" viewBox="0 0 960 540">
|
||||
<rect slide:role="background" x="0" y="0" width="960" height="540" fill="rgba(255,255,255,1)"/>
|
||||
<image slide:role="image" slide:shape-type="image" href="${svgPath}" x="60" y="80" width="240" height="160"/>
|
||||
</svg>`);
|
||||
writeReceipt(root, validReceipt(root, {
|
||||
assets: [
|
||||
{
|
||||
slide: "slides/slide_01.svg",
|
||||
purpose: "small storefront photo",
|
||||
asset_kind: "storefront",
|
||||
expressive_role: "grounds the company analysis in a real retail scene",
|
||||
content_relevance: "shows the store format discussed on the slide",
|
||||
format: "svg",
|
||||
source_type: "web_search",
|
||||
status: "ready",
|
||||
query: "tea company storefront photo",
|
||||
source_url: "https://example.com/storefront",
|
||||
local_path: svgPath,
|
||||
assignment_reason: "storefront photo for operating model discussion"
|
||||
}
|
||||
],
|
||||
slides: [
|
||||
{
|
||||
file: "slides/slide_01.svg",
|
||||
slide_role: "content",
|
||||
visual_tier: "supporting",
|
||||
primary_visual_required: false,
|
||||
layout_family: "image-stat-split",
|
||||
visual_assets: [svgPath]
|
||||
}
|
||||
]
|
||||
}));
|
||||
const result = run(root);
|
||||
assert.equal(result.status, 1);
|
||||
assert.match(result.stderr, /photographic asset should use png, jpg, jpeg, or webp/);
|
||||
});
|
||||
@@ -87,6 +87,28 @@ if (deckUsesCharts(slideFiles) && !promptTraceHasChartEvidence(promptTracePath))
|
||||
fail("Prompt read trace is missing generate_svg_chart reads required by slide:role=\"chart\"", 1);
|
||||
}
|
||||
|
||||
const assetReadinessPath = path.join(root, "receipts", "asset_readiness.json");
|
||||
if (!fs.existsSync(assetReadinessPath)) {
|
||||
fail(`Missing asset readiness receipt: ${assetReadinessPath}`, 1);
|
||||
}
|
||||
const assetValidator = path.resolve("skills/lark-slides/scripts/svg_slides_asset_readiness.mjs");
|
||||
const validateAssets = spawnSync("node", [assetValidator, root, "--json"], { encoding: "utf8" });
|
||||
if (validateAssets.status !== 0) {
|
||||
process.stderr.write(validateAssets.stderr || validateAssets.stdout);
|
||||
process.exit(validateAssets.status || 1);
|
||||
}
|
||||
|
||||
const typographyPlanPath = path.join(root, "receipts", "typography_plan.json");
|
||||
if (!fs.existsSync(typographyPlanPath)) {
|
||||
fail(`Missing typography plan receipt: ${typographyPlanPath}`, 1);
|
||||
}
|
||||
const typographyValidator = path.resolve("skills/lark-slides/scripts/svg_slides_typography_plan.mjs");
|
||||
const validateTypography = spawnSync("node", [typographyValidator, root, "--json"], { encoding: "utf8" });
|
||||
if (validateTypography.status !== 0) {
|
||||
process.stderr.write(validateTypography.stderr || validateTypography.stdout);
|
||||
process.exit(validateTypography.status || 1);
|
||||
}
|
||||
|
||||
const pages = slideFiles.map((file, index) => {
|
||||
const abs = file;
|
||||
const raw = fs.readFileSync(abs);
|
||||
@@ -108,7 +130,9 @@ const manifest = {
|
||||
pages,
|
||||
receipts: {
|
||||
validate_svg_deck: "receipts/validate_svg_deck.json",
|
||||
prompt_read_trace: "receipts/prompt_read_trace.json"
|
||||
prompt_read_trace: "receipts/prompt_read_trace.json",
|
||||
asset_readiness: "receipts/asset_readiness.json",
|
||||
typography_plan: "receipts/typography_plan.json"
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ function tempRoot(prefix) {
|
||||
|
||||
const validSlide = `<svg xmlns="http://www.w3.org/2000/svg" xmlns:slide="https://slides.bytedance.com/ns" slide:role="slide" id="bundle_slide" viewBox="0 0 960 540">
|
||||
<rect slide:role="background" x="0" y="0" width="960" height="540" fill="rgba(255,255,255,1)"/>
|
||||
<foreignObject slide:role="shape" slide:shape-type="text" x="80" y="80" width="600" height="80" style="font-size:32px;font-family:DM Sans,PingFang SC,Noto Sans SC,Arial,sans-serif;color:rgba(15,23,42,1);line-height:1.2;letter-spacing:0px;padding:0px">
|
||||
<foreignObject slide:role="shape" slide:shape-type="text" x="80" y="80" width="600" height="80" style="font-size:32px;font-family:DM Sans, 黑体, sans-serif;color:rgba(15,23,42,1);line-height:1.2;letter-spacing:0px;padding:0px">
|
||||
<p xmlns="http://www.w3.org/1999/xhtml" style="margin:0px;font-size:32px;color:rgba(15,23,42,1)">Bundle</p>
|
||||
</foreignObject>
|
||||
</svg>`;
|
||||
@@ -30,33 +30,37 @@ const chartSlide = `<svg xmlns="http://www.w3.org/2000/svg" xmlns:slide="https:/
|
||||
<rect slide:role="chart" href="resources/charts/example_bar.svg" x="80" y="120" width="640" height="360"/>
|
||||
</svg>`;
|
||||
|
||||
function writeDoc(root, relPath, role, sourceLines = "1-1", body = "content") {
|
||||
function writeDoc(root, relPath, role, body = "content") {
|
||||
const filePath = path.join(root, relPath);
|
||||
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
||||
fs.writeFileSync(filePath, `# ${path.basename(relPath, ".md")}\n\n## Context\n\n- Role: ${role}\n- Source lines: ${sourceLines}\n\n## Content\n\n${body}\n`);
|
||||
fs.writeFileSync(filePath, `# ${path.basename(relPath, ".md")}\n\n## Context\n\n- Role: ${role}\n\n## Content\n\n${body}\n`);
|
||||
}
|
||||
|
||||
function writeDeckReferenceSet(root) {
|
||||
writeDoc(root, "README.md", "reference", "1-1");
|
||||
writeDoc(root, "system-prompt.md", "prompt", "3-196");
|
||||
writeDoc(root, "svg-reference.md", "reference", "198-865");
|
||||
writeDoc(root, "deck-design-reference-catalog.md", "reference", "1082-1233");
|
||||
writeDoc(root, "resolve-design-brief/README.md", "reference", "867-1080");
|
||||
writeDoc(root, "resolve-design-brief/full.md", "prompt_bundle", "867-1080");
|
||||
writeDoc(root, "tools/slide-outline.md", "tool_contract", "1236-1254");
|
||||
writeDoc(root, "tools/activate-slides-edit.md", "tool_contract", "1256-1262");
|
||||
writeDoc(root, "tools/slides-edit.md", "tool_contract", "1264-1331");
|
||||
writeDoc(root, "tools/finish-slides-edit.md", "tool_contract", "1333-1339");
|
||||
writeDoc(root, "validation/prompt-read-trace.md", "schema", "generated");
|
||||
writeDoc(root, "README.md", "reference");
|
||||
writeDoc(root, "system-prompt.md", "prompt");
|
||||
writeDoc(root, "svg-reference.md", "reference");
|
||||
writeDoc(root, "deck-design-reference-catalog.md", "reference");
|
||||
writeDoc(root, "resolve-design-brief/README.md", "reference");
|
||||
writeDoc(root, "resolve-design-brief/tool-contract.md", "tool_contract");
|
||||
writeDoc(root, "resolve-design-brief/style-director-prompt.md", "prompt");
|
||||
writeDoc(root, "resolve-design-brief/full.md", "prompt_bundle");
|
||||
writeDoc(root, "tools/slide-outline.md", "tool_contract");
|
||||
writeDoc(root, "tools/activate-slides-edit.md", "tool_contract");
|
||||
writeDoc(root, "tools/slides-edit.md", "tool_contract");
|
||||
writeDoc(root, "tools/finish-slides-edit.md", "tool_contract");
|
||||
writeDoc(root, "validation/asset-readiness.md", "schema");
|
||||
writeDoc(root, "validation/typography-plan.md", "schema");
|
||||
writeDoc(root, "validation/prompt-read-trace.md", "schema");
|
||||
}
|
||||
|
||||
function writeChartReferenceSet(root) {
|
||||
writeDoc(root, "generate-svg-chart/README.md", "reference", "1357-2356");
|
||||
writeDoc(root, "generate-svg-chart/input-schema.md", "schema", "1367-1434");
|
||||
writeDoc(root, "generate-svg-chart/chart-agent-contract.md", "prompt", "1436-1487");
|
||||
writeDoc(root, "generate-svg-chart/design-envelope.md", "reference", "1489-1586");
|
||||
writeDoc(root, "generate-svg-chart/full.md", "prompt_bundle", "1357-2356");
|
||||
writeDoc(root, "generate-svg-chart/snippets/README.md", "reference", "1590-2354");
|
||||
writeDoc(root, "generate-svg-chart/README.md", "reference");
|
||||
writeDoc(root, "generate-svg-chart/input-schema.md", "schema");
|
||||
writeDoc(root, "generate-svg-chart/chart-agent-contract.md", "prompt");
|
||||
writeDoc(root, "generate-svg-chart/design-envelope.md", "reference");
|
||||
writeDoc(root, "generate-svg-chart/full.md", "prompt_bundle");
|
||||
writeDoc(root, "generate-svg-chart/snippets/README.md", "reference");
|
||||
}
|
||||
|
||||
function createPromptTrace(root, refs, extraArgs = []) {
|
||||
@@ -69,6 +73,73 @@ function createChartPromptTrace(root, refs) {
|
||||
assert.equal(result.status, 0, result.stderr || result.stdout);
|
||||
}
|
||||
|
||||
function writeAssetReadiness(root) {
|
||||
const imagePath = path.join(root, "resources", "images", "hero.png");
|
||||
fs.mkdirSync(path.dirname(imagePath), { recursive: true });
|
||||
fs.writeFileSync(imagePath, "fake image bytes");
|
||||
const slidePath = path.join(root, "slides", "slide_01.svg");
|
||||
const slideSource = fs.readFileSync(slidePath, "utf8");
|
||||
if (!slideSource.includes(imagePath)) {
|
||||
fs.writeFileSync(slidePath, slideSource.replace(
|
||||
"</svg>",
|
||||
` <image slide:role="image" slide:shape-type="image" href="${imagePath}" x="720" y="420" width="80" height="45"/>\n</svg>`
|
||||
));
|
||||
}
|
||||
fs.mkdirSync(path.join(root, "receipts"), { recursive: true });
|
||||
fs.writeFileSync(path.join(root, "receipts", "asset_readiness.json"), `${JSON.stringify({
|
||||
version: "svg-slides.asset-readiness.v1",
|
||||
visual_identity_fingerprint: {
|
||||
style_family: "technical-paper-terminal",
|
||||
composition_signature: "asymmetric-code-panels",
|
||||
imagery_treatment: "generated diagrams",
|
||||
palette_signature: "navy-cyan-orange"
|
||||
},
|
||||
required_search_entities: [],
|
||||
assets: [
|
||||
{
|
||||
slide: "slides/slide_01.svg",
|
||||
purpose: "primary hero visual",
|
||||
asset_kind: "illustration",
|
||||
expressive_role: "gives the test deck a concrete visual anchor",
|
||||
content_relevance: "supports the slide's main bundle validation example",
|
||||
format: "png",
|
||||
source_type: "generated",
|
||||
status: "ready",
|
||||
local_path: imagePath,
|
||||
assignment_reason: "test deck needs a prepared visual"
|
||||
}
|
||||
],
|
||||
slides: [
|
||||
{
|
||||
file: "slides/slide_01.svg",
|
||||
slide_role: "cover",
|
||||
visual_tier: "hero",
|
||||
primary_visual_required: true,
|
||||
primary_visual: imagePath,
|
||||
layout_family: "hero-image-left",
|
||||
text_overlay: false,
|
||||
mask_required: false,
|
||||
visual_assets: [imagePath]
|
||||
}
|
||||
]
|
||||
}, null, 2)}\n`);
|
||||
}
|
||||
|
||||
function writeTypographyPlan(root) {
|
||||
fs.mkdirSync(path.join(root, "receipts"), { recursive: true });
|
||||
fs.writeFileSync(path.join(root, "receipts", "typography_plan.json"), `${JSON.stringify({
|
||||
version: "svg-slides.typography-plan.v1",
|
||||
font_category: "clean sans display and body",
|
||||
display_font: "DM Sans, 黑体, sans-serif",
|
||||
body_font: "DM Sans, 黑体, sans-serif",
|
||||
label_font: "DM Sans, 黑体, sans-serif",
|
||||
source: "visual_system.typography",
|
||||
selection_reason: "Test deck uses a clean sans pairing that matches the slide SVG.",
|
||||
forbidden_substitutions: ["calligraphy body"],
|
||||
runtime_candidates_used: false
|
||||
}, null, 2)}\n`);
|
||||
}
|
||||
|
||||
function readPromptTrace(root) {
|
||||
return JSON.parse(fs.readFileSync(path.join(root, "receipts", "prompt_read_trace.json"), "utf8"));
|
||||
}
|
||||
@@ -79,6 +150,8 @@ test("bundle builder writes manifest and validation receipt", () => {
|
||||
writeDeckReferenceSet(refs);
|
||||
fs.writeFileSync(path.join(root, "slides", "slide_01.svg"), validSlide);
|
||||
createPromptTrace(root, refs);
|
||||
writeAssetReadiness(root);
|
||||
writeTypographyPlan(root);
|
||||
const result = spawnSync("node", [script, root, "--title", "Bundle Test", "--references", refs], { encoding: "utf8" });
|
||||
assert.equal(result.status, 0, result.stderr || result.stdout);
|
||||
const manifest = JSON.parse(fs.readFileSync(path.join(root, "manifest.json"), "utf8"));
|
||||
@@ -89,18 +162,47 @@ test("bundle builder writes manifest and validation receipt", () => {
|
||||
assert.equal(manifest.publish_ready, true);
|
||||
assert.equal(manifest.published, false);
|
||||
assert.equal(manifest.receipts.prompt_read_trace, "receipts/prompt_read_trace.json");
|
||||
assert.equal(manifest.receipts.asset_readiness, "receipts/asset_readiness.json");
|
||||
assert.equal(manifest.receipts.typography_plan, "receipts/typography_plan.json");
|
||||
assert.equal(manifest.pages.length, 1);
|
||||
assert.match(manifest.pages[0].sha256, /^[a-f0-9]{64}$/);
|
||||
const receipt = JSON.parse(fs.readFileSync(path.join(root, "receipts", "validate_svg_deck.json"), "utf8"));
|
||||
assert.equal(receipt.totalErrors, 0);
|
||||
});
|
||||
|
||||
test("bundle builder rejects missing asset readiness receipt", () => {
|
||||
const root = tempDeck();
|
||||
const refs = tempRoot("svg-slides-bundle-refs-");
|
||||
writeDeckReferenceSet(refs);
|
||||
fs.writeFileSync(path.join(root, "slides", "slide_01.svg"), validSlide);
|
||||
createPromptTrace(root, refs);
|
||||
const result = spawnSync("node", [script, root, "--title", "Missing Assets", "--references", refs], { encoding: "utf8" });
|
||||
assert.equal(result.status, 1);
|
||||
assert.match(result.stderr, /Missing asset readiness receipt/);
|
||||
assert.equal(fs.existsSync(path.join(root, "manifest.json")), false);
|
||||
});
|
||||
|
||||
test("bundle builder rejects missing typography plan receipt", () => {
|
||||
const root = tempDeck();
|
||||
const refs = tempRoot("svg-slides-bundle-refs-");
|
||||
writeDeckReferenceSet(refs);
|
||||
fs.writeFileSync(path.join(root, "slides", "slide_01.svg"), validSlide);
|
||||
createPromptTrace(root, refs);
|
||||
writeAssetReadiness(root);
|
||||
const result = spawnSync("node", [script, root, "--title", "Missing Typography", "--references", refs], { encoding: "utf8" });
|
||||
assert.equal(result.status, 1);
|
||||
assert.match(result.stderr, /Missing typography plan receipt/);
|
||||
assert.equal(fs.existsSync(path.join(root, "manifest.json")), false);
|
||||
});
|
||||
|
||||
test("bundle builder rejects invalid SVG deck", () => {
|
||||
const root = tempDeck();
|
||||
const refs = tempRoot("svg-slides-bundle-refs-");
|
||||
writeDeckReferenceSet(refs);
|
||||
fs.writeFileSync(path.join(root, "slides", "slide_01.svg"), validSlide.replace("rgba(255,255,255,1)", "#fff"));
|
||||
createPromptTrace(root, refs);
|
||||
writeAssetReadiness(root);
|
||||
writeTypographyPlan(root);
|
||||
const result = spawnSync("node", [script, root, "--title", "Invalid", "--references", refs], { encoding: "utf8" });
|
||||
assert.equal(result.status, 1);
|
||||
assert.match(result.stderr, /not publish-ready/);
|
||||
@@ -123,6 +225,8 @@ test("bundle builder rejects forged prompt trace hashes", () => {
|
||||
writeDeckReferenceSet(refs);
|
||||
fs.writeFileSync(path.join(root, "slides", "slide_01.svg"), validSlide);
|
||||
createPromptTrace(root, refs);
|
||||
writeAssetReadiness(root);
|
||||
writeTypographyPlan(root);
|
||||
const receipt = readPromptTrace(root);
|
||||
receipt.read_order[0].sha256 = "f".repeat(64);
|
||||
fs.writeFileSync(path.join(root, "receipts", "prompt_read_trace.json"), `${JSON.stringify(receipt, null, 2)}\n`);
|
||||
@@ -138,6 +242,8 @@ test("bundle builder rejects prompt trace entries stripped to path and sha256 on
|
||||
writeDeckReferenceSet(refs);
|
||||
fs.writeFileSync(path.join(root, "slides", "slide_01.svg"), validSlide);
|
||||
createPromptTrace(root, refs);
|
||||
writeAssetReadiness(root);
|
||||
writeTypographyPlan(root);
|
||||
const receipt = readPromptTrace(root);
|
||||
receipt.read_order = receipt.read_order.map((entry) => ({
|
||||
path: entry.path,
|
||||
@@ -156,6 +262,8 @@ test("bundle builder rejects chart-only prompt trace for deck bundle", () => {
|
||||
writeChartReferenceSet(refs);
|
||||
fs.writeFileSync(path.join(root, "slides", "slide_01.svg"), validSlide);
|
||||
createChartPromptTrace(root, refs);
|
||||
writeAssetReadiness(root);
|
||||
writeTypographyPlan(root);
|
||||
const result = spawnSync("node", [script, root, "--title", "Chart Trace", "--references", refs], { encoding: "utf8" });
|
||||
assert.equal(result.status, 1);
|
||||
assert.match(result.stderr, /task mismatch/);
|
||||
@@ -168,6 +276,8 @@ test("bundle builder rejects chart deck without chart prompt reads", () => {
|
||||
writeDeckReferenceSet(refs);
|
||||
fs.writeFileSync(path.join(root, "slides", "slide_01.svg"), chartSlide);
|
||||
createPromptTrace(root, refs);
|
||||
writeAssetReadiness(root);
|
||||
writeTypographyPlan(root);
|
||||
const result = spawnSync("node", [script, root, "--title", "Chart Without Reads", "--references", refs], { encoding: "utf8" });
|
||||
assert.equal(result.status, 1);
|
||||
assert.match(result.stderr, /missing generate_svg_chart reads/);
|
||||
@@ -181,9 +291,13 @@ test("bundle builder accepts chart deck with deck and chart prompt reads", () =>
|
||||
writeChartReferenceSet(refs);
|
||||
fs.writeFileSync(path.join(root, "slides", "slide_01.svg"), chartSlide);
|
||||
createPromptTrace(root, refs, ["--use-full-fallback"]);
|
||||
writeAssetReadiness(root);
|
||||
writeTypographyPlan(root);
|
||||
const result = spawnSync("node", [script, root, "--title", "Chart With Reads", "--references", refs], { encoding: "utf8" });
|
||||
assert.equal(result.status, 0, result.stderr || result.stdout);
|
||||
const manifest = JSON.parse(fs.readFileSync(path.join(root, "manifest.json"), "utf8"));
|
||||
assert.equal(manifest.publish_ready, true);
|
||||
assert.equal(manifest.receipts.prompt_read_trace, "receipts/prompt_read_trace.json");
|
||||
assert.equal(manifest.receipts.asset_readiness, "receipts/asset_readiness.json");
|
||||
assert.equal(manifest.receipts.typography_plan, "receipts/typography_plan.json");
|
||||
});
|
||||
|
||||
@@ -2,11 +2,32 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
const rootArg = process.argv.find((arg) => !arg.startsWith("--") && arg !== process.argv[0] && arg !== process.argv[1]);
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
function argValue(name, fallback = "") {
|
||||
const index = args.indexOf(name);
|
||||
return index >= 0 ? args[index + 1] : fallback;
|
||||
}
|
||||
|
||||
function positionalArgs() {
|
||||
const values = [];
|
||||
for (let index = 0; index < args.length; index += 1) {
|
||||
const arg = args[index];
|
||||
if (arg === "--source-root") {
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
if (!arg.startsWith("--")) values.push(arg);
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
const [rootArg] = positionalArgs();
|
||||
const root = path.resolve(rootArg || "skills/lark-slides/references/svg-slides");
|
||||
const dryRun = process.argv.includes("--dry-run");
|
||||
const sourcePath = path.join(root, "source", "full.debranded.md");
|
||||
const manifestPath = path.join(root, "source", "split-manifest.json");
|
||||
const sourceRoot = path.resolve(argValue("--source-root", "skills/lark-slides/prompt-sources/svg-slides"));
|
||||
const dryRun = args.includes("--dry-run");
|
||||
const sourcePath = path.join(sourceRoot, "full.debranded.md");
|
||||
const manifestPath = path.join(sourceRoot, "split-manifest.json");
|
||||
|
||||
const obsoleteFiles = [
|
||||
"workflow.md",
|
||||
@@ -39,6 +60,13 @@ const manifestTargets = {
|
||||
slides_parse_template_tool: "tools/slides-parse-template.md"
|
||||
};
|
||||
|
||||
if (!fs.existsSync(sourcePath)) {
|
||||
throw new Error(`missing prompt source: ${sourcePath}`);
|
||||
}
|
||||
if (!fs.existsSync(manifestPath)) {
|
||||
throw new Error(`missing split manifest: ${manifestPath}`);
|
||||
}
|
||||
|
||||
const sourceLines = fs.readFileSync(sourcePath, "utf8").split(/\r?\n/);
|
||||
|
||||
function lineRange(start, end) {
|
||||
@@ -75,7 +103,7 @@ function removePath(relPath) {
|
||||
}
|
||||
}
|
||||
|
||||
function doc({ title, role, purpose, source, coverageIds = [], requiredReadPath = [], content }) {
|
||||
function doc({ title, role, purpose, requiredReadPath = [], content }) {
|
||||
const readPath = requiredReadPath.length === 0
|
||||
? ""
|
||||
: [
|
||||
@@ -84,22 +112,6 @@ function doc({ title, role, purpose, source, coverageIds = [], requiredReadPath
|
||||
...requiredReadPath.map((item, index) => `${index + 1}. ${item}`),
|
||||
""
|
||||
].join("\n");
|
||||
const coverage = coverageIds.length === 0
|
||||
? [
|
||||
"## Source Slice",
|
||||
"",
|
||||
`- Source lines: ${source}`,
|
||||
""
|
||||
].join("\n")
|
||||
: [
|
||||
"## Source Coverage",
|
||||
"",
|
||||
`- Covers manifest sections: ${coverageIds.join(", ")}`,
|
||||
`- Source lines: ${source}`,
|
||||
"- Coverage mode: extracted runtime-unit content, preserving executable prompt or contract boundaries.",
|
||||
"- Omitted source-only artifacts: copied implementation filenames only.",
|
||||
""
|
||||
].join("\n");
|
||||
return [
|
||||
`# ${title}`,
|
||||
"",
|
||||
@@ -107,14 +119,11 @@ function doc({ title, role, purpose, source, coverageIds = [], requiredReadPath
|
||||
"",
|
||||
`- Role: ${role}`,
|
||||
`- Purpose: ${purpose}`,
|
||||
`- Source lines: ${source}`,
|
||||
"",
|
||||
readPath,
|
||||
"## Content",
|
||||
"",
|
||||
cleanSourceOnlyArtifacts(content),
|
||||
"",
|
||||
coverage
|
||||
cleanSourceOnlyArtifacts(content)
|
||||
].filter(Boolean).join("\n");
|
||||
}
|
||||
|
||||
@@ -122,7 +131,9 @@ function writeRootReadme() {
|
||||
writeFile("README.md", [
|
||||
"# SVG Slides Local Generation",
|
||||
"",
|
||||
"This reference family is generated from `source/full.debranded.md` and organized by runtime unit.",
|
||||
"This directory is the runtime reference contract for local SVG Slides generation.",
|
||||
"",
|
||||
"Do not use maintainer prompt-source files as generation context. Runtime agents should follow the read routes below.",
|
||||
"",
|
||||
"## Read Routes",
|
||||
"",
|
||||
@@ -131,9 +142,11 @@ function writeRootReadme() {
|
||||
"| Understand the main SVG Slides workflow | `system-prompt.md` | `svg-reference.md`, `tools/slides-edit.md` |",
|
||||
"| Author or repair SVG protocol content | `svg-reference.md` | `tools/slides-edit.md`, `tools/compute-custom-shape-bbox.md` |",
|
||||
"| Resolve deck design direction | `resolve-design-brief/README.md` | `deck-design-reference-catalog.md` |",
|
||||
"| Choose or audit typography | `resolve-design-brief/style-director-prompt.md` | `system-prompt.md`, `svg-reference.md`, `validation/typography-plan.md` |",
|
||||
"| Generate or revise charts | `generate-svg-chart/README.md` | matching files under `generate-svg-chart/snippets/` |",
|
||||
"| Continue or convert an existing deck | `tools/slides-convert.md` | `tools/slide-organize.md`, `tools/slides-edit.md` |",
|
||||
"| Audit source coverage | `validation/source-coverage.md` | `source/split-manifest.json`, `source/full.debranded.md` |",
|
||||
"| Prove asset search/readiness | `validation/asset-readiness.md` | `../../../scripts/svg_slides_asset_readiness.mjs`, `../../../scripts/svg_slides_visual_identity_check.mjs` |",
|
||||
"| Prove typography planning | `validation/typography-plan.md` | `../../../scripts/svg_slides_typography_plan.mjs` |",
|
||||
"| Prove prompt reads for a generated deck | `validation/prompt-read-trace.md` | `../../scripts/svg_slides_prompt_trace.mjs` |",
|
||||
"",
|
||||
"## Boundary",
|
||||
@@ -144,13 +157,19 @@ function writeRootReadme() {
|
||||
"",
|
||||
"Every generated local bundle must include `receipts/prompt_read_trace.json` before `manifest.json` may set `publish_ready=true`.",
|
||||
"",
|
||||
"The trace records the prompt/reference Markdown files read for the generation task, including path, role, reason, source lines, and SHA-256.",
|
||||
"The trace records the runtime prompt/reference Markdown files read for the generation task, including path, role, reason, and SHA-256.",
|
||||
"",
|
||||
"## Source Coverage",
|
||||
"## Asset Readiness Gate",
|
||||
"",
|
||||
"- Covers manifest sections: title",
|
||||
"- Source lines: 1-1",
|
||||
"- Coverage mode: routing entry; executable content lives in the linked runtime-unit files."
|
||||
"Every generated local bundle must include `receipts/asset_readiness.json` before `manifest.json` may set `publish_ready=true`.",
|
||||
"",
|
||||
"The receipt records the deck's visual identity fingerprint, required image-search entities, prepared local assets, and per-slide layout families.",
|
||||
"",
|
||||
"## Typography Plan Gate",
|
||||
"",
|
||||
"Every generated local bundle must include `receipts/typography_plan.json` before `manifest.json` may set `publish_ready=true`.",
|
||||
"",
|
||||
"The receipt records the deck's selected font category, display/body/label stacks, source, and rejected substitutions. Slide SVG text must use only those planned stacks."
|
||||
].join("\n"));
|
||||
}
|
||||
|
||||
@@ -218,11 +237,7 @@ function writeResolveDesignBriefDocs() {
|
||||
"3. `design-director-prompt.md`",
|
||||
"4. `../deck-design-reference-catalog.md`",
|
||||
"",
|
||||
"If routing is uncertain, read `full.md`.",
|
||||
"",
|
||||
"## Source Slice",
|
||||
"",
|
||||
"- Source lines: 867-1080"
|
||||
"If routing is uncertain, read `full.md`."
|
||||
].join("\n"));
|
||||
for (const part of parts) {
|
||||
writeFile(part.relPath, doc(part));
|
||||
@@ -336,11 +351,7 @@ function writeGenerateChartDocs() {
|
||||
"4. `snippets/README.md`",
|
||||
"5. Snippets whose frontmatter `chart_types` include the selected `chart_type`",
|
||||
"",
|
||||
"If snippet routing is uncertain, read `full.md`.",
|
||||
"",
|
||||
"## Source Slice",
|
||||
"",
|
||||
"- Source lines: 1357-2356"
|
||||
"If snippet routing is uncertain, read `full.md`."
|
||||
].join("\n"));
|
||||
for (const [relPath, title, source, content, purpose] of parts) {
|
||||
writeFile(relPath, doc({
|
||||
@@ -355,7 +366,7 @@ function writeGenerateChartDocs() {
|
||||
const snippetIndex = [];
|
||||
for (const snippet of snippets) {
|
||||
const types = snippetChartTypes(snippet.content);
|
||||
snippetIndex.push(`- \`${snippet.name}\` (${snippet.source}): ${types.join(", ")}`);
|
||||
snippetIndex.push(`- \`${snippet.name}\`: ${types.join(", ")}`);
|
||||
writeFile(`generate-svg-chart/snippets/${snippet.name}`, doc({
|
||||
title: snippet.name.replace(/\.md$/, ""),
|
||||
role: "snippet",
|
||||
@@ -372,11 +383,7 @@ function writeGenerateChartDocs() {
|
||||
"## Snippet Index",
|
||||
"",
|
||||
...snippetIndex,
|
||||
"",
|
||||
"## Source Slice",
|
||||
"",
|
||||
"- Source lines: 1590-2354",
|
||||
"- Snippet entries include their own source line ranges for traceability"
|
||||
""
|
||||
].join("\n"));
|
||||
writeFile("generate-svg-chart/full.md", doc({
|
||||
title: "Generate SVG Chart Full",
|
||||
@@ -413,28 +420,137 @@ function writeValidationDocs() {
|
||||
"- Protocol validation script: `../../../scripts/validate_svg_deck.mjs`",
|
||||
"- Bundle manifest script: `../../../scripts/svg_slides_bundle.mjs`",
|
||||
"- Browser text bounds script: `../../../scripts/svg_slides_browser_text_bounds.mjs`",
|
||||
"- Prompt read trace script: `../../../scripts/svg_slides_prompt_trace.mjs`",
|
||||
"",
|
||||
"## Source Slice",
|
||||
"",
|
||||
"- Source lines: derived from local validation scripts, not a manifest section"
|
||||
"- Asset readiness script: `../../../scripts/svg_slides_asset_readiness.mjs`",
|
||||
"- Visual identity comparison script: `../../../scripts/svg_slides_visual_identity_check.mjs`",
|
||||
"- Typography plan script: `../../../scripts/svg_slides_typography_plan.mjs`",
|
||||
"- Prompt read trace script: `../../../scripts/svg_slides_prompt_trace.mjs`"
|
||||
].join("\n"));
|
||||
writeFile("validation/source-coverage.md", [
|
||||
"# Source Coverage Audit",
|
||||
"# Runtime Reference Coverage Audit",
|
||||
"",
|
||||
"Coverage is declared by `source/split-manifest.json`. Each manifest section must be covered by the exact target Markdown path named in its `target` field.",
|
||||
"Coverage is a maintainer audit, not deck-generation context.",
|
||||
"",
|
||||
"The split manifest lives under `skills/lark-slides/prompt-sources/svg-slides/`. Runtime agents should not read that directory while generating decks.",
|
||||
"",
|
||||
"Rules:",
|
||||
"",
|
||||
"- Scan Markdown files recursively under `skills/lark-slides/references/svg-slides`.",
|
||||
"- Ignore `source/` because it is provenance input, not a coverage target.",
|
||||
"- Each covered section id must appear in exactly one target file.",
|
||||
"- Each manifest section's target file must exist in the runtime reference tree.",
|
||||
"- Runtime Markdown must not expose prompt-source paths, source-line metadata, or source-coverage blocks.",
|
||||
"- `generate-svg-chart/snippets/` must contain exactly 22 snippet Markdown files excluding `README.md`.",
|
||||
"- Component files may use `## Source Slice`; coverage authority remains the target file's `## Source Coverage` block.",
|
||||
"- The source manifest remains the authority for maintainer coverage."
|
||||
].join("\n"));
|
||||
writeFile("validation/asset-readiness.md", [
|
||||
"# Asset Readiness",
|
||||
"",
|
||||
"## Source Slice",
|
||||
"Every generated SVG Slides bundle must write `receipts/asset_readiness.json` before `manifest.json` may set `publish_ready=true`.",
|
||||
"",
|
||||
"- Source lines: coverage policy for the extracted tree"
|
||||
"The receipt proves that visual assets were planned and prepared before slide authoring. It closes two failure modes:",
|
||||
"",
|
||||
"- slides that pass SVG protocol validation but contain only decorative shapes instead of searched or generated visuals",
|
||||
"- multiple decks in one run that reuse the same generic visual system and layout vocabulary",
|
||||
"",
|
||||
"## Required Receipt",
|
||||
"",
|
||||
"```json",
|
||||
"{",
|
||||
" \"version\": \"svg-slides.asset-readiness.v1\",",
|
||||
" \"visual_identity_fingerprint\": {",
|
||||
" \"style_family\": \"technical-paper-terminal\",",
|
||||
" \"composition_signature\": \"asymmetric-code-panels\",",
|
||||
" \"imagery_treatment\": \"paper screenshots and architecture diagrams\",",
|
||||
" \"palette_signature\": \"navy-cyan-orange\"",
|
||||
" },",
|
||||
" \"required_search_entities\": [\"Named company or product when applicable\"],",
|
||||
" \"assets\": [",
|
||||
" {",
|
||||
" \"slide\": \"slides/slide_01.svg\",",
|
||||
" \"purpose\": \"primary hero visual\",",
|
||||
" \"asset_kind\": \"photograph | product_image | person_image | storefront | screenshot | document_capture | illustration | chart | icon | decoration\",",
|
||||
" \"expressive_role\": \"what emotion, proof, or narrative function this asset carries\",",
|
||||
" \"content_relevance\": \"why this exact visual belongs to this slide's key message\",",
|
||||
" \"format\": \"png\",",
|
||||
" \"source_type\": \"web_search\",",
|
||||
" \"status\": \"ready\",",
|
||||
" \"query\": \"specific query used\",",
|
||||
" \"source_url\": \"https://example.com/source\",",
|
||||
" \"local_path\": \"/absolute/path/to/local/image.png\",",
|
||||
" \"assignment_reason\": \"why this asset belongs on this slide\"",
|
||||
" }",
|
||||
" ],",
|
||||
" \"slides\": [",
|
||||
" {",
|
||||
" \"file\": \"slides/slide_01.svg\",",
|
||||
" \"slide_role\": \"cover | section-divider | content | key-showcase | closing\",",
|
||||
" \"visual_tier\": \"hero | supporting | chart | none\",",
|
||||
" \"primary_visual_required\": true,",
|
||||
" \"primary_visual\": \"/absolute/path/to/local/image.png\",",
|
||||
" \"layout_family\": \"hero-image-left-code-right\",",
|
||||
" \"text_overlay\": true,",
|
||||
" \"mask_required\": true,",
|
||||
" \"mask_strategy\": \"dark-gradient-left\",",
|
||||
" \"visual_assets\": [\"/absolute/path/to/local/image.png\"]",
|
||||
" }",
|
||||
" ]",
|
||||
"}",
|
||||
"```",
|
||||
"",
|
||||
"## Rules",
|
||||
"",
|
||||
"- For specific real-world entities (named companies, products, people, places, venues, artworks, or papers), record them in `required_search_entities` and use `source_type: \"web_search\"` evidence for each entity.",
|
||||
"- Search failures must still be explicit assets with `status: \"not_found\"`, `query`, and `failure_reason`; then use `generated` fallback assets where appropriate.",
|
||||
"- Abstract concepts can use `generated` assets, but they still require a concrete subject, local file path, and assignment reason.",
|
||||
"- Every slide must declare `slide_role` and `layout_family`. `cover`, `section-divider`, `closing`, and `key-showcase` slides must set `primary_visual_required: true` and provide a `primary_visual`.",
|
||||
"- Primary/hero assets must declare `asset_kind`, `expressive_role`, `content_relevance`, and `format`; charts, icons, decorative patterns, textures, and abstract geometry do not satisfy a primary visual requirement.",
|
||||
"- Photographic/product/person/storefront/screenshot/document-capture assets should use PNG/JPG/JPEG/WebP. SVG is allowed for those only with `format_exception_reason`.",
|
||||
"- If slide text overlays a hero image (`text_overlay: true` and `mask_required: true`), the slide SVG must include a readable mask/scrim/safe-zone structure such as `slide:role=\"text-scrim\"`, `slide:role=\"image-mask\"`, a gradient overlay, or a semi-transparent overlay rect.",
|
||||
"- Every `ready` visual asset must have an existing local path, be assigned to a slide's `visual_assets`, and be embedded by that slide SVG. Remote-only URLs are not publish-ready.",
|
||||
"- Every slide must declare a `layout_family`; multi-deck/team runs must compare `visual_identity_fingerprint` and layout-family overlap with `../../../scripts/svg_slides_visual_identity_check.mjs`.",
|
||||
"",
|
||||
"## Validation Commands",
|
||||
"",
|
||||
"```bash",
|
||||
"node skills/lark-slides/scripts/svg_slides_asset_readiness.mjs <deck-dir>",
|
||||
"node skills/lark-slides/scripts/svg_slides_visual_identity_check.mjs <deck-a> <deck-b> [<deck-c>...]",
|
||||
"```"
|
||||
].join("\n"));
|
||||
writeFile("validation/typography-plan.md", [
|
||||
"# Typography Plan",
|
||||
"",
|
||||
"Every generated SVG Slides bundle must write `receipts/typography_plan.json` before `manifest.json` may set `publish_ready=true`.",
|
||||
"",
|
||||
"The receipt makes deck-level font choice explicit. It prevents a generator from silently substituting fonts during slide authoring after the design brief chose a visual direction.",
|
||||
"",
|
||||
"## Required Receipt",
|
||||
"",
|
||||
"```json",
|
||||
"{",
|
||||
" \"version\": \"svg-slides.typography-plan.v1\",",
|
||||
" \"font_category\": \"clean sans display and body\",",
|
||||
" \"display_font\": \"DM Sans, 黑体, sans-serif\",",
|
||||
" \"body_font\": \"DM Sans, 黑体, sans-serif\",",
|
||||
" \"label_font\": \"DM Sans, 黑体, sans-serif\",",
|
||||
" \"source\": \"visual_system.typography\",",
|
||||
" \"selection_reason\": \"Why this category and stack match the deck's topic, audience, and tone.\",",
|
||||
" \"forbidden_substitutions\": [\"serif display for a sans visual system\", \"calligraphy body\"],",
|
||||
" \"runtime_candidates_used\": true",
|
||||
"}",
|
||||
"```",
|
||||
"",
|
||||
"## Rules",
|
||||
"",
|
||||
"- The design brief's `visual_system.typography` is authoritative. Keep its category and treatment: serif vs sans-serif vs rounded vs mono, weight, case, and letter spacing.",
|
||||
"- Select explicit `display_font`, `body_font`, and `label_font` stacks before slide authoring. Use `<runtime_font_candidates>` when provided; otherwise use the Font Palette in `svg-reference.md`.",
|
||||
"- Each stack must start with a Latin font and end with a generic fallback such as `serif`, `sans-serif`, `monospace`, or `cursive`.",
|
||||
"- Body text must not use calligraphy, cursive, handwriting, or decorative script fonts.",
|
||||
"- `forbidden_substitutions` must name the tempting but wrong swaps for this deck, such as flipping serif and sans-serif or replacing a technical mono treatment with a cute rounded font.",
|
||||
"- All slide SVG text must use only the planned display/body/label stacks. If a new stack is needed, update `typography_plan.json` first and keep the category rationale explicit.",
|
||||
"",
|
||||
"## Validation Command",
|
||||
"",
|
||||
"```bash",
|
||||
"node skills/lark-slides/scripts/svg_slides_typography_plan.mjs <deck-dir>",
|
||||
"```"
|
||||
].join("\n"));
|
||||
writeFile("validation/prompt-read-trace.md", [
|
||||
"# Prompt Read Trace",
|
||||
@@ -456,7 +572,6 @@ function writeValidationDocs() {
|
||||
" \"path\": \"system-prompt.md\",",
|
||||
" \"role\": \"prompt\",",
|
||||
" \"reason\": \"main workflow\",",
|
||||
" \"source_lines\": \"3-196\",",
|
||||
" \"sha256\": \"...\"",
|
||||
" }",
|
||||
" ],",
|
||||
@@ -470,16 +585,14 @@ function writeValidationDocs() {
|
||||
"- Missing required files fail trace creation.",
|
||||
"- Missing required reads fail validation.",
|
||||
"- A bundle without `receipts/prompt_read_trace.json` must not be marked `publish_ready=true`.",
|
||||
"- If chart snippet routing is uncertain, record `generate-svg-chart/full.md` in `fallbacks` and in `read_order`.",
|
||||
"",
|
||||
"## Source Slice",
|
||||
"",
|
||||
"- Source lines: generated validation contract for prompt read provenance"
|
||||
"- If chart snippet routing is uncertain, record `generate-svg-chart/full.md` in `fallbacks` and in `read_order`."
|
||||
].join("\n"));
|
||||
}
|
||||
|
||||
function updateManifest() {
|
||||
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
||||
manifest.source = "skills/lark-slides/prompt-sources/svg-slides/full.debranded.md";
|
||||
manifest.source_role = "maintainer_prompt_source_not_runtime_generation_context";
|
||||
manifest.sections = manifest.sections.map((section) => {
|
||||
const target = manifestTargets[section.id];
|
||||
if (!target) throw new Error(`missing manifest target for ${section.id}`);
|
||||
@@ -500,6 +613,7 @@ updateManifest();
|
||||
const snippetCount = parseSnippetBundle().length;
|
||||
const report = {
|
||||
root,
|
||||
sourceRoot,
|
||||
dryRun,
|
||||
snippetCount,
|
||||
manifestTargets: Object.keys(manifestTargets).length
|
||||
|
||||
@@ -6,24 +6,25 @@ import { spawnSync } from "node:child_process";
|
||||
import test from "node:test";
|
||||
|
||||
const script = path.resolve("skills/lark-slides/scripts/svg_slides_extract_references.mjs");
|
||||
const realRoot = path.resolve("skills/lark-slides/references/svg-slides");
|
||||
const realSourceRoot = path.resolve("skills/lark-slides/prompt-sources/svg-slides");
|
||||
|
||||
function copyFixture() {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "svg-slides-extract-"));
|
||||
fs.cpSync(path.join(realRoot, "source"), path.join(root, "source"), { recursive: true });
|
||||
const sourceRoot = fs.mkdtempSync(path.join(os.tmpdir(), "svg-slides-extract-source-"));
|
||||
fs.cpSync(realSourceRoot, sourceRoot, { recursive: true });
|
||||
fs.mkdirSync(path.join(root, "examples"), { recursive: true });
|
||||
fs.writeFileSync(path.join(root, "workflow.md"), "# Old Workflow\n");
|
||||
fs.writeFileSync(path.join(root, "protocol.md"), "# Old Protocol\n");
|
||||
return root;
|
||||
return { root, sourceRoot };
|
||||
}
|
||||
|
||||
function run(root) {
|
||||
return spawnSync("node", [script, root], { encoding: "utf8" });
|
||||
function run(root, sourceRoot) {
|
||||
return spawnSync("node", [script, root, "--source-root", sourceRoot], { encoding: "utf8" });
|
||||
}
|
||||
|
||||
test("extracts runtime-unit documents and removes obsolete summaries", () => {
|
||||
const root = copyFixture();
|
||||
const result = run(root);
|
||||
const { root, sourceRoot } = copyFixture();
|
||||
const result = run(root, sourceRoot);
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
assert.ok(fs.existsSync(path.join(root, "system-prompt.md")));
|
||||
assert.ok(fs.existsSync(path.join(root, "svg-reference.md")));
|
||||
@@ -31,18 +32,19 @@ test("extracts runtime-unit documents and removes obsolete summaries", () => {
|
||||
assert.ok(fs.existsSync(path.join(root, "resolve-design-brief", "full.md")));
|
||||
assert.ok(fs.existsSync(path.join(root, "generate-svg-chart", "full.md")));
|
||||
assert.match(fs.readFileSync(path.join(root, "tools", "slides-edit.md"), "utf8"), /- Role: tool_contract/);
|
||||
assert.match(fs.readFileSync(path.join(root, "deck-design-reference-catalog.md"), "utf8"), /- Source lines: 1082-1234/);
|
||||
assert.match(fs.readFileSync(path.join(root, "validation", "README.md"), "utf8"), /\.\.\/\.\.\/\.\.\/scripts\/validate_svg_deck\.mjs/);
|
||||
assert.equal(fs.existsSync(path.join(root, "workflow.md")), false);
|
||||
assert.equal(fs.existsSync(path.join(root, "protocol.md")), false);
|
||||
assert.equal(fs.existsSync(path.join(root, "source")), false);
|
||||
});
|
||||
|
||||
test("updates manifest targets to nested runtime-unit paths", () => {
|
||||
const root = copyFixture();
|
||||
const result = run(root);
|
||||
const { root, sourceRoot } = copyFixture();
|
||||
const result = run(root, sourceRoot);
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
const manifest = JSON.parse(fs.readFileSync(path.join(root, "source", "split-manifest.json"), "utf8"));
|
||||
const manifest = JSON.parse(fs.readFileSync(path.join(sourceRoot, "split-manifest.json"), "utf8"));
|
||||
const byID = new Map(manifest.sections.map((section) => [section.id, section.target]));
|
||||
assert.equal(manifest.source, "skills/lark-slides/prompt-sources/svg-slides/full.debranded.md");
|
||||
assert.equal(byID.get("system_prompt_workflow"), "system-prompt.md");
|
||||
assert.equal(byID.get("resolve_design_brief"), "resolve-design-brief/full.md");
|
||||
assert.equal(byID.get("generate_svg_chart_tool"), "generate-svg-chart/full.md");
|
||||
@@ -50,8 +52,8 @@ test("updates manifest targets to nested runtime-unit paths", () => {
|
||||
});
|
||||
|
||||
test("extracts exactly twenty two chart snippets", () => {
|
||||
const root = copyFixture();
|
||||
const result = run(root);
|
||||
const { root, sourceRoot } = copyFixture();
|
||||
const result = run(root, sourceRoot);
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
const snippetsDir = path.join(root, "generate-svg-chart", "snippets");
|
||||
const snippets = fs.readdirSync(snippetsDir).filter((name) => name.endsWith(".md") && name !== "README.md");
|
||||
@@ -61,8 +63,8 @@ test("extracts exactly twenty two chart snippets", () => {
|
||||
});
|
||||
|
||||
test("generated chart docs do not keep implementation filename noise", () => {
|
||||
const root = copyFixture();
|
||||
const result = run(root);
|
||||
const { root, sourceRoot } = copyFixture();
|
||||
const result = run(root, sourceRoot);
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
const allMarkdown = [];
|
||||
function walk(dir) {
|
||||
@@ -81,19 +83,19 @@ test("generated chart docs do not keep implementation filename noise", () => {
|
||||
assert.equal(/snippets\/\\?\*\.md/.test(joined), false);
|
||||
});
|
||||
|
||||
test("snippet docs preserve specific source line ranges and omit wrapper fence", () => {
|
||||
const root = copyFixture();
|
||||
const result = run(root);
|
||||
test("snippet docs omit source provenance and wrapper fence", () => {
|
||||
const { root, sourceRoot } = copyFixture();
|
||||
const result = run(root, sourceRoot);
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
const waterfall = fs.readFileSync(path.join(root, "generate-svg-chart", "snippets", "waterfall-connector.md"), "utf8");
|
||||
assert.match(waterfall, /- Source lines: \d+-\d+/);
|
||||
assert.match(waterfall, /- Source lines: 2330-2353/);
|
||||
assert.doesNotMatch(waterfall, /Source lines:/);
|
||||
assert.doesNotMatch(waterfall, /Source Slice/);
|
||||
assert.equal(waterfall.trimEnd().endsWith("````"), false);
|
||||
});
|
||||
|
||||
test("strips copied implementation filenames from generated docs", () => {
|
||||
const root = copyFixture();
|
||||
const result = run(root);
|
||||
const { root, sourceRoot } = copyFixture();
|
||||
const result = run(root, sourceRoot);
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
const allMarkdown = [];
|
||||
function walk(dir) {
|
||||
@@ -110,3 +112,24 @@ test("strips copied implementation filenames from generated docs", () => {
|
||||
assert.equal(joined.includes("svgchartservice/service.go"), false);
|
||||
assert.equal(joined.includes("snippets/*.md"), false);
|
||||
});
|
||||
|
||||
test("generated runtime docs do not expose prompt source provenance", () => {
|
||||
const { root, sourceRoot } = copyFixture();
|
||||
const result = run(root, sourceRoot);
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
const allMarkdown = [];
|
||||
function walk(dir) {
|
||||
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||
const filePath = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) walk(filePath);
|
||||
if (entry.isFile() && entry.name.endsWith(".md")) allMarkdown.push(filePath);
|
||||
}
|
||||
}
|
||||
walk(root);
|
||||
const joined = allMarkdown.map((filePath) => fs.readFileSync(filePath, "utf8")).join("\n");
|
||||
assert.doesNotMatch(joined, /Source lines:/);
|
||||
assert.doesNotMatch(joined, /## Source Coverage/);
|
||||
assert.doesNotMatch(joined, /## Source Slice/);
|
||||
assert.doesNotMatch(joined, /source\/full\.debranded\.md/);
|
||||
assert.doesNotMatch(joined, /source\/split-manifest\.json/);
|
||||
});
|
||||
|
||||
@@ -25,11 +25,6 @@ function readText(filePath) {
|
||||
return fs.readFileSync(filePath, "utf8");
|
||||
}
|
||||
|
||||
function sourceLines(markdown) {
|
||||
const match = markdown.match(/^- Source lines:\s*(.+)$/m);
|
||||
return match ? match[1].trim() : "";
|
||||
}
|
||||
|
||||
function role(markdown) {
|
||||
const match = markdown.match(/^- Role:\s*(.+)$/m);
|
||||
return match ? match[1].trim() : "reference";
|
||||
@@ -84,11 +79,15 @@ function requiredReads(referenceRoot, task, chartType, useFullFallback) {
|
||||
["svg-reference.md", "SVG schema and examples"],
|
||||
["deck-design-reference-catalog.md", "narrative archetypes and depth rubric"],
|
||||
["resolve-design-brief/README.md", "design brief read path"],
|
||||
["resolve-design-brief/tool-contract.md", "design brief tool contract"],
|
||||
["resolve-design-brief/style-director-prompt.md", "typography and visual style director prompt"],
|
||||
["resolve-design-brief/full.md", "design brief prompt bundle"],
|
||||
["tools/slide-outline.md", "outline tool contract"],
|
||||
["tools/activate-slides-edit.md", "activate edit mode contract"],
|
||||
["tools/slides-edit.md", "SVG authoring tool contract"],
|
||||
["tools/finish-slides-edit.md", "finish edit mode contract"],
|
||||
["validation/asset-readiness.md", "asset search and visual identity readiness contract"],
|
||||
["validation/typography-plan.md", "typography plan receipt contract"],
|
||||
["validation/prompt-read-trace.md", "prompt read trace receipt contract"]
|
||||
];
|
||||
if (useFullFallback || chartType) {
|
||||
@@ -121,7 +120,6 @@ function buildReadOrder(referenceRoot, entries) {
|
||||
path: relPath,
|
||||
role: role(markdown),
|
||||
reason,
|
||||
source_lines: sourceLines(markdown),
|
||||
sha256: sha256(abs)
|
||||
});
|
||||
}
|
||||
@@ -196,9 +194,6 @@ function validateTrace({ deckDir, referenceRoot, expectedTask = "", expectedChar
|
||||
if (typeof entry.reason !== "string" || entry.reason.length === 0) {
|
||||
fail(`prompt read trace has invalid read_order entry reason: ${entry.path}`, 1);
|
||||
}
|
||||
if (typeof entry.source_lines !== "string" || entry.source_lines.length === 0) {
|
||||
fail(`prompt read trace has invalid read_order entry source_lines: ${entry.path}`, 1);
|
||||
}
|
||||
if (typeof entry.sha256 !== "string" || !shaPattern.test(entry.sha256)) {
|
||||
fail(`prompt read trace has invalid read_order entry sha256: ${entry.path}`, 1);
|
||||
}
|
||||
|
||||
@@ -11,35 +11,39 @@ function tempRoot() {
|
||||
return fs.mkdtempSync(path.join(os.tmpdir(), "svg-slides-prompt-trace-"));
|
||||
}
|
||||
|
||||
function writeDoc(root, relPath, role, sourceLines = "1-1", body = "content") {
|
||||
function writeDoc(root, relPath, role, body = "content") {
|
||||
const filePath = path.join(root, relPath);
|
||||
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
||||
fs.writeFileSync(filePath, `# ${path.basename(relPath, ".md")}\n\n## Context\n\n- Role: ${role}\n- Source lines: ${sourceLines}\n\n## Content\n\n${body}\n`);
|
||||
fs.writeFileSync(filePath, `# ${path.basename(relPath, ".md")}\n\n## Context\n\n- Role: ${role}\n\n## Content\n\n${body}\n`);
|
||||
}
|
||||
|
||||
function writeDeckReferenceSet(root) {
|
||||
writeDoc(root, "README.md", "reference", "1-1");
|
||||
writeDoc(root, "system-prompt.md", "prompt", "3-196");
|
||||
writeDoc(root, "svg-reference.md", "reference", "198-865");
|
||||
writeDoc(root, "deck-design-reference-catalog.md", "reference", "1082-1233");
|
||||
writeDoc(root, "resolve-design-brief/README.md", "reference", "867-1080");
|
||||
writeDoc(root, "resolve-design-brief/full.md", "prompt_bundle", "867-1080");
|
||||
writeDoc(root, "tools/slide-outline.md", "tool_contract", "1236-1254");
|
||||
writeDoc(root, "tools/activate-slides-edit.md", "tool_contract", "1256-1262");
|
||||
writeDoc(root, "tools/slides-edit.md", "tool_contract", "1264-1331");
|
||||
writeDoc(root, "tools/finish-slides-edit.md", "tool_contract", "1333-1339");
|
||||
writeDoc(root, "validation/prompt-read-trace.md", "schema", "generated");
|
||||
writeDoc(root, "README.md", "reference");
|
||||
writeDoc(root, "system-prompt.md", "prompt");
|
||||
writeDoc(root, "svg-reference.md", "reference");
|
||||
writeDoc(root, "deck-design-reference-catalog.md", "reference");
|
||||
writeDoc(root, "resolve-design-brief/README.md", "reference");
|
||||
writeDoc(root, "resolve-design-brief/tool-contract.md", "tool_contract");
|
||||
writeDoc(root, "resolve-design-brief/style-director-prompt.md", "prompt");
|
||||
writeDoc(root, "resolve-design-brief/full.md", "prompt_bundle");
|
||||
writeDoc(root, "tools/slide-outline.md", "tool_contract");
|
||||
writeDoc(root, "tools/activate-slides-edit.md", "tool_contract");
|
||||
writeDoc(root, "tools/slides-edit.md", "tool_contract");
|
||||
writeDoc(root, "tools/finish-slides-edit.md", "tool_contract");
|
||||
writeDoc(root, "validation/asset-readiness.md", "schema");
|
||||
writeDoc(root, "validation/typography-plan.md", "schema");
|
||||
writeDoc(root, "validation/prompt-read-trace.md", "schema");
|
||||
}
|
||||
|
||||
function writeChartReferenceSet(root) {
|
||||
writeDoc(root, "generate-svg-chart/README.md", "reference", "1357-2356");
|
||||
writeDoc(root, "generate-svg-chart/input-schema.md", "schema", "1367-1434");
|
||||
writeDoc(root, "generate-svg-chart/chart-agent-contract.md", "prompt", "1436-1487");
|
||||
writeDoc(root, "generate-svg-chart/design-envelope.md", "reference", "1489-1586");
|
||||
writeDoc(root, "generate-svg-chart/full.md", "prompt_bundle", "1357-2356");
|
||||
writeDoc(root, "generate-svg-chart/snippets/README.md", "reference", "1590-2354");
|
||||
writeDoc(root, "generate-svg-chart/snippets/waterfall-connector.md", "snippet", "2330-2354", "---\nchart_types: [waterfall]\n---\n");
|
||||
writeDoc(root, "generate-svg-chart/snippets/value-line.md", "snippet", "2268-2298", "---\nchart_types: [waterfall, line]\n---\n");
|
||||
writeDoc(root, "generate-svg-chart/README.md", "reference");
|
||||
writeDoc(root, "generate-svg-chart/input-schema.md", "schema");
|
||||
writeDoc(root, "generate-svg-chart/chart-agent-contract.md", "prompt");
|
||||
writeDoc(root, "generate-svg-chart/design-envelope.md", "reference");
|
||||
writeDoc(root, "generate-svg-chart/full.md", "prompt_bundle");
|
||||
writeDoc(root, "generate-svg-chart/snippets/README.md", "reference");
|
||||
writeDoc(root, "generate-svg-chart/snippets/waterfall-connector.md", "snippet", "---\nchart_types: [waterfall]\n---\n");
|
||||
writeDoc(root, "generate-svg-chart/snippets/value-line.md", "snippet", "---\nchart_types: [waterfall, line]\n---\n");
|
||||
}
|
||||
|
||||
function run(args) {
|
||||
@@ -60,10 +64,25 @@ test("creates generate_deck prompt read trace with hashes", () => {
|
||||
assert.equal(receipt.version, "svg-slides.prompt-read-trace.v1");
|
||||
assert.equal(receipt.task, "generate_deck");
|
||||
assert.equal(receipt.read_order[0].path, "README.md");
|
||||
assert.ok(receipt.read_order.some((entry) => entry.path === "resolve-design-brief/style-director-prompt.md"));
|
||||
assert.ok(receipt.read_order.some((entry) => entry.path === "validation/asset-readiness.md"));
|
||||
assert.ok(receipt.read_order.some((entry) => entry.path === "validation/typography-plan.md"));
|
||||
assert.ok(receipt.read_order.every((entry) => /^[a-f0-9]{64}$/.test(entry.sha256)));
|
||||
assert.ok(receipt.read_order.every((entry) => !("source_lines" in entry)));
|
||||
assert.deepEqual(receipt.missing_required_reads, []);
|
||||
});
|
||||
|
||||
test("fails when typography prompt references are missing", () => {
|
||||
const refs = tempRoot();
|
||||
const deck = tempRoot();
|
||||
writeDeckReferenceSet(refs);
|
||||
fs.rmSync(path.join(refs, "resolve-design-brief", "style-director-prompt.md"));
|
||||
const result = run([deck, "--task", "generate_deck", "--references", refs]);
|
||||
assert.equal(result.status, 1);
|
||||
assert.match(result.stderr, /missing required prompt reads/);
|
||||
assert.match(result.stderr, /resolve-design-brief\/style-director-prompt\.md/);
|
||||
});
|
||||
|
||||
test("selects matching chart snippets by chart_type", () => {
|
||||
const refs = tempRoot();
|
||||
const deck = tempRoot();
|
||||
@@ -193,13 +212,6 @@ test("validate fails when read_order entries are missing required metadata", ()
|
||||
},
|
||||
pattern: /invalid read_order entry reason/
|
||||
},
|
||||
{
|
||||
label: "source_lines",
|
||||
mutate(entry) {
|
||||
delete entry.source_lines;
|
||||
},
|
||||
pattern: /invalid read_order entry source_lines/
|
||||
},
|
||||
{
|
||||
label: "sha256",
|
||||
mutate(entry) {
|
||||
|
||||
@@ -2,10 +2,31 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
const rootArg = process.argv.find((arg) => !arg.startsWith("--") && arg !== process.argv[1] && arg !== process.argv[0]);
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
function argValue(name, fallback = "") {
|
||||
const index = args.indexOf(name);
|
||||
return index >= 0 ? args[index + 1] : fallback;
|
||||
}
|
||||
|
||||
function positionalArgs() {
|
||||
const values = [];
|
||||
for (let index = 0; index < args.length; index += 1) {
|
||||
const arg = args[index];
|
||||
if (arg === "--source-root") {
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
if (!arg.startsWith("--")) values.push(arg);
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
const [rootArg] = positionalArgs();
|
||||
const root = path.resolve(rootArg || "skills/lark-slides/references/svg-slides");
|
||||
const json = process.argv.includes("--json");
|
||||
const manifestPath = path.join(root, "source", "split-manifest.json");
|
||||
const sourceRoot = path.resolve(argValue("--source-root", "skills/lark-slides/prompt-sources/svg-slides"));
|
||||
const json = args.includes("--json");
|
||||
const manifestPath = path.join(sourceRoot, "split-manifest.json");
|
||||
|
||||
function readText(file) {
|
||||
return fs.readFileSync(file, "utf8");
|
||||
@@ -31,24 +52,6 @@ function walkMarkdownFiles(dir, files = []) {
|
||||
return files;
|
||||
}
|
||||
|
||||
function coverageBlock(markdown) {
|
||||
const lines = markdown.split(/\r?\n/);
|
||||
const start = lines.findIndex((line) => line.trim() === "## Source Coverage");
|
||||
if (start === -1) return "";
|
||||
const block = [];
|
||||
for (let i = start + 1; i < lines.length; i += 1) {
|
||||
if (/^#{1,2}\s+/.test(lines[i])) break;
|
||||
block.push(lines[i]);
|
||||
}
|
||||
return block.join("\n");
|
||||
}
|
||||
|
||||
function coverageIds(block) {
|
||||
const match = block.match(/^- Covers manifest sections:\s*(.+)$/m);
|
||||
if (!match) return [];
|
||||
return match[1].split(",").map((value) => value.trim()).filter(Boolean);
|
||||
}
|
||||
|
||||
function chartSnippetCount() {
|
||||
const snippetsDir = path.join(root, "generate-svg-chart", "snippets");
|
||||
if (!fs.existsSync(snippetsDir)) return 0;
|
||||
@@ -63,42 +66,38 @@ if (!fs.existsSync(manifestPath)) {
|
||||
}
|
||||
|
||||
const manifest = errors.length === 0 ? JSON.parse(readText(manifestPath)) : { sections: [] };
|
||||
const sectionsById = new Map(manifest.sections.map((section) => [section.id, section]));
|
||||
const seen = new Map();
|
||||
const markdownFiles = walkMarkdownFiles(root);
|
||||
const markdownFilesByRelPath = new Set(markdownFiles.map((filePath) => toPosix(path.relative(root, filePath))));
|
||||
const forbiddenRuntimePatterns = [
|
||||
[/Source lines:/, "Source lines metadata"],
|
||||
[/## Source Coverage/, "Source Coverage block"],
|
||||
[/## Source Slice/, "Source Slice block"],
|
||||
[/source\/full\.debranded\.md/, "runtime source file path"],
|
||||
[/source\/split-manifest\.json/, "runtime manifest path"],
|
||||
[/prompt-sources\/svg-slides\/full\.debranded\.md/, "maintainer source file path"]
|
||||
];
|
||||
|
||||
for (const filePath of markdownFiles) {
|
||||
const relPath = toPosix(path.relative(root, filePath));
|
||||
const block = coverageBlock(readText(filePath));
|
||||
if (!block) continue;
|
||||
const ids = coverageIds(block);
|
||||
if (ids.length === 0) {
|
||||
errors.push(`${relPath}: missing "- Covers manifest sections:" line`);
|
||||
continue;
|
||||
}
|
||||
for (const id of ids) {
|
||||
const section = sectionsById.get(id);
|
||||
if (!section) {
|
||||
errors.push(`${relPath}: unknown manifest section "${id}"`);
|
||||
continue;
|
||||
const markdown = readText(filePath);
|
||||
for (const [pattern, label] of forbiddenRuntimePatterns) {
|
||||
if (pattern.test(markdown)) {
|
||||
errors.push(`${relPath}: contains forbidden runtime provenance marker (${label})`);
|
||||
}
|
||||
if (section.target !== relPath) {
|
||||
errors.push(`${relPath}: section "${id}" belongs to ${section.target}`);
|
||||
}
|
||||
const files = seen.get(id) || [];
|
||||
files.push(relPath);
|
||||
seen.set(id, files);
|
||||
}
|
||||
}
|
||||
|
||||
const coveredTargets = new Set();
|
||||
for (const section of manifest.sections) {
|
||||
const files = seen.get(section.id) || [];
|
||||
if (files.length === 0) {
|
||||
errors.push(`${section.id}: not covered by ${section.target}`);
|
||||
if (!section.target || typeof section.target !== "string") {
|
||||
errors.push(`${section.id}: missing target`);
|
||||
continue;
|
||||
}
|
||||
if (files.length > 1) {
|
||||
errors.push(`${section.id}: covered multiple times by ${files.join(", ")}`);
|
||||
if (!markdownFilesByRelPath.has(section.target)) {
|
||||
errors.push(`${section.id}: target file does not exist in runtime references: ${section.target}`);
|
||||
continue;
|
||||
}
|
||||
coveredTargets.add(section.target);
|
||||
}
|
||||
|
||||
const snippetCount = chartSnippetCount();
|
||||
@@ -109,9 +108,11 @@ if (hasChartTarget && snippetCount !== 22) {
|
||||
|
||||
const report = {
|
||||
root,
|
||||
sourceRoot,
|
||||
manifest: manifestPath,
|
||||
sectionCount: manifest.sections.length,
|
||||
coveredCount: seen.size,
|
||||
coveredCount: errors.length === 0 ? manifest.sections.length : coveredTargets.size,
|
||||
targetCount: coveredTargets.size,
|
||||
snippetCount,
|
||||
errors
|
||||
};
|
||||
|
||||
@@ -9,14 +9,19 @@ const script = path.resolve("skills/lark-slides/scripts/svg_slides_source_covera
|
||||
|
||||
function tempRoot() {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "svg-slides-coverage-"));
|
||||
fs.mkdirSync(path.join(root, "source"), { recursive: true });
|
||||
return root;
|
||||
}
|
||||
|
||||
function writeManifest(root, sections) {
|
||||
fs.writeFileSync(path.join(root, "source", "split-manifest.json"), JSON.stringify({
|
||||
function tempSourceRoot() {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "svg-slides-coverage-source-"));
|
||||
return root;
|
||||
}
|
||||
|
||||
function writeManifest(sourceRoot, sections) {
|
||||
fs.mkdirSync(sourceRoot, { recursive: true });
|
||||
fs.writeFileSync(path.join(sourceRoot, "split-manifest.json"), JSON.stringify({
|
||||
version: "svg-slides.split-manifest.v1",
|
||||
source: "source/full.debranded.md",
|
||||
source: "skills/lark-slides/prompt-sources/svg-slides/full.debranded.md",
|
||||
sections
|
||||
}, null, 2));
|
||||
}
|
||||
@@ -30,60 +35,62 @@ function writeSnippetSet(root, count) {
|
||||
}
|
||||
}
|
||||
|
||||
function run(root) {
|
||||
return spawnSync("node", [script, root, "--json"], { encoding: "utf8" });
|
||||
function run(root, sourceRoot) {
|
||||
return spawnSync("node", [script, root, "--source-root", sourceRoot, "--json"], { encoding: "utf8" });
|
||||
}
|
||||
|
||||
test("passes when each manifest section is covered by its nested target file", () => {
|
||||
const root = tempRoot();
|
||||
writeManifest(root, [{ id: "workflow", lines: [1, 10], target: "workflow/system.md" }]);
|
||||
const sourceRoot = tempSourceRoot();
|
||||
writeManifest(sourceRoot, [{ id: "workflow", lines: [1, 10], target: "workflow/system.md" }]);
|
||||
fs.mkdirSync(path.join(root, "workflow"), { recursive: true });
|
||||
fs.writeFileSync(path.join(root, "workflow", "system.md"), "# Workflow\n\n## Source Coverage\n\n- Covers manifest sections: workflow\n");
|
||||
const result = run(root);
|
||||
fs.writeFileSync(path.join(root, "workflow", "system.md"), "# Workflow\n\n## Content\n\nRuntime rules\n");
|
||||
const result = run(root, sourceRoot);
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
assert.equal(JSON.parse(result.stdout).errors.length, 0);
|
||||
});
|
||||
|
||||
test("fails when a section is missing from Source Coverage", () => {
|
||||
test("fails when a manifest target is missing from runtime references", () => {
|
||||
const root = tempRoot();
|
||||
writeManifest(root, [{ id: "protocol", lines: [1, 10], target: "protocol/schema.md" }]);
|
||||
fs.mkdirSync(path.join(root, "protocol"), { recursive: true });
|
||||
fs.writeFileSync(path.join(root, "protocol", "schema.md"), "# Protocol\n\n## Source Coverage\n\n- Covers manifest sections: other\n");
|
||||
const result = run(root);
|
||||
const sourceRoot = tempSourceRoot();
|
||||
writeManifest(sourceRoot, [{ id: "protocol", lines: [1, 10], target: "protocol/schema.md" }]);
|
||||
const result = run(root, sourceRoot);
|
||||
assert.equal(result.status, 1);
|
||||
const report = JSON.parse(result.stdout);
|
||||
assert.ok(report.errors.some((error) => error.includes("unknown manifest section")));
|
||||
assert.ok(report.errors.some((error) => error.includes("not covered")));
|
||||
assert.ok(report.errors.some((error) => error.includes("target file does not exist")));
|
||||
});
|
||||
|
||||
test("fails when a section is declared by the wrong nested target file", () => {
|
||||
test("fails when runtime references expose source provenance markers", () => {
|
||||
const root = tempRoot();
|
||||
writeManifest(root, [{ id: "visual", lines: [1, 10], target: "design/visual.md" }]);
|
||||
fs.mkdirSync(path.join(root, "workflow"), { recursive: true });
|
||||
fs.writeFileSync(path.join(root, "workflow", "visual.md"), "# Visual\n\n## Source Coverage\n\n- Covers manifest sections: visual\n");
|
||||
const result = run(root);
|
||||
const sourceRoot = tempSourceRoot();
|
||||
writeManifest(sourceRoot, [{ id: "visual", lines: [1, 10], target: "design/visual.md" }]);
|
||||
fs.mkdirSync(path.join(root, "design"), { recursive: true });
|
||||
fs.writeFileSync(path.join(root, "design", "visual.md"), "# Visual\n\n## Source Slice\n\n- Source lines: 1-10\n");
|
||||
const result = run(root, sourceRoot);
|
||||
assert.equal(result.status, 1);
|
||||
assert.ok(JSON.parse(result.stdout).errors.some((error) => error.includes("belongs to design/visual.md")));
|
||||
assert.ok(JSON.parse(result.stdout).errors.some((error) => error.includes("forbidden runtime provenance marker")));
|
||||
});
|
||||
|
||||
test("fails when generate_svg_chart has fewer than twenty two snippets", () => {
|
||||
const root = tempRoot();
|
||||
writeManifest(root, [{ id: "generate_svg_chart_tool", lines: [1, 10], target: "generate-svg-chart/full.md" }]);
|
||||
const sourceRoot = tempSourceRoot();
|
||||
writeManifest(sourceRoot, [{ id: "generate_svg_chart_tool", lines: [1, 10], target: "generate-svg-chart/full.md" }]);
|
||||
fs.mkdirSync(path.join(root, "generate-svg-chart"), { recursive: true });
|
||||
fs.writeFileSync(path.join(root, "generate-svg-chart", "full.md"), "# Chart\n\n## Source Coverage\n\n- Covers manifest sections: generate_svg_chart_tool\n");
|
||||
fs.writeFileSync(path.join(root, "generate-svg-chart", "full.md"), "# Chart\n\n## Content\n\nRuntime chart rules\n");
|
||||
writeSnippetSet(root, 21);
|
||||
const result = run(root);
|
||||
const result = run(root, sourceRoot);
|
||||
assert.equal(result.status, 1);
|
||||
assert.ok(JSON.parse(result.stdout).errors.some((error) => error.includes("expected 22 snippet files")));
|
||||
});
|
||||
|
||||
test("passes when generate_svg_chart has exactly twenty two snippets", () => {
|
||||
const root = tempRoot();
|
||||
writeManifest(root, [{ id: "generate_svg_chart_tool", lines: [1, 10], target: "generate-svg-chart/full.md" }]);
|
||||
const sourceRoot = tempSourceRoot();
|
||||
writeManifest(sourceRoot, [{ id: "generate_svg_chart_tool", lines: [1, 10], target: "generate-svg-chart/full.md" }]);
|
||||
fs.mkdirSync(path.join(root, "generate-svg-chart"), { recursive: true });
|
||||
fs.writeFileSync(path.join(root, "generate-svg-chart", "full.md"), "# Chart\n\n## Source Coverage\n\n- Covers manifest sections: generate_svg_chart_tool\n");
|
||||
fs.writeFileSync(path.join(root, "generate-svg-chart", "full.md"), "# Chart\n\n## Content\n\nRuntime chart rules\n");
|
||||
writeSnippetSet(root, 22);
|
||||
const result = run(root);
|
||||
const result = run(root, sourceRoot);
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
assert.equal(JSON.parse(result.stdout).snippetCount, 22);
|
||||
});
|
||||
|
||||
179
skills/lark-slides/scripts/svg_slides_typography_plan.mjs
Normal file
179
skills/lark-slides/scripts/svg_slides_typography_plan.mjs
Normal file
@@ -0,0 +1,179 @@
|
||||
#!/usr/bin/env node
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
function fail(message, code = 2) {
|
||||
console.error(message);
|
||||
process.exit(code);
|
||||
}
|
||||
|
||||
function readJson(filePath) {
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
||||
} catch (error) {
|
||||
fail(`Invalid typography plan JSON: ${error.message}`, 1);
|
||||
}
|
||||
}
|
||||
|
||||
function stringValue(value) {
|
||||
return typeof value === "string" ? value.trim() : "";
|
||||
}
|
||||
|
||||
function stripQuotes(value) {
|
||||
return value.replace(/^["']+|["']+$/g, "").trim();
|
||||
}
|
||||
|
||||
function normalizeFontName(value) {
|
||||
return stripQuotes(value).replace(/\s+/g, " ").trim();
|
||||
}
|
||||
|
||||
function normalizeFontStack(value) {
|
||||
return stringValue(value)
|
||||
.split(",")
|
||||
.map(normalizeFontName)
|
||||
.filter(Boolean)
|
||||
.join(", ");
|
||||
}
|
||||
|
||||
function stackTokens(value) {
|
||||
return normalizeFontStack(value).split(",").map((token) => token.trim()).filter(Boolean);
|
||||
}
|
||||
|
||||
function containsCjk(value) {
|
||||
return /[\u3400-\u9fff]/u.test(value);
|
||||
}
|
||||
|
||||
function startsWithLatinFont(tokens) {
|
||||
return tokens.length > 0 && /[A-Za-z]/.test(tokens[0]) && !containsCjk(tokens[0]);
|
||||
}
|
||||
|
||||
function isGenericFallback(value) {
|
||||
return ["serif", "sans-serif", "monospace", "cursive"].includes(value.toLowerCase());
|
||||
}
|
||||
|
||||
function hasCalligraphyFont(tokens) {
|
||||
return tokens.some((token) => (
|
||||
/cursive/i.test(token)
|
||||
|| /calligraphy/i.test(token)
|
||||
|| /script/i.test(token)
|
||||
|| /毛草|毛笔|行书|草书|手书|书法|隶书/u.test(token)
|
||||
|| ["Ma Shan Zheng", "Zhi Mang Xing"].includes(token)
|
||||
));
|
||||
}
|
||||
|
||||
function listSvgFiles(root) {
|
||||
const slidesDir = fs.existsSync(path.join(root, "slides")) ? path.join(root, "slides") : root;
|
||||
if (!fs.existsSync(slidesDir)) return [];
|
||||
return fs.readdirSync(slidesDir)
|
||||
.filter((file) => file.endsWith(".svg"))
|
||||
.sort()
|
||||
.map((file) => path.join(slidesDir, file));
|
||||
}
|
||||
|
||||
function decodeXmlAttr(value) {
|
||||
return value
|
||||
.replace(/"/g, "\"")
|
||||
.replace(/'/g, "'")
|
||||
.replace(/&/g, "&");
|
||||
}
|
||||
|
||||
function fontFamiliesInSvg(source) {
|
||||
const families = [];
|
||||
const stylePattern = /\bstyle\s*=\s*(["'])(.*?)\1/gis;
|
||||
for (const match of source.matchAll(stylePattern)) {
|
||||
const style = decodeXmlAttr(match[2]);
|
||||
for (const declaration of style.split(";")) {
|
||||
const [property, ...rest] = declaration.split(":");
|
||||
if (!property || property.trim().toLowerCase() !== "font-family") continue;
|
||||
const value = rest.join(":").trim();
|
||||
if (value) families.push(value);
|
||||
}
|
||||
}
|
||||
|
||||
const attrPattern = /\bfont-family\s*=\s*(["'])(.*?)\1/gis;
|
||||
for (const match of source.matchAll(attrPattern)) {
|
||||
if (match[0].toLowerCase().startsWith("style")) continue;
|
||||
const value = decodeXmlAttr(match[2]).trim();
|
||||
if (value) families.push(value);
|
||||
}
|
||||
return families;
|
||||
}
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const deckArg = args.find((arg) => !arg.startsWith("--"));
|
||||
const json = args.includes("--json");
|
||||
if (!deckArg) {
|
||||
fail("Usage: node skills/lark-slides/scripts/svg_slides_typography_plan.mjs <deck-dir> [--json]");
|
||||
}
|
||||
|
||||
const root = path.resolve(deckArg);
|
||||
const receiptPath = path.join(root, "receipts", "typography_plan.json");
|
||||
if (!fs.existsSync(receiptPath)) {
|
||||
fail(`Missing typography plan receipt: ${receiptPath}`, 1);
|
||||
}
|
||||
|
||||
const receipt = readJson(receiptPath);
|
||||
const errors = [];
|
||||
if (receipt.version !== "svg-slides.typography-plan.v1") {
|
||||
errors.push(`invalid typography plan version: ${receipt.version}`);
|
||||
}
|
||||
|
||||
for (const field of ["font_category", "display_font", "body_font", "label_font", "source", "selection_reason"]) {
|
||||
if (!stringValue(receipt[field])) errors.push(`${field} is required`);
|
||||
}
|
||||
if (!Array.isArray(receipt.forbidden_substitutions)) {
|
||||
errors.push("forbidden_substitutions must be an array");
|
||||
}
|
||||
if (typeof receipt.runtime_candidates_used !== "boolean") {
|
||||
errors.push("runtime_candidates_used must be a boolean");
|
||||
}
|
||||
|
||||
const plannedStacks = new Set();
|
||||
for (const field of ["display_font", "body_font", "label_font"]) {
|
||||
const normalized = normalizeFontStack(receipt[field]);
|
||||
if (normalized) plannedStacks.add(normalized);
|
||||
const tokens = stackTokens(receipt[field]);
|
||||
if (tokens.length > 0 && !startsWithLatinFont(tokens)) {
|
||||
errors.push(`${field} must start with a Latin font`);
|
||||
}
|
||||
if (tokens.length > 0 && !isGenericFallback(tokens[tokens.length - 1])) {
|
||||
errors.push(`${field} must end with a generic fallback`);
|
||||
}
|
||||
if (field === "body_font" && hasCalligraphyFont(tokens)) {
|
||||
errors.push("body_font must not use calligraphy fonts");
|
||||
}
|
||||
}
|
||||
|
||||
const svgFiles = listSvgFiles(root);
|
||||
if (svgFiles.length === 0) {
|
||||
errors.push("no slide SVG files found for typography validation");
|
||||
}
|
||||
|
||||
const encounteredStacks = new Set();
|
||||
for (const filePath of svgFiles) {
|
||||
const relPath = path.relative(root, filePath).split(path.sep).join("/");
|
||||
const source = fs.readFileSync(filePath, "utf8");
|
||||
for (const family of fontFamiliesInSvg(source)) {
|
||||
const normalized = normalizeFontStack(family);
|
||||
if (!normalized) continue;
|
||||
encounteredStacks.add(normalized);
|
||||
if (!plannedStacks.has(normalized)) {
|
||||
errors.push(`font-family not allowed by typography_plan: ${normalized} in ${relPath}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const report = {
|
||||
ok: errors.length === 0,
|
||||
receipt: receiptPath,
|
||||
fontFamiliesChecked: encounteredStacks.size,
|
||||
errors
|
||||
};
|
||||
|
||||
if (errors.length > 0) {
|
||||
if (json) console.error(JSON.stringify(report, null, 2));
|
||||
else for (const error of errors) console.error(error);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(JSON.stringify(report, null, 2));
|
||||
@@ -0,0 +1,92 @@
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import test from "node:test";
|
||||
|
||||
const script = path.resolve("skills/lark-slides/scripts/svg_slides_typography_plan.mjs");
|
||||
|
||||
function tempDeck() {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "svg-slides-typography-"));
|
||||
fs.mkdirSync(path.join(root, "slides"), { recursive: true });
|
||||
fs.writeFileSync(path.join(root, "slides", "slide_01.svg"), `<svg xmlns="http://www.w3.org/2000/svg" xmlns:slide="https://slides.bytedance.com/ns" slide:role="slide" id="slide_01" viewBox="0 0 960 540">
|
||||
<rect slide:role="background" x="0" y="0" width="960" height="540" fill="rgba(255,255,255,1)"/>
|
||||
<foreignObject slide:role="shape" slide:shape-type="text" x="80" y="80" width="600" height="80" style="font-size:32px;font-family:DM Sans, 黑体, sans-serif;color:rgba(15,23,42,1);line-height:1.2;letter-spacing:0px;padding:0px">
|
||||
<p xmlns="http://www.w3.org/1999/xhtml" style="margin:0px;font-size:32px;color:rgba(15,23,42,1)">Typography</p>
|
||||
</foreignObject>
|
||||
</svg>`);
|
||||
return root;
|
||||
}
|
||||
|
||||
function validPlan(overrides = {}) {
|
||||
return {
|
||||
version: "svg-slides.typography-plan.v1",
|
||||
font_category: "clean sans display and body",
|
||||
display_font: "DM Sans, 黑体, sans-serif",
|
||||
body_font: "DM Sans, 黑体, sans-serif",
|
||||
label_font: "DM Sans, 黑体, sans-serif",
|
||||
source: "visual_system.typography",
|
||||
selection_reason: "The clean sans pairing matches a technical deck and keeps body text readable.",
|
||||
forbidden_substitutions: ["calligraphy body", "rounded cute fonts"],
|
||||
runtime_candidates_used: false,
|
||||
...overrides
|
||||
};
|
||||
}
|
||||
|
||||
function writePlan(root, plan) {
|
||||
fs.mkdirSync(path.join(root, "receipts"), { recursive: true });
|
||||
fs.writeFileSync(path.join(root, "receipts", "typography_plan.json"), `${JSON.stringify(plan, null, 2)}\n`);
|
||||
}
|
||||
|
||||
function run(root) {
|
||||
return spawnSync("node", [script, root], { encoding: "utf8" });
|
||||
}
|
||||
|
||||
test("typography plan accepts a valid deck font plan", () => {
|
||||
const root = tempDeck();
|
||||
writePlan(root, validPlan());
|
||||
const result = run(root);
|
||||
assert.equal(result.status, 0, result.stderr || result.stdout);
|
||||
const report = JSON.parse(result.stdout);
|
||||
assert.equal(report.ok, true);
|
||||
assert.equal(report.fontFamiliesChecked, 1);
|
||||
});
|
||||
|
||||
test("typography plan rejects missing receipt", () => {
|
||||
const root = tempDeck();
|
||||
const result = run(root);
|
||||
assert.equal(result.status, 1);
|
||||
assert.match(result.stderr, /Missing typography plan receipt/);
|
||||
});
|
||||
|
||||
test("typography plan rejects font stacks with Chinese first", () => {
|
||||
const root = tempDeck();
|
||||
writePlan(root, validPlan({ display_font: "黑体, DM Sans, sans-serif" }));
|
||||
const result = run(root);
|
||||
assert.equal(result.status, 1);
|
||||
assert.match(result.stderr, /display_font must start with a Latin font/);
|
||||
});
|
||||
|
||||
test("typography plan rejects calligraphy body fonts", () => {
|
||||
const root = tempDeck();
|
||||
writePlan(root, validPlan({ body_font: "EB Garamond, 钟齐流江毛草, cursive" }));
|
||||
const result = run(root);
|
||||
assert.equal(result.status, 1);
|
||||
assert.match(result.stderr, /body_font must not use calligraphy fonts/);
|
||||
});
|
||||
|
||||
test("typography plan rejects SVG text using a font outside the plan", () => {
|
||||
const root = tempDeck();
|
||||
fs.writeFileSync(path.join(root, "slides", "slide_01.svg"), `<svg xmlns="http://www.w3.org/2000/svg" xmlns:slide="https://slides.bytedance.com/ns" slide:role="slide" id="slide_01" viewBox="0 0 960 540">
|
||||
<rect slide:role="background" x="0" y="0" width="960" height="540" fill="rgba(255,255,255,1)"/>
|
||||
<foreignObject slide:role="shape" slide:shape-type="text" x="80" y="80" width="600" height="80" style="font-size:32px;font-family:Playfair Display, 寒蝉锦书宋, serif;color:rgba(15,23,42,1);line-height:1.2;letter-spacing:0px;padding:0px">
|
||||
<p xmlns="http://www.w3.org/1999/xhtml" style="margin:0px;font-size:32px;color:rgba(15,23,42,1)">Typography</p>
|
||||
</foreignObject>
|
||||
</svg>`);
|
||||
writePlan(root, validPlan());
|
||||
const result = run(root);
|
||||
assert.equal(result.status, 1);
|
||||
assert.match(result.stderr, /font-family not allowed by typography_plan/);
|
||||
});
|
||||
|
||||
101
skills/lark-slides/scripts/svg_slides_visual_identity_check.mjs
Normal file
101
skills/lark-slides/scripts/svg_slides_visual_identity_check.mjs
Normal file
@@ -0,0 +1,101 @@
|
||||
#!/usr/bin/env node
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
function fail(message, code = 2) {
|
||||
console.error(message);
|
||||
process.exit(code);
|
||||
}
|
||||
|
||||
function readJson(filePath) {
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
||||
} catch (error) {
|
||||
fail(`Invalid asset readiness JSON: ${error.message}`, 1);
|
||||
}
|
||||
}
|
||||
|
||||
function normalize(value) {
|
||||
return typeof value === "string" ? value.trim().toLowerCase() : "";
|
||||
}
|
||||
|
||||
function setJaccard(left, right) {
|
||||
const a = new Set(left);
|
||||
const b = new Set(right);
|
||||
const union = new Set([...a, ...b]);
|
||||
if (union.size === 0) return 0;
|
||||
let intersection = 0;
|
||||
for (const item of a) {
|
||||
if (b.has(item)) intersection += 1;
|
||||
}
|
||||
return intersection / union.size;
|
||||
}
|
||||
|
||||
function loadDeck(rootArg) {
|
||||
const root = path.resolve(rootArg);
|
||||
const receiptPath = path.join(root, "receipts", "asset_readiness.json");
|
||||
if (!fs.existsSync(receiptPath)) {
|
||||
fail(`Missing asset readiness receipt: ${receiptPath}`, 1);
|
||||
}
|
||||
const receipt = readJson(receiptPath);
|
||||
const identity = receipt.visual_identity_fingerprint || {};
|
||||
const layoutFamilies = (Array.isArray(receipt.slides) ? receipt.slides : [])
|
||||
.map((slide) => normalize(slide.layout_family))
|
||||
.filter(Boolean);
|
||||
return {
|
||||
root,
|
||||
receiptPath,
|
||||
identity: {
|
||||
style_family: normalize(identity.style_family),
|
||||
composition_signature: normalize(identity.composition_signature),
|
||||
imagery_treatment: normalize(identity.imagery_treatment),
|
||||
palette_signature: normalize(identity.palette_signature)
|
||||
},
|
||||
layoutFamilies
|
||||
};
|
||||
}
|
||||
|
||||
const roots = process.argv.slice(2).filter((arg) => !arg.startsWith("--"));
|
||||
if (roots.length === 0) {
|
||||
fail("Usage: node skills/lark-slides/scripts/svg_slides_visual_identity_check.mjs <deck-dir> [<deck-dir>...]");
|
||||
}
|
||||
|
||||
const decks = roots.map(loadDeck);
|
||||
const errors = [];
|
||||
const pairs = [];
|
||||
for (let i = 0; i < decks.length; i += 1) {
|
||||
for (let j = i + 1; j < decks.length; j += 1) {
|
||||
const left = decks[i];
|
||||
const right = decks[j];
|
||||
const sameStyle = left.identity.style_family && left.identity.style_family === right.identity.style_family;
|
||||
const sameComposition = left.identity.composition_signature && left.identity.composition_signature === right.identity.composition_signature;
|
||||
const sameImagery = left.identity.imagery_treatment && left.identity.imagery_treatment === right.identity.imagery_treatment;
|
||||
const layoutSimilarity = setJaccard(left.layoutFamilies, right.layoutFamilies);
|
||||
const tooSimilar = sameStyle && sameComposition && (sameImagery || layoutSimilarity >= 0.75);
|
||||
pairs.push({
|
||||
left: left.root,
|
||||
right: right.root,
|
||||
sameStyle,
|
||||
sameComposition,
|
||||
sameImagery,
|
||||
layoutSimilarity
|
||||
});
|
||||
if (tooSimilar) {
|
||||
errors.push(`visual identity too similar: ${left.root} vs ${right.root}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const report = {
|
||||
ok: errors.length === 0,
|
||||
deckCount: decks.length,
|
||||
pairs,
|
||||
errors
|
||||
};
|
||||
|
||||
if (errors.length > 0) {
|
||||
for (const error of errors) console.error(error);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(JSON.stringify(report, null, 2));
|
||||
@@ -0,0 +1,79 @@
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import test from "node:test";
|
||||
|
||||
const script = path.resolve("skills/lark-slides/scripts/svg_slides_visual_identity_check.mjs");
|
||||
|
||||
function deckRoot(name) {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), `svg-slides-identity-${name}-`));
|
||||
fs.mkdirSync(path.join(root, "receipts"), { recursive: true });
|
||||
return root;
|
||||
}
|
||||
|
||||
function writeIdentity(root, identity, layouts) {
|
||||
fs.writeFileSync(path.join(root, "receipts", "asset_readiness.json"), `${JSON.stringify({
|
||||
version: "svg-slides.asset-readiness.v1",
|
||||
visual_identity_fingerprint: identity,
|
||||
required_search_entities: [],
|
||||
assets: [
|
||||
{
|
||||
slide: "slides/slide_01.svg",
|
||||
purpose: "hero visual",
|
||||
source_type: "generated",
|
||||
status: "ready",
|
||||
local_path: path.join(root, "hero.png"),
|
||||
assignment_reason: "test"
|
||||
}
|
||||
],
|
||||
slides: layouts.map((layout, index) => ({
|
||||
file: `slides/slide_${String(index + 1).padStart(2, "0")}.svg`,
|
||||
layout_family: layout,
|
||||
visual_assets: [path.join(root, "hero.png")]
|
||||
}))
|
||||
}, null, 2)}\n`);
|
||||
}
|
||||
|
||||
function run(roots) {
|
||||
return spawnSync("node", [script, ...roots], { encoding: "utf8" });
|
||||
}
|
||||
|
||||
test("visual identity check rejects two decks with the same visual fingerprint and layout families", () => {
|
||||
const first = deckRoot("first");
|
||||
const second = deckRoot("second");
|
||||
const identity = {
|
||||
style_family: "generic-business-report",
|
||||
composition_signature: "card-grid-plus-bars",
|
||||
imagery_treatment: "flat vector placeholders",
|
||||
palette_signature: "blue-orange-white"
|
||||
};
|
||||
const layouts = ["cover-title-visual", "three-card-grid", "bar-table-split", "closing-summary"];
|
||||
writeIdentity(first, identity, layouts);
|
||||
writeIdentity(second, identity, layouts);
|
||||
const result = run([first, second]);
|
||||
assert.equal(result.status, 1);
|
||||
assert.match(result.stderr, /visual identity too similar/);
|
||||
});
|
||||
|
||||
test("visual identity check accepts decks with distinct style families and layout vocabularies", () => {
|
||||
const first = deckRoot("first");
|
||||
const second = deckRoot("second");
|
||||
writeIdentity(first, {
|
||||
style_family: "technical-paper-terminal",
|
||||
composition_signature: "asymmetric-code-panels",
|
||||
imagery_treatment: "paper screenshots and architecture diagrams",
|
||||
palette_signature: "navy-cyan-orange"
|
||||
}, ["cover-terminal-hero", "attention-funnel", "architecture-schematic", "paper-evidence-strip"]);
|
||||
writeIdentity(second, {
|
||||
style_family: "equity-research-terminal",
|
||||
composition_signature: "dense-scorecard-dashboard",
|
||||
imagery_treatment: "brand photos and valuation matrices",
|
||||
palette_signature: "ink-green-red-amber"
|
||||
}, ["ticker-board-cover", "company-logo-grid", "valuation-matrix", "scorecard-table"]);
|
||||
const result = run([first, second]);
|
||||
assert.equal(result.status, 0, result.stderr || result.stdout);
|
||||
const report = JSON.parse(result.stdout);
|
||||
assert.equal(report.ok, true);
|
||||
});
|
||||
Reference in New Issue
Block a user