test(svglide): close theme system p0 plan evidence

This commit is contained in:
songtianyi.theo
2026-06-21 21:37:31 +08:00
parent 842e2d44de
commit 4a37a2fe7b
40 changed files with 1620 additions and 4 deletions

View File

@@ -0,0 +1,308 @@
# SVGlide Theme System P0 Evidence
This file is the reviewer-facing evidence for the Theme System P0 plan in
`/Users/bytedance/Desktop/SVGlide-Theme-System-立项文档.md`.
## Scope
P0 proves a minimal executable loop:
- ThemeSpec exists and rejects invalid themes.
- `theme_validate` validates plan/canvas `theme_id` against the registry.
- `theme_adherence` checks final `04-svg/prepared/*.svg` colors and direct
text contrast.
- `quality_gate` consumes theme receipts and freshness.
- `production_live` requires a current human `pre_submit_review` before
`live_create`.
- `direct_svg` is not blocked by artboard package checks.
P0 does not claim automatic aesthetics, image/gradient sampling, full CSS
cascade, editor UI, slide server changes, PPTX export, animation, narrated
output, or a complete theme/template product surface.
P1/P2 are not targets for this evidence slice. In particular, this file does
not claim model-driven theme extraction, productized theme authoring UI,
cross-deck theme migration, export packaging, or automated aesthetic approval.
## Branch And Baseline
Observed during this evidence update:
```text
worktree: /Users/bytedance/bd-projects/workspaces/SVGlide/.worktrees/cli-svglide-svg-private
branch: feat/svglide-artboard-satori
implementation base HEAD before evidence patch: 842e2d44dec7a16f1b432687e2ae4e49fb9b2333
```
The original document's section 0 described an older target HEAD where many
files were still dirty overlay. Current branch HEAD already contains the Theme
System implementation files from commit `842e2d44`.
## P0 Checklist
| Item | Status | Evidence |
| --- | --- | --- |
| P0-0 baseline normalization | PASS | artboard renderer, package check, canvas schema, and template registry are tracked in HEAD |
| P0-1 ThemeSpec schema | PASS | `references/svglide-theme-spec.schema.json`; `svglide_theme_test.py` |
| P0-2 theme utilities | PASS | `svglide_theme.py` covers registry/theme load, stable hashes, static SVG colors, contrast |
| P0-3 theme_validate | PASS | `svglide_theme_validate.py` writes `06-check/theme-validate.json` and `receipts/theme-validate.json` |
| P0-4 theme_adherence | PASS | `svglide_theme_adherence.py` checks final prepared SVG colors, contrast, and stale theme validation |
| P0-5 pre_submit_review | PASS | `svglide_pre_submit_review.py` requires structured human approval and freshness |
| P0-6a runner theme/package stages | PASS | `svglide_project_runner.py` supports `theme_validate`, `package_check`, `theme_adherence` |
| P0-6b production_live submit gate | PASS | `production_live` path requires current `pre_submit_review` before `live_create` |
| P0-7 quality_gate integration | PASS | `svglide_quality_gate.py` consumes theme receipts; `artboard_satori` requires package check; `direct_svg` does not |
| P0-8 fixtures/evidence/regression | PASS | positive and negative fixtures added under `fixtures/svglide_artboard/theme-system-*`; this file records commands |
Contract details closed during this update:
- `pre_submit_review` accepts both the keyed object format and the document
example format `[{kind, path, sha256}]` for `reviewed_artifacts`.
- runner `pre_submit_review` uses the documented default human input file:
`06-check/pre-submit-human-review.json`.
## Fixture Coverage
Positive fixtures:
- `skills/lark-slides/scripts/fixtures/svglide_artboard/theme-system-p0/artboard-satori`
- three pages: cover, content, closing
- `generation_mode=artboard_satori`
- expected: `theme_validate=passed`, `theme_adherence=passed`
- `skills/lark-slides/scripts/fixtures/svglide_artboard/theme-system-p0/direct-svg`
- one direct SVG page
- expected: `theme_validate=passed`, `theme_adherence=passed`
- used to document that `direct_svg` does not require artboard package check
Negative fixtures:
- `skills/lark-slides/scripts/fixtures/svglide_artboard/theme-system-invalid/unknown-color`
- expected issue: `theme_unknown_color`
- `skills/lark-slides/scripts/fixtures/svglide_artboard/theme-system-invalid/low-contrast`
- expected issue: `contrast_too_low`
- `skills/lark-slides/scripts/fixtures/svglide_artboard/theme-system-invalid/stale-theme-validate`
- expected issue: `theme_validate_plan_stale`
The fixture regression is executable through:
```bash
env PYTHONPYCACHEPREFIX=/private/tmp/svglide-pycache \
python3 -m unittest skills/lark-slides/scripts/svglide_theme_system_p0_fixture_test.py
```
## P0-8 Receipt Paths
Positive receipt paths written by the fixture commands:
```text
skills/lark-slides/scripts/fixtures/svglide_artboard/theme-system-p0/artboard-satori/06-check/theme-validate.json
skills/lark-slides/scripts/fixtures/svglide_artboard/theme-system-p0/artboard-satori/receipts/theme-validate.json
skills/lark-slides/scripts/fixtures/svglide_artboard/theme-system-p0/artboard-satori/06-check/theme-adherence.json
skills/lark-slides/scripts/fixtures/svglide_artboard/theme-system-p0/artboard-satori/receipts/theme-adherence.json
skills/lark-slides/scripts/fixtures/svglide_artboard/theme-system-p0/direct-svg/06-check/theme-validate.json
skills/lark-slides/scripts/fixtures/svglide_artboard/theme-system-p0/direct-svg/receipts/theme-validate.json
skills/lark-slides/scripts/fixtures/svglide_artboard/theme-system-p0/direct-svg/06-check/theme-adherence.json
skills/lark-slides/scripts/fixtures/svglide_artboard/theme-system-p0/direct-svg/receipts/theme-adherence.json
```
Negative receipt paths written by the fixture commands:
```text
skills/lark-slides/scripts/fixtures/svglide_artboard/theme-system-invalid/unknown-color/06-check/theme-validate.json
skills/lark-slides/scripts/fixtures/svglide_artboard/theme-system-invalid/unknown-color/receipts/theme-validate.json
skills/lark-slides/scripts/fixtures/svglide_artboard/theme-system-invalid/unknown-color/06-check/theme-adherence.json
skills/lark-slides/scripts/fixtures/svglide_artboard/theme-system-invalid/unknown-color/receipts/theme-adherence.json
skills/lark-slides/scripts/fixtures/svglide_artboard/theme-system-invalid/low-contrast/06-check/theme-validate.json
skills/lark-slides/scripts/fixtures/svglide_artboard/theme-system-invalid/low-contrast/receipts/theme-validate.json
skills/lark-slides/scripts/fixtures/svglide_artboard/theme-system-invalid/low-contrast/06-check/theme-adherence.json
skills/lark-slides/scripts/fixtures/svglide_artboard/theme-system-invalid/low-contrast/receipts/theme-adherence.json
skills/lark-slides/scripts/fixtures/svglide_artboard/theme-system-invalid/stale-theme-validate/06-check/theme-validate.json
skills/lark-slides/scripts/fixtures/svglide_artboard/theme-system-invalid/stale-theme-validate/receipts/theme-validate.json
skills/lark-slides/scripts/fixtures/svglide_artboard/theme-system-invalid/stale-theme-validate/06-check/theme-adherence.json
skills/lark-slides/scripts/fixtures/svglide_artboard/theme-system-invalid/stale-theme-validate/receipts/theme-adherence.json
```
## Commands Run
Command templates used for reviewer reproduction:
```bash
env PYTHONPYCACHEPREFIX=/private/tmp/svglide-pycache \
python3 skills/lark-slides/scripts/svglide_theme_validate.py <project_root> --pretty
env PYTHONPYCACHEPREFIX=/private/tmp/svglide-pycache \
python3 skills/lark-slides/scripts/svglide_theme_adherence.py <project_root> --pretty
env PYTHONPYCACHEPREFIX=/private/tmp/svglide-pycache \
python3 -m unittest <test_file_or_test_case>
```
Real fixture commands run for this P0-8 evidence pass:
```bash
git -C /Users/bytedance/bd-projects/workspaces/SVGlide/.worktrees/cli-svglide-svg-private status --short --branch
git -C /Users/bytedance/bd-projects/workspaces/SVGlide/.worktrees/cli-svglide-svg-private rev-parse HEAD
env PYTHONPYCACHEPREFIX=/private/tmp/svglide-pycache \
python3 skills/lark-slides/scripts/svglide_theme_validate.py \
skills/lark-slides/scripts/fixtures/svglide_artboard/theme-system-p0/artboard-satori \
--pretty
env PYTHONPYCACHEPREFIX=/private/tmp/svglide-pycache \
python3 skills/lark-slides/scripts/svglide_theme_validate.py \
skills/lark-slides/scripts/fixtures/svglide_artboard/theme-system-p0/direct-svg \
--pretty
env PYTHONPYCACHEPREFIX=/private/tmp/svglide-pycache \
python3 skills/lark-slides/scripts/svglide_theme_validate.py \
skills/lark-slides/scripts/fixtures/svglide_artboard/theme-system-invalid/unknown-color \
--pretty
env PYTHONPYCACHEPREFIX=/private/tmp/svglide-pycache \
python3 skills/lark-slides/scripts/svglide_theme_validate.py \
skills/lark-slides/scripts/fixtures/svglide_artboard/theme-system-invalid/low-contrast \
--pretty
env PYTHONPYCACHEPREFIX=/private/tmp/svglide-pycache \
python3 skills/lark-slides/scripts/svglide_theme_adherence.py \
skills/lark-slides/scripts/fixtures/svglide_artboard/theme-system-p0/artboard-satori \
--pretty
env PYTHONPYCACHEPREFIX=/private/tmp/svglide-pycache \
python3 skills/lark-slides/scripts/svglide_theme_adherence.py \
skills/lark-slides/scripts/fixtures/svglide_artboard/theme-system-p0/direct-svg \
--pretty
env PYTHONPYCACHEPREFIX=/private/tmp/svglide-pycache \
python3 skills/lark-slides/scripts/svglide_theme_adherence.py \
skills/lark-slides/scripts/fixtures/svglide_artboard/theme-system-invalid/unknown-color \
--pretty
env PYTHONPYCACHEPREFIX=/private/tmp/svglide-pycache \
python3 skills/lark-slides/scripts/svglide_theme_adherence.py \
skills/lark-slides/scripts/fixtures/svglide_artboard/theme-system-invalid/low-contrast \
--pretty
env PYTHONPYCACHEPREFIX=/private/tmp/svglide-pycache \
python3 skills/lark-slides/scripts/svglide_theme_adherence.py \
skills/lark-slides/scripts/fixtures/svglide_artboard/theme-system-invalid/stale-theme-validate \
--pretty
env PYTHONPYCACHEPREFIX=/private/tmp/svglide-pycache \
python3 -m unittest \
skills/lark-slides/scripts/svglide_theme_validate_test.py \
skills/lark-slides/scripts/svglide_theme_adherence_test.py
env PYTHONPYCACHEPREFIX=/private/tmp/svglide-pycache \
python3 -m unittest skills/lark-slides/scripts/svglide_theme_system_p0_fixture_test.py
```
Observed P0-8 command results:
```text
branch: feat/svglide-artboard-satori
HEAD: 842e2d44dec7a16f1b432687e2ae4e49fb9b2333
artboard-satori theme_validate: passed, page_count=3, theme_count=1
artboard-satori theme_adherence: passed, prepared_svg_count=3
direct-svg theme_validate: passed, page_count=1, theme_count=1
direct-svg theme_adherence: passed, prepared_svg_count=1
unknown-color theme_validate: passed
unknown-color theme_adherence: failed as expected with theme_unknown_color
low-contrast theme_validate: passed
low-contrast theme_adherence: failed as expected with contrast_too_low
stale-theme-validate theme_adherence: failed as expected with theme_validate_plan_stale
svglide_theme_validate_test.py + svglide_theme_adherence_test.py: Ran 11 tests, OK
theme_system_p0_fixture_test.py: Ran 4 tests, OK
```
Targeted P0 tests:
```bash
env PYTHONPYCACHEPREFIX=/private/tmp/svglide-pycache \
python3 -m unittest \
skills/lark-slides/scripts/svglide_theme_test.py \
skills/lark-slides/scripts/svglide_theme_validate_test.py \
skills/lark-slides/scripts/svglide_theme_adherence_test.py \
skills/lark-slides/scripts/svglide_pre_submit_review_test.py \
skills/lark-slides/scripts/svglide_project_runner_test.py \
skills/lark-slides/scripts/svglide_quality_gate_test.py \
skills/lark-slides/scripts/svglide_theme_system_p0_fixture_test.py
```
Result:
```text
Ran 103 tests in 8.395s
OK
```
Full scripts unittest discovery:
```bash
env PYTHONPYCACHEPREFIX=/private/tmp/svglide-pycache \
python3 -m unittest discover skills/lark-slides/scripts -p '*_test.py'
```
Result:
```text
Ran 335 tests in 16.636s
OK
```
Package check tests:
```bash
env PYTHONPYCACHEPREFIX=/private/tmp/svglide-pycache \
python3 -m unittest skills/lark-slides/scripts/svglide_artboard_package_check_test.py
```
Result:
```text
Ran 4 tests in 0.127s
OK
```
Go root package test:
```bash
env GOCACHE=/private/tmp/svglide-gocache go test .
```
Result:
```text
ok github.com/larksuite/cli 0.739s
```
Package runtime check:
```bash
env PYTHONPYCACHEPREFIX=/private/tmp/svglide-pycache \
python3 skills/lark-slides/scripts/svglide_artboard_package_check.py \
--output-dir /private/tmp/svglide-theme-system-p0-package-check-final \
--pretty
```
Result:
```text
status: passed
runtime_check_count: 2
output receipts:
/private/tmp/svglide-theme-system-p0-package-check-final/06-check/artboard-package-check.json
/private/tmp/svglide-theme-system-p0-package-check-final/receipts/artboard-package-check.json
```
## Key Runtime Boundaries
- `pre_submit_review` is a human receipt validator. It is not an automatic
aesthetics model.
- `theme_adherence` only checks static SVG colors and direct text/background
contrast in final prepared SVG files.
- Satori SVG remains an artboard preview/intermediate artifact; final theme
adherence is checked on `04-svg/prepared/*.svg`.
- `direct_svg` still runs theme checks, but it must not require
`artboard-package-check`.
- `export` is outside Theme System P0. The P0 plan explicitly does not claim
PPTX or narrated deck output.

View File

@@ -0,0 +1,9 @@
Theme System P0 negative fixtures.
Each child directory is a project root for a specific failing condition:
- `unknown-color`: prepared SVG contains a color outside the active ThemeSpec.
- `low-contrast`: prepared SVG uses allowed colors but fails text contrast.
- `stale-theme-validate`: `theme-validate.json` is intentionally stale.
These fixtures are exercised by `svglide_theme_system_p0_fixture_test.py`.

View File

@@ -0,0 +1,16 @@
{
"generation_mode": "artboard_satori",
"language": "zh-CN",
"theme_id": "dark-clarity",
"slides": [
{
"page": 1,
"title": "低对比度负例",
"canvas_spec": {
"version": "svglide-canvas-spec/v1",
"template_id": "cover-hero",
"theme_id": "dark-clarity"
}
}
]
}

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="960" height="540" viewBox="0 0 960 540"><rect width="960" height="540" fill="#0F172A"/><text x="88" y="140" fill="#111827">低对比度负例</text></svg>

After

Width:  |  Height:  |  Size: 202 B

View File

@@ -0,0 +1,63 @@
{
"action": "repair_and_rerun",
"checked_at": "2026-06-21T21:34:08+08:00",
"contrast_failures": [
{
"min": 4.5,
"page": 1,
"path": "04-svg/prepared/page-001.svg",
"ratio": 1.0064,
"text_index": 0
}
],
"contrast_unresolved": [],
"inputs": {
"plan_sha256": "9d54b389938c4a1d52a2ff9ef4cffacf7fc90d4282aee5154ba85d1b78198fc3",
"slide_plan": "02-plan/slide_plan.json",
"theme_validate": "06-check/theme-validate.json",
"theme_validate_sha256": "0d72cbf9f836201ee3e7f8589a2b2fbdd00907e5e89a69640d5060bc98d7c65b"
},
"issues": [
{
"code": "contrast_too_low",
"message": "text contrast 1.01 is below 4.5",
"page": 1,
"path": "04-svg/prepared/page-001.svg"
}
],
"output_path": "06-check/theme-adherence.json",
"pages": [
{
"issues": [
{
"code": "contrast_too_low",
"message": "text contrast 1.01 is below 4.5",
"page": 1,
"path": "04-svg/prepared/page-001.svg"
}
],
"page": 1,
"path": "04-svg/prepared/page-001.svg",
"sha256": "b18e154042df52110f187d53683dbb3faff57944db65a5c218e313b57615d7f6",
"status": "failed",
"theme_id": "dark-clarity"
}
],
"prepared_files": [
{
"path": "04-svg/prepared/page-001.svg",
"sha256": "b18e154042df52110f187d53683dbb3faff57944db65a5c218e313b57615d7f6"
}
],
"schema_version": "svglide-theme-adherence/v1",
"stage": "theme_adherence",
"status": "failed",
"summary": {
"contrast_unresolved_count": 0,
"error_count": 1,
"prepared_svg_count": 1,
"unknown_color_count": 0,
"warning_count": 0
},
"unknown_colors": []
}

View File

@@ -0,0 +1,39 @@
{
"action": "create_live",
"checked_at": "2026-06-21T21:34:00+08:00",
"inputs": {
"plan_sha256": "9d54b389938c4a1d52a2ff9ef4cffacf7fc90d4282aee5154ba85d1b78198fc3",
"slide_plan": "02-plan/slide_plan.json",
"template_registry": "skills/lark-slides/references/svglide-template-registry.json",
"template_registry_sha256": "78c6639ee435ff8b8818f64f7b536967a7486281eacab5abce8e20626e20f6b3",
"theme_registry": "skills/lark-slides/scripts/artboard_renderer/themes/registry.json",
"theme_registry_sha256": "62ea0e69b9f5380a11622a6a47f6947943601f4cc3f1bdba1c7f4e45d77db959"
},
"issues": [],
"output_path": "06-check/theme-validate.json",
"pages": [
{
"issues": [],
"page": 1,
"status": "passed",
"template_id": "cover-hero",
"theme_id": "dark-clarity",
"theme_sha256": "707e671d244fb10d975a10da1c78692b215e8544bc44eb1bb4c0a55dfaec01c1"
}
],
"schema_version": "svglide-theme-validate/v1",
"stage": "theme_validate",
"status": "passed",
"summary": {
"error_count": 0,
"page_count": 1,
"theme_count": 1,
"warning_count": 0
},
"theme_files": [
{
"path": "skills/lark-slides/scripts/artboard_renderer/themes/dark-clarity.json",
"sha256": "f47cfdcc5c29d4b694820c097a8cccedc964d072211b579c0a91e3702189ab1b"
}
]
}

View File

@@ -0,0 +1,63 @@
{
"action": "repair_and_rerun",
"checked_at": "2026-06-21T21:34:08+08:00",
"contrast_failures": [
{
"min": 4.5,
"page": 1,
"path": "04-svg/prepared/page-001.svg",
"ratio": 1.0064,
"text_index": 0
}
],
"contrast_unresolved": [],
"inputs": {
"plan_sha256": "9d54b389938c4a1d52a2ff9ef4cffacf7fc90d4282aee5154ba85d1b78198fc3",
"slide_plan": "02-plan/slide_plan.json",
"theme_validate": "06-check/theme-validate.json",
"theme_validate_sha256": "0d72cbf9f836201ee3e7f8589a2b2fbdd00907e5e89a69640d5060bc98d7c65b"
},
"issues": [
{
"code": "contrast_too_low",
"message": "text contrast 1.01 is below 4.5",
"page": 1,
"path": "04-svg/prepared/page-001.svg"
}
],
"output_path": "06-check/theme-adherence.json",
"pages": [
{
"issues": [
{
"code": "contrast_too_low",
"message": "text contrast 1.01 is below 4.5",
"page": 1,
"path": "04-svg/prepared/page-001.svg"
}
],
"page": 1,
"path": "04-svg/prepared/page-001.svg",
"sha256": "b18e154042df52110f187d53683dbb3faff57944db65a5c218e313b57615d7f6",
"status": "failed",
"theme_id": "dark-clarity"
}
],
"prepared_files": [
{
"path": "04-svg/prepared/page-001.svg",
"sha256": "b18e154042df52110f187d53683dbb3faff57944db65a5c218e313b57615d7f6"
}
],
"schema_version": "svglide-theme-adherence/v1",
"stage": "theme_adherence",
"status": "failed",
"summary": {
"contrast_unresolved_count": 0,
"error_count": 1,
"prepared_svg_count": 1,
"unknown_color_count": 0,
"warning_count": 0
},
"unknown_colors": []
}

View File

@@ -0,0 +1,39 @@
{
"action": "create_live",
"checked_at": "2026-06-21T21:34:00+08:00",
"inputs": {
"plan_sha256": "9d54b389938c4a1d52a2ff9ef4cffacf7fc90d4282aee5154ba85d1b78198fc3",
"slide_plan": "02-plan/slide_plan.json",
"template_registry": "skills/lark-slides/references/svglide-template-registry.json",
"template_registry_sha256": "78c6639ee435ff8b8818f64f7b536967a7486281eacab5abce8e20626e20f6b3",
"theme_registry": "skills/lark-slides/scripts/artboard_renderer/themes/registry.json",
"theme_registry_sha256": "62ea0e69b9f5380a11622a6a47f6947943601f4cc3f1bdba1c7f4e45d77db959"
},
"issues": [],
"output_path": "06-check/theme-validate.json",
"pages": [
{
"issues": [],
"page": 1,
"status": "passed",
"template_id": "cover-hero",
"theme_id": "dark-clarity",
"theme_sha256": "707e671d244fb10d975a10da1c78692b215e8544bc44eb1bb4c0a55dfaec01c1"
}
],
"schema_version": "svglide-theme-validate/v1",
"stage": "theme_validate",
"status": "passed",
"summary": {
"error_count": 0,
"page_count": 1,
"theme_count": 1,
"warning_count": 0
},
"theme_files": [
{
"path": "skills/lark-slides/scripts/artboard_renderer/themes/dark-clarity.json",
"sha256": "f47cfdcc5c29d4b694820c097a8cccedc964d072211b579c0a91e3702189ab1b"
}
]
}

View File

@@ -0,0 +1,16 @@
{
"generation_mode": "artboard_satori",
"language": "zh-CN",
"theme_id": "dark-clarity",
"slides": [
{
"page": 1,
"title": "stale receipt 负例",
"canvas_spec": {
"version": "svglide-canvas-spec/v1",
"template_id": "cover-hero",
"theme_id": "dark-clarity"
}
}
]
}

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="960" height="540" viewBox="0 0 960 540"><rect width="960" height="540" fill="#0F172A"/><text x="88" y="140" fill="#F8FAFC">stale receipt 负例</text></svg>

After

Width:  |  Height:  |  Size: 204 B

View File

@@ -0,0 +1,47 @@
{
"action": "repair_and_rerun",
"checked_at": "2026-06-21T21:34:11+08:00",
"contrast_failures": [],
"contrast_unresolved": [],
"inputs": {
"plan_sha256": "776295d1edea25ac19696a5b5114ada5cce7a99b2a02ecc0a8a3ac65d697f121",
"slide_plan": "02-plan/slide_plan.json",
"theme_validate": "06-check/theme-validate.json",
"theme_validate_sha256": "bc51b146bf85d911dda444f8b43b955347fdeee6c69b9bb467d5fceff4b1ebe3"
},
"issues": [
{
"code": "theme_validate_plan_stale",
"message": "theme_validate plan_sha256 does not match current slide_plan.json",
"path": "06-check/theme-validate.json"
}
],
"output_path": "06-check/theme-adherence.json",
"pages": [
{
"issues": [],
"page": 1,
"path": "04-svg/prepared/page-001.svg",
"sha256": "6287b6edeed2bf3219eaa1035a5a922406f47d7d905b2e34ab6f1899901aab92",
"status": "passed",
"theme_id": "dark-clarity"
}
],
"prepared_files": [
{
"path": "04-svg/prepared/page-001.svg",
"sha256": "6287b6edeed2bf3219eaa1035a5a922406f47d7d905b2e34ab6f1899901aab92"
}
],
"schema_version": "svglide-theme-adherence/v1",
"stage": "theme_adherence",
"status": "failed",
"summary": {
"contrast_unresolved_count": 0,
"error_count": 1,
"prepared_svg_count": 1,
"unknown_color_count": 0,
"warning_count": 0
},
"unknown_colors": []
}

View File

@@ -0,0 +1,34 @@
{
"action": "create_live",
"checked_at": "2026-06-21T00:00:00+08:00",
"inputs": {
"plan_sha256": "stale",
"slide_plan": "02-plan/slide_plan.json",
"template_registry": "skills/lark-slides/references/svglide-template-registry.json",
"template_registry_sha256": "stale",
"theme_registry": "skills/lark-slides/scripts/artboard_renderer/themes/registry.json",
"theme_registry_sha256": "stale"
},
"issues": [],
"output_path": "06-check/theme-validate.json",
"pages": [
{
"issues": [],
"page": 1,
"status": "passed",
"template_id": "cover-hero",
"theme_id": "dark-clarity",
"theme_sha256": "stale"
}
],
"schema_version": "svglide-theme-validate/v1",
"stage": "theme_validate",
"status": "passed",
"summary": {
"error_count": 0,
"page_count": 1,
"theme_count": 1,
"warning_count": 0
},
"theme_files": []
}

View File

@@ -0,0 +1,47 @@
{
"action": "repair_and_rerun",
"checked_at": "2026-06-21T21:34:11+08:00",
"contrast_failures": [],
"contrast_unresolved": [],
"inputs": {
"plan_sha256": "776295d1edea25ac19696a5b5114ada5cce7a99b2a02ecc0a8a3ac65d697f121",
"slide_plan": "02-plan/slide_plan.json",
"theme_validate": "06-check/theme-validate.json",
"theme_validate_sha256": "bc51b146bf85d911dda444f8b43b955347fdeee6c69b9bb467d5fceff4b1ebe3"
},
"issues": [
{
"code": "theme_validate_plan_stale",
"message": "theme_validate plan_sha256 does not match current slide_plan.json",
"path": "06-check/theme-validate.json"
}
],
"output_path": "06-check/theme-adherence.json",
"pages": [
{
"issues": [],
"page": 1,
"path": "04-svg/prepared/page-001.svg",
"sha256": "6287b6edeed2bf3219eaa1035a5a922406f47d7d905b2e34ab6f1899901aab92",
"status": "passed",
"theme_id": "dark-clarity"
}
],
"prepared_files": [
{
"path": "04-svg/prepared/page-001.svg",
"sha256": "6287b6edeed2bf3219eaa1035a5a922406f47d7d905b2e34ab6f1899901aab92"
}
],
"schema_version": "svglide-theme-adherence/v1",
"stage": "theme_adherence",
"status": "failed",
"summary": {
"contrast_unresolved_count": 0,
"error_count": 1,
"prepared_svg_count": 1,
"unknown_color_count": 0,
"warning_count": 0
},
"unknown_colors": []
}

View File

@@ -0,0 +1,30 @@
{
"action": "create_live",
"checked_at": "2026-06-21T00:00:00+08:00",
"inputs": {
"plan_sha256": "stale",
"slide_plan": "02-plan/slide_plan.json"
},
"issues": [],
"output_path": "06-check/theme-validate.json",
"pages": [
{
"issues": [],
"page": 1,
"status": "passed",
"template_id": "cover-hero",
"theme_id": "dark-clarity",
"theme_sha256": "stale"
}
],
"schema_version": "svglide-theme-validate/v1",
"stage": "theme_validate",
"status": "passed",
"summary": {
"error_count": 0,
"page_count": 1,
"theme_count": 1,
"warning_count": 0
},
"theme_files": []
}

View File

@@ -0,0 +1,16 @@
{
"generation_mode": "artboard_satori",
"language": "zh-CN",
"theme_id": "dark-clarity",
"slides": [
{
"page": 1,
"title": "未知颜色负例",
"canvas_spec": {
"version": "svglide-canvas-spec/v1",
"template_id": "cover-hero",
"theme_id": "dark-clarity"
}
}
]
}

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="960" height="540" viewBox="0 0 960 540"><rect width="960" height="540" fill="#0F172A"/><text x="88" y="140" fill="#F8FAFC">未知颜色负例</text><circle cx="760" cy="160" r="72" fill="#123456"/></svg>

After

Width:  |  Height:  |  Size: 251 B

View File

@@ -0,0 +1,61 @@
{
"action": "repair_and_rerun",
"checked_at": "2026-06-21T21:34:08+08:00",
"contrast_failures": [],
"contrast_unresolved": [],
"inputs": {
"plan_sha256": "37ec82d0f615e7daf9ab498684597ebc93748372a1f8c56daec548ba01a3e425",
"slide_plan": "02-plan/slide_plan.json",
"theme_validate": "06-check/theme-validate.json",
"theme_validate_sha256": "bec8dd0adf58083b70607bfa7d1ddaabd67b53826bf1c15e43c92348bb999d12"
},
"issues": [
{
"code": "theme_unknown_color",
"message": "color #123456 is not allowed by theme dark-clarity",
"page": 1,
"path": "04-svg/prepared/page-001.svg"
}
],
"output_path": "06-check/theme-adherence.json",
"pages": [
{
"issues": [
{
"code": "theme_unknown_color",
"message": "color #123456 is not allowed by theme dark-clarity",
"page": 1,
"path": "04-svg/prepared/page-001.svg"
}
],
"page": 1,
"path": "04-svg/prepared/page-001.svg",
"sha256": "9a86436dad4a18fcb65e7a41a37ed7c6aa6f59bd79ef15f041feaa16675e62b7",
"status": "failed",
"theme_id": "dark-clarity"
}
],
"prepared_files": [
{
"path": "04-svg/prepared/page-001.svg",
"sha256": "9a86436dad4a18fcb65e7a41a37ed7c6aa6f59bd79ef15f041feaa16675e62b7"
}
],
"schema_version": "svglide-theme-adherence/v1",
"stage": "theme_adherence",
"status": "failed",
"summary": {
"contrast_unresolved_count": 0,
"error_count": 1,
"prepared_svg_count": 1,
"unknown_color_count": 1,
"warning_count": 0
},
"unknown_colors": [
{
"color": "#123456",
"page": 1,
"path": "04-svg/prepared/page-001.svg"
}
]
}

View File

@@ -0,0 +1,39 @@
{
"action": "create_live",
"checked_at": "2026-06-21T21:34:00+08:00",
"inputs": {
"plan_sha256": "37ec82d0f615e7daf9ab498684597ebc93748372a1f8c56daec548ba01a3e425",
"slide_plan": "02-plan/slide_plan.json",
"template_registry": "skills/lark-slides/references/svglide-template-registry.json",
"template_registry_sha256": "78c6639ee435ff8b8818f64f7b536967a7486281eacab5abce8e20626e20f6b3",
"theme_registry": "skills/lark-slides/scripts/artboard_renderer/themes/registry.json",
"theme_registry_sha256": "62ea0e69b9f5380a11622a6a47f6947943601f4cc3f1bdba1c7f4e45d77db959"
},
"issues": [],
"output_path": "06-check/theme-validate.json",
"pages": [
{
"issues": [],
"page": 1,
"status": "passed",
"template_id": "cover-hero",
"theme_id": "dark-clarity",
"theme_sha256": "707e671d244fb10d975a10da1c78692b215e8544bc44eb1bb4c0a55dfaec01c1"
}
],
"schema_version": "svglide-theme-validate/v1",
"stage": "theme_validate",
"status": "passed",
"summary": {
"error_count": 0,
"page_count": 1,
"theme_count": 1,
"warning_count": 0
},
"theme_files": [
{
"path": "skills/lark-slides/scripts/artboard_renderer/themes/dark-clarity.json",
"sha256": "f47cfdcc5c29d4b694820c097a8cccedc964d072211b579c0a91e3702189ab1b"
}
]
}

View File

@@ -0,0 +1,61 @@
{
"action": "repair_and_rerun",
"checked_at": "2026-06-21T21:34:08+08:00",
"contrast_failures": [],
"contrast_unresolved": [],
"inputs": {
"plan_sha256": "37ec82d0f615e7daf9ab498684597ebc93748372a1f8c56daec548ba01a3e425",
"slide_plan": "02-plan/slide_plan.json",
"theme_validate": "06-check/theme-validate.json",
"theme_validate_sha256": "bec8dd0adf58083b70607bfa7d1ddaabd67b53826bf1c15e43c92348bb999d12"
},
"issues": [
{
"code": "theme_unknown_color",
"message": "color #123456 is not allowed by theme dark-clarity",
"page": 1,
"path": "04-svg/prepared/page-001.svg"
}
],
"output_path": "06-check/theme-adherence.json",
"pages": [
{
"issues": [
{
"code": "theme_unknown_color",
"message": "color #123456 is not allowed by theme dark-clarity",
"page": 1,
"path": "04-svg/prepared/page-001.svg"
}
],
"page": 1,
"path": "04-svg/prepared/page-001.svg",
"sha256": "9a86436dad4a18fcb65e7a41a37ed7c6aa6f59bd79ef15f041feaa16675e62b7",
"status": "failed",
"theme_id": "dark-clarity"
}
],
"prepared_files": [
{
"path": "04-svg/prepared/page-001.svg",
"sha256": "9a86436dad4a18fcb65e7a41a37ed7c6aa6f59bd79ef15f041feaa16675e62b7"
}
],
"schema_version": "svglide-theme-adherence/v1",
"stage": "theme_adherence",
"status": "failed",
"summary": {
"contrast_unresolved_count": 0,
"error_count": 1,
"prepared_svg_count": 1,
"unknown_color_count": 1,
"warning_count": 0
},
"unknown_colors": [
{
"color": "#123456",
"page": 1,
"path": "04-svg/prepared/page-001.svg"
}
]
}

View File

@@ -0,0 +1,39 @@
{
"action": "create_live",
"checked_at": "2026-06-21T21:34:00+08:00",
"inputs": {
"plan_sha256": "37ec82d0f615e7daf9ab498684597ebc93748372a1f8c56daec548ba01a3e425",
"slide_plan": "02-plan/slide_plan.json",
"template_registry": "skills/lark-slides/references/svglide-template-registry.json",
"template_registry_sha256": "78c6639ee435ff8b8818f64f7b536967a7486281eacab5abce8e20626e20f6b3",
"theme_registry": "skills/lark-slides/scripts/artboard_renderer/themes/registry.json",
"theme_registry_sha256": "62ea0e69b9f5380a11622a6a47f6947943601f4cc3f1bdba1c7f4e45d77db959"
},
"issues": [],
"output_path": "06-check/theme-validate.json",
"pages": [
{
"issues": [],
"page": 1,
"status": "passed",
"template_id": "cover-hero",
"theme_id": "dark-clarity",
"theme_sha256": "707e671d244fb10d975a10da1c78692b215e8544bc44eb1bb4c0a55dfaec01c1"
}
],
"schema_version": "svglide-theme-validate/v1",
"stage": "theme_validate",
"status": "passed",
"summary": {
"error_count": 0,
"page_count": 1,
"theme_count": 1,
"warning_count": 0
},
"theme_files": [
{
"path": "skills/lark-slides/scripts/artboard_renderer/themes/dark-clarity.json",
"sha256": "f47cfdcc5c29d4b694820c097a8cccedc964d072211b579c0a91e3702189ab1b"
}
]
}

View File

@@ -0,0 +1,19 @@
Theme System P0 positive fixtures.
Use these project roots to prove that ThemeSpec validation and final SVG
theme adherence can pass without relying on plan-only visual distinctness.
- `artboard-satori`: three-page cover/content/closing artboard project.
- `direct-svg`: one-page direct SVG project; it validates theme usage without
requiring the artboard package check.
Recommended smoke commands:
```bash
python3 skills/lark-slides/scripts/svglide_theme_validate.py \
skills/lark-slides/scripts/fixtures/svglide_artboard/theme-system-p0/artboard-satori \
--pretty
python3 skills/lark-slides/scripts/svglide_theme_adherence.py \
skills/lark-slides/scripts/fixtures/svglide_artboard/theme-system-p0/artboard-satori \
--pretty
```

View File

@@ -0,0 +1,55 @@
{
"generation_mode": "artboard_satori",
"language": "zh-CN",
"theme_id": "dark-clarity",
"slides": [
{
"page": 1,
"page_type": "cover",
"title": "主题系统封面",
"canvas_spec": {
"version": "svglide-canvas-spec/v1",
"template_id": "cover-hero",
"theme_id": "dark-clarity",
"theme": {
"colors": {
"background": "#0F172A",
"text": "#F8FAFC"
}
}
}
},
{
"page": 2,
"page_type": "content",
"title": "主题系统正文",
"canvas_spec": {
"version": "svglide-canvas-spec/v1",
"template_id": "comparison-cards",
"theme_id": "dark-clarity",
"theme": {
"colors": {
"background": "#0F172A",
"text": "#F8FAFC"
}
}
}
},
{
"page": 3,
"page_type": "closing",
"title": "主题系统结尾",
"canvas_spec": {
"version": "svglide-canvas-spec/v1",
"template_id": "summary-final",
"theme_id": "dark-clarity",
"theme": {
"colors": {
"background": "#0F172A",
"text": "#F8FAFC"
}
}
}
}
]
}

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="960" height="540" viewBox="0 0 960 540"><rect width="960" height="540" fill="#0F172A"/><rect x="72" y="72" width="640" height="300" fill="#111827"/><text x="96" y="150" fill="#F8FAFC">主题系统封面</text><text x="96" y="210" fill="#CBD5E1">final prepared SVG 使用 ThemeSpec 颜色</text><circle cx="800" cy="130" r="64" fill="#38BDF8"/></svg>

After

Width:  |  Height:  |  Size: 397 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="960" height="540" viewBox="0 0 960 540"><rect width="960" height="540" fill="#0F172A"/><rect x="80" y="96" width="360" height="280" fill="#111827"/><rect x="520" y="96" width="360" height="280" fill="#111827"/><text x="112" y="152" fill="#F8FAFC">主题系统正文</text><text x="112" y="210" fill="#CBD5E1">左侧观点</text><text x="552" y="210" fill="#CBD5E1">右侧观点</text><path d="M112 300 L250 240 L388 300" stroke="#38BDF8" fill="none"/></svg>

After

Width:  |  Height:  |  Size: 505 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="960" height="540" viewBox="0 0 960 540"><rect width="960" height="540" fill="#0F172A"/><rect x="96" y="112" width="768" height="260" fill="#111827"/><text x="132" y="180" fill="#F8FAFC">主题系统结尾</text><text x="132" y="242" fill="#CBD5E1">ThemeSpec、adherence 与 quality gate 闭环</text><rect x="132" y="300" width="168" height="40" fill="#A78BFA"/></svg>

After

Width:  |  Height:  |  Size: 416 B

View File

@@ -0,0 +1,65 @@
{
"action": "create_live",
"checked_at": "2026-06-21T21:34:08+08:00",
"contrast_failures": [],
"contrast_unresolved": [],
"inputs": {
"plan_sha256": "4831ac4c6c82d461a671a67561e50fe6111af4c6baba957a2640512c39517afd",
"slide_plan": "02-plan/slide_plan.json",
"theme_validate": "06-check/theme-validate.json",
"theme_validate_sha256": "3a16056b5947ff22a2145592ef6a774c92c7b292272913527a837ec998ab295d"
},
"issues": [],
"output_path": "06-check/theme-adherence.json",
"pages": [
{
"issues": [],
"page": 1,
"path": "04-svg/prepared/page-001.svg",
"sha256": "f6f43d6b72f7872fcdc80f7a6130d16a2247f9b0ec2bdbd40fb6e0d2e9a336ef",
"status": "passed",
"theme_id": "dark-clarity"
},
{
"issues": [],
"page": 2,
"path": "04-svg/prepared/page-002.svg",
"sha256": "fe7e6b41731a60b3cc48a9431640104f55374cf4f4a69548072546e1b51f7301",
"status": "passed",
"theme_id": "dark-clarity"
},
{
"issues": [],
"page": 3,
"path": "04-svg/prepared/page-003.svg",
"sha256": "a4a09d108e7bfb33ca8104e4261983794e48a8a4b947fcdf5f06c820e6936bf0",
"status": "passed",
"theme_id": "dark-clarity"
}
],
"prepared_files": [
{
"path": "04-svg/prepared/page-001.svg",
"sha256": "f6f43d6b72f7872fcdc80f7a6130d16a2247f9b0ec2bdbd40fb6e0d2e9a336ef"
},
{
"path": "04-svg/prepared/page-002.svg",
"sha256": "fe7e6b41731a60b3cc48a9431640104f55374cf4f4a69548072546e1b51f7301"
},
{
"path": "04-svg/prepared/page-003.svg",
"sha256": "a4a09d108e7bfb33ca8104e4261983794e48a8a4b947fcdf5f06c820e6936bf0"
}
],
"schema_version": "svglide-theme-adherence/v1",
"stage": "theme_adherence",
"status": "passed",
"summary": {
"contrast_unresolved_count": 0,
"error_count": 0,
"prepared_svg_count": 3,
"unknown_color_count": 0,
"warning_count": 0
},
"unknown_colors": []
}

View File

@@ -0,0 +1,55 @@
{
"action": "create_live",
"checked_at": "2026-06-21T21:34:00+08:00",
"inputs": {
"plan_sha256": "4831ac4c6c82d461a671a67561e50fe6111af4c6baba957a2640512c39517afd",
"slide_plan": "02-plan/slide_plan.json",
"template_registry": "skills/lark-slides/references/svglide-template-registry.json",
"template_registry_sha256": "78c6639ee435ff8b8818f64f7b536967a7486281eacab5abce8e20626e20f6b3",
"theme_registry": "skills/lark-slides/scripts/artboard_renderer/themes/registry.json",
"theme_registry_sha256": "62ea0e69b9f5380a11622a6a47f6947943601f4cc3f1bdba1c7f4e45d77db959"
},
"issues": [],
"output_path": "06-check/theme-validate.json",
"pages": [
{
"issues": [],
"page": 1,
"status": "passed",
"template_id": "cover-hero",
"theme_id": "dark-clarity",
"theme_sha256": "707e671d244fb10d975a10da1c78692b215e8544bc44eb1bb4c0a55dfaec01c1"
},
{
"issues": [],
"page": 2,
"status": "passed",
"template_id": "comparison-cards",
"theme_id": "dark-clarity",
"theme_sha256": "707e671d244fb10d975a10da1c78692b215e8544bc44eb1bb4c0a55dfaec01c1"
},
{
"issues": [],
"page": 3,
"status": "passed",
"template_id": "summary-final",
"theme_id": "dark-clarity",
"theme_sha256": "707e671d244fb10d975a10da1c78692b215e8544bc44eb1bb4c0a55dfaec01c1"
}
],
"schema_version": "svglide-theme-validate/v1",
"stage": "theme_validate",
"status": "passed",
"summary": {
"error_count": 0,
"page_count": 3,
"theme_count": 1,
"warning_count": 0
},
"theme_files": [
{
"path": "skills/lark-slides/scripts/artboard_renderer/themes/dark-clarity.json",
"sha256": "f47cfdcc5c29d4b694820c097a8cccedc964d072211b579c0a91e3702189ab1b"
}
]
}

View File

@@ -0,0 +1,65 @@
{
"action": "create_live",
"checked_at": "2026-06-21T21:34:08+08:00",
"contrast_failures": [],
"contrast_unresolved": [],
"inputs": {
"plan_sha256": "4831ac4c6c82d461a671a67561e50fe6111af4c6baba957a2640512c39517afd",
"slide_plan": "02-plan/slide_plan.json",
"theme_validate": "06-check/theme-validate.json",
"theme_validate_sha256": "3a16056b5947ff22a2145592ef6a774c92c7b292272913527a837ec998ab295d"
},
"issues": [],
"output_path": "06-check/theme-adherence.json",
"pages": [
{
"issues": [],
"page": 1,
"path": "04-svg/prepared/page-001.svg",
"sha256": "f6f43d6b72f7872fcdc80f7a6130d16a2247f9b0ec2bdbd40fb6e0d2e9a336ef",
"status": "passed",
"theme_id": "dark-clarity"
},
{
"issues": [],
"page": 2,
"path": "04-svg/prepared/page-002.svg",
"sha256": "fe7e6b41731a60b3cc48a9431640104f55374cf4f4a69548072546e1b51f7301",
"status": "passed",
"theme_id": "dark-clarity"
},
{
"issues": [],
"page": 3,
"path": "04-svg/prepared/page-003.svg",
"sha256": "a4a09d108e7bfb33ca8104e4261983794e48a8a4b947fcdf5f06c820e6936bf0",
"status": "passed",
"theme_id": "dark-clarity"
}
],
"prepared_files": [
{
"path": "04-svg/prepared/page-001.svg",
"sha256": "f6f43d6b72f7872fcdc80f7a6130d16a2247f9b0ec2bdbd40fb6e0d2e9a336ef"
},
{
"path": "04-svg/prepared/page-002.svg",
"sha256": "fe7e6b41731a60b3cc48a9431640104f55374cf4f4a69548072546e1b51f7301"
},
{
"path": "04-svg/prepared/page-003.svg",
"sha256": "a4a09d108e7bfb33ca8104e4261983794e48a8a4b947fcdf5f06c820e6936bf0"
}
],
"schema_version": "svglide-theme-adherence/v1",
"stage": "theme_adherence",
"status": "passed",
"summary": {
"contrast_unresolved_count": 0,
"error_count": 0,
"prepared_svg_count": 3,
"unknown_color_count": 0,
"warning_count": 0
},
"unknown_colors": []
}

View File

@@ -0,0 +1,55 @@
{
"action": "create_live",
"checked_at": "2026-06-21T21:34:00+08:00",
"inputs": {
"plan_sha256": "4831ac4c6c82d461a671a67561e50fe6111af4c6baba957a2640512c39517afd",
"slide_plan": "02-plan/slide_plan.json",
"template_registry": "skills/lark-slides/references/svglide-template-registry.json",
"template_registry_sha256": "78c6639ee435ff8b8818f64f7b536967a7486281eacab5abce8e20626e20f6b3",
"theme_registry": "skills/lark-slides/scripts/artboard_renderer/themes/registry.json",
"theme_registry_sha256": "62ea0e69b9f5380a11622a6a47f6947943601f4cc3f1bdba1c7f4e45d77db959"
},
"issues": [],
"output_path": "06-check/theme-validate.json",
"pages": [
{
"issues": [],
"page": 1,
"status": "passed",
"template_id": "cover-hero",
"theme_id": "dark-clarity",
"theme_sha256": "707e671d244fb10d975a10da1c78692b215e8544bc44eb1bb4c0a55dfaec01c1"
},
{
"issues": [],
"page": 2,
"status": "passed",
"template_id": "comparison-cards",
"theme_id": "dark-clarity",
"theme_sha256": "707e671d244fb10d975a10da1c78692b215e8544bc44eb1bb4c0a55dfaec01c1"
},
{
"issues": [],
"page": 3,
"status": "passed",
"template_id": "summary-final",
"theme_id": "dark-clarity",
"theme_sha256": "707e671d244fb10d975a10da1c78692b215e8544bc44eb1bb4c0a55dfaec01c1"
}
],
"schema_version": "svglide-theme-validate/v1",
"stage": "theme_validate",
"status": "passed",
"summary": {
"error_count": 0,
"page_count": 3,
"theme_count": 1,
"warning_count": 0
},
"theme_files": [
{
"path": "skills/lark-slides/scripts/artboard_renderer/themes/dark-clarity.json",
"sha256": "f47cfdcc5c29d4b694820c097a8cccedc964d072211b579c0a91e3702189ab1b"
}
]
}

View File

@@ -0,0 +1,12 @@
{
"generation_mode": "direct_svg",
"language": "zh-CN",
"theme_id": "dark-clarity",
"slides": [
{
"page": 1,
"page_type": "content",
"title": "Direct SVG 主题检查"
}
]
}

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="960" height="540" viewBox="0 0 960 540"><rect width="960" height="540" fill="#0F172A"/><text x="88" y="140" fill="#F8FAFC">Direct SVG 主题检查</text><text x="88" y="196" fill="#CBD5E1">direct_svg 不依赖 artboard package check</text><rect x="88" y="252" width="220" height="48" fill="#38BDF8"/></svg>

After

Width:  |  Height:  |  Size: 354 B

View File

@@ -0,0 +1,41 @@
{
"action": "create_live",
"checked_at": "2026-06-21T21:34:08+08:00",
"contrast_failures": [],
"contrast_unresolved": [],
"inputs": {
"plan_sha256": "b17941fe2a32a4fb4b5dce9404baac08d14183ffe2ae2a9b2ff07d6b3bb0c10a",
"slide_plan": "02-plan/slide_plan.json",
"theme_validate": "06-check/theme-validate.json",
"theme_validate_sha256": "dc0b34dfe8f2e2d82f2c3b9053e559b67225736e6a7806fef603b58086242eb9"
},
"issues": [],
"output_path": "06-check/theme-adherence.json",
"pages": [
{
"issues": [],
"page": 1,
"path": "04-svg/prepared/page-001.svg",
"sha256": "e1447907a30468942c2605b6de2a0571dd6503102e3f49fe9a53958bbabd11c8",
"status": "passed",
"theme_id": "dark-clarity"
}
],
"prepared_files": [
{
"path": "04-svg/prepared/page-001.svg",
"sha256": "e1447907a30468942c2605b6de2a0571dd6503102e3f49fe9a53958bbabd11c8"
}
],
"schema_version": "svglide-theme-adherence/v1",
"stage": "theme_adherence",
"status": "passed",
"summary": {
"contrast_unresolved_count": 0,
"error_count": 0,
"prepared_svg_count": 1,
"unknown_color_count": 0,
"warning_count": 0
},
"unknown_colors": []
}

View File

@@ -0,0 +1,39 @@
{
"action": "create_live",
"checked_at": "2026-06-21T21:34:00+08:00",
"inputs": {
"plan_sha256": "b17941fe2a32a4fb4b5dce9404baac08d14183ffe2ae2a9b2ff07d6b3bb0c10a",
"slide_plan": "02-plan/slide_plan.json",
"template_registry": "skills/lark-slides/references/svglide-template-registry.json",
"template_registry_sha256": "78c6639ee435ff8b8818f64f7b536967a7486281eacab5abce8e20626e20f6b3",
"theme_registry": "skills/lark-slides/scripts/artboard_renderer/themes/registry.json",
"theme_registry_sha256": "62ea0e69b9f5380a11622a6a47f6947943601f4cc3f1bdba1c7f4e45d77db959"
},
"issues": [],
"output_path": "06-check/theme-validate.json",
"pages": [
{
"issues": [],
"page": 1,
"status": "passed",
"template_id": null,
"theme_id": "dark-clarity",
"theme_sha256": "707e671d244fb10d975a10da1c78692b215e8544bc44eb1bb4c0a55dfaec01c1"
}
],
"schema_version": "svglide-theme-validate/v1",
"stage": "theme_validate",
"status": "passed",
"summary": {
"error_count": 0,
"page_count": 1,
"theme_count": 1,
"warning_count": 0
},
"theme_files": [
{
"path": "skills/lark-slides/scripts/artboard_renderer/themes/dark-clarity.json",
"sha256": "f47cfdcc5c29d4b694820c097a8cccedc964d072211b579c0a91e3702189ab1b"
}
]
}

View File

@@ -0,0 +1,41 @@
{
"action": "create_live",
"checked_at": "2026-06-21T21:34:08+08:00",
"contrast_failures": [],
"contrast_unresolved": [],
"inputs": {
"plan_sha256": "b17941fe2a32a4fb4b5dce9404baac08d14183ffe2ae2a9b2ff07d6b3bb0c10a",
"slide_plan": "02-plan/slide_plan.json",
"theme_validate": "06-check/theme-validate.json",
"theme_validate_sha256": "dc0b34dfe8f2e2d82f2c3b9053e559b67225736e6a7806fef603b58086242eb9"
},
"issues": [],
"output_path": "06-check/theme-adherence.json",
"pages": [
{
"issues": [],
"page": 1,
"path": "04-svg/prepared/page-001.svg",
"sha256": "e1447907a30468942c2605b6de2a0571dd6503102e3f49fe9a53958bbabd11c8",
"status": "passed",
"theme_id": "dark-clarity"
}
],
"prepared_files": [
{
"path": "04-svg/prepared/page-001.svg",
"sha256": "e1447907a30468942c2605b6de2a0571dd6503102e3f49fe9a53958bbabd11c8"
}
],
"schema_version": "svglide-theme-adherence/v1",
"stage": "theme_adherence",
"status": "passed",
"summary": {
"contrast_unresolved_count": 0,
"error_count": 0,
"prepared_svg_count": 1,
"unknown_color_count": 0,
"warning_count": 0
},
"unknown_colors": []
}

View File

@@ -0,0 +1,39 @@
{
"action": "create_live",
"checked_at": "2026-06-21T21:34:00+08:00",
"inputs": {
"plan_sha256": "b17941fe2a32a4fb4b5dce9404baac08d14183ffe2ae2a9b2ff07d6b3bb0c10a",
"slide_plan": "02-plan/slide_plan.json",
"template_registry": "skills/lark-slides/references/svglide-template-registry.json",
"template_registry_sha256": "78c6639ee435ff8b8818f64f7b536967a7486281eacab5abce8e20626e20f6b3",
"theme_registry": "skills/lark-slides/scripts/artboard_renderer/themes/registry.json",
"theme_registry_sha256": "62ea0e69b9f5380a11622a6a47f6947943601f4cc3f1bdba1c7f4e45d77db959"
},
"issues": [],
"output_path": "06-check/theme-validate.json",
"pages": [
{
"issues": [],
"page": 1,
"status": "passed",
"template_id": null,
"theme_id": "dark-clarity",
"theme_sha256": "707e671d244fb10d975a10da1c78692b215e8544bc44eb1bb4c0a55dfaec01c1"
}
],
"schema_version": "svglide-theme-validate/v1",
"stage": "theme_validate",
"status": "passed",
"summary": {
"error_count": 0,
"page_count": 1,
"theme_count": 1,
"warning_count": 0
},
"theme_files": [
{
"path": "skills/lark-slides/scripts/artboard_renderer/themes/dark-clarity.json",
"sha256": "f47cfdcc5c29d4b694820c097a8cccedc964d072211b579c0a91e3702189ab1b"
}
]
}

View File

@@ -151,6 +151,33 @@ def sorted_hash_records(records: list[dict[str, str]]) -> list[dict[str, str]]:
return sorted(records, key=lambda item: item["path"])
def normalize_reviewed_artifacts(value: Any) -> dict[str, Any] | None:
if isinstance(value, dict):
return value
if not isinstance(value, list):
return None
normalized: dict[str, Any] = {}
prepared: list[dict[str, str]] = []
for item in value:
if not isinstance(item, dict):
return None
kind = item.get("kind")
path = item.get("path")
sha = item.get("sha256")
if not isinstance(kind, str) or not isinstance(path, str) or not isinstance(sha, str):
return None
record = {"path": path, "sha256": sha}
if kind == "prepared_svg":
prepared.append(record)
elif kind in ARTIFACT_PATHS:
normalized[kind] = record
else:
normalized[kind] = record
if prepared:
normalized["prepared_svg"] = prepared
return normalized
def validate_hash_value(
issues: list[dict[str, str]],
*,
@@ -386,7 +413,7 @@ def validate_reviewed_artifacts(
project: Path,
current_prepared: list[dict[str, str]],
) -> list[dict[str, Any]]:
reviewed = human.get("reviewed_artifacts") if isinstance(human.get("reviewed_artifacts"), dict) else {}
reviewed = normalize_reviewed_artifacts(human.get("reviewed_artifacts"))
evidence: list[dict[str, Any]] = []
if not reviewed:
issues.append(issue("human_review", "reviewed_artifacts_missing", "human review must include reviewed_artifacts", category="missing_input"))
@@ -446,7 +473,7 @@ def validate_human_bindings(
elif recorded_prepared != sorted_hash_records(current_prepared):
issues.append(issue("human_review", "human_prepared_files_stale", "human review prepared_files do not match current prepared SVG files", category="stale_hash"))
reviewed_artifacts = human.get("reviewed_artifacts") if isinstance(human.get("reviewed_artifacts"), dict) else {}
reviewed_artifacts = normalize_reviewed_artifacts(human.get("reviewed_artifacts")) or {}
quality_gate_review = reviewed_artifacts.get("quality_gate") if isinstance(reviewed_artifacts, dict) else None
if isinstance(quality_gate_review, dict) and quality_gate_review.get("sha256") != human.get("quality_gate_sha256"):
issues.append(issue("human_review", "human_quality_gate_hash_disagrees", "quality_gate_sha256 must match reviewed_artifacts.quality_gate.sha256", category="stale_hash"))

View File

@@ -162,6 +162,25 @@ def human_review_payload(project: Path) -> dict[str, object]:
}
def human_review_payload_with_artifact_array(project: Path) -> dict[str, object]:
payload = human_review_payload(project)
reviewed = payload["reviewed_artifacts"]
assert isinstance(reviewed, dict)
current_prepared = reviewed["prepared_svg"]
assert isinstance(current_prepared, list)
artifacts: list[dict[str, str]] = [
{"kind": "prepared_svg", "path": item["path"], "sha256": item["sha256"]}
for item in current_prepared
if isinstance(item, dict) and isinstance(item.get("path"), str) and isinstance(item.get("sha256"), str)
]
for kind in ("contact_sheet", "preview", "preview_manifest", "quality_gate"):
item = reviewed[kind]
assert isinstance(item, dict)
artifacts.append({"kind": kind, "path": item["path"], "sha256": item["sha256"]})
payload["reviewed_artifacts"] = artifacts
return payload
def write_human_review(project: Path, payload: dict[str, object] | None = None) -> Path:
path = project / "06-check/pre-submit-human-review.json"
write_json(path, payload or human_review_payload(project))
@@ -187,6 +206,16 @@ class PreSubmitReviewTest(unittest.TestCase):
self.assertEqual(check["status"], "passed")
self.assertEqual(check, receipt)
def test_document_array_reviewed_artifacts_contract_passes(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
project = make_project(Path(tmp))
write_human_review(project, human_review_payload_with_artifact_array(project))
result = pre_submit_review.run_pre_submit_review(project, project / "06-check/pre-submit-human-review.json")
self.assertEqual(result["status"], "passed", result["issues"])
self.assertEqual(result["issues"], [])
def test_missing_human_file_writes_failed_receipt(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
project = make_project(Path(tmp))

View File

@@ -1808,12 +1808,12 @@ def run_implemented_stage(project_root: Path, stage: str, state: dict[str, Any],
(SCRIPT_DIR / "svglide_pre_submit_review.py").as_posix(),
project_root.as_posix(),
"--human-review",
(project_root / "06-check" / "pre-submit-review.input.json").as_posix(),
(project_root / "06-check" / "pre-submit-human-review.json").as_posix(),
"--pretty",
],
output_json=project_root / "06-check" / "pre-submit-review.json",
inputs=[
"06-check/pre-submit-review.input.json",
"06-check/pre-submit-human-review.json",
"06-check/quality-gate.json",
"06-check/theme-adherence.json",
"06-check/visual-distinctness.json",

View File

@@ -528,6 +528,53 @@ class SVGlideProjectRunnerTest(unittest.TestCase):
self.assertLess(called_stages.index("pre_submit_review"), called_stages.index("live_create"))
self.assertTrue(all(profile == "production_live" for _, profile in calls))
def test_pre_submit_review_uses_documented_human_review_path(self) -> None:
with tempfile.TemporaryDirectory() as tmpdir:
plan_root = Path(tmpdir) / ".lark-slides/plan"
result = runner.init_project("smoke", "Smoke", plan_root=plan_root)
project_root = Path(result["project_root"])
state = runner.load_state(project_root)
state.setdefault("stages", {})["ppe_proof"] = {"status": "passed"}
captured: dict[str, object] = {}
original_run_script_stage = runner.run_script_stage
def fake_run_script_stage(
project_root: Path,
state: dict[str, object],
stage: str,
command: list[str],
*,
output_json: Path | None = None,
inputs: list[str] | None = None,
outputs: list[str] | None = None,
command_runner=runner.subprocess.run,
) -> dict[str, object]:
captured["command"] = command
captured["inputs"] = inputs
return runner.complete_stage(
project_root,
state,
stage,
"passed",
started_at=runner.now_iso(),
inputs=inputs,
outputs=outputs,
command=command,
)
try:
runner.run_script_stage = fake_run_script_stage
runner.run_implemented_stage(project_root, "pre_submit_review", state, profile="production_live")
finally:
runner.run_script_stage = original_run_script_stage
command = captured["command"]
inputs = captured["inputs"]
self.assertIsInstance(command, list)
self.assertIsInstance(inputs, list)
self.assertIn((project_root / "06-check/pre-submit-human-review.json").as_posix(), command)
self.assertIn("06-check/pre-submit-human-review.json", inputs)
def test_init_creates_project_directories_manifest_and_state(self) -> None:
with tempfile.TemporaryDirectory() as tmpdir:
plan_root = Path(tmpdir) / ".lark-slides/plan"

View File

@@ -0,0 +1,93 @@
#!/usr/bin/env python3
# Copyright (c) 2026 Lark Technologies Pte. Ltd.
# SPDX-License-Identifier: MIT
from __future__ import annotations
import shutil
import sys
import tempfile
import unittest
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
import svglide_theme_adherence
import svglide_theme_validate
SCRIPT_DIR = Path(__file__).resolve().parent
FIXTURE_ROOT = SCRIPT_DIR / "fixtures" / "svglide_artboard"
EVIDENCE_PATH = SCRIPT_DIR.parent / "references" / "svglide-theme-system-p0-evidence.md"
def copy_fixture(relative: str, tmpdir: str) -> Path:
source = FIXTURE_ROOT / relative
target = Path(tmpdir) / relative.replace("/", "-")
shutil.copytree(source, target)
return target
def issue_codes(payload: dict[str, object]) -> set[str]:
issues = payload.get("issues")
if not isinstance(issues, list):
return set()
return {item.get("code") for item in issues if isinstance(item, dict) and isinstance(item.get("code"), str)}
class SVGlideThemeSystemP0FixtureTest(unittest.TestCase):
def test_evidence_file_exists_and_keeps_p0_scope(self) -> None:
self.assertTrue(EVIDENCE_PATH.exists(), f"missing evidence file: {EVIDENCE_PATH}")
text = EVIDENCE_PATH.read_text(encoding="utf-8")
for required in ["P0-0", "P0-8", "theme_validate", "theme_adherence", "pre_submit_review", "direct_svg"]:
self.assertIn(required, text)
for forbidden in ["自动审美已通过", "已支持完整主题系统", "PPTX export 已完成"]:
self.assertNotIn(forbidden, text)
def test_positive_artboard_fixture_validates_theme_and_final_svg(self) -> None:
with tempfile.TemporaryDirectory() as tmpdir:
project = copy_fixture("theme-system-p0/artboard-satori", tmpdir)
validation = svglide_theme_validate.validate_project(project)
svglide_theme_validate.write_outputs(project, validation)
adherence = svglide_theme_adherence.validate_project(project)
self.assertEqual(validation["status"], "passed", validation["issues"])
self.assertEqual(adherence["status"], "passed", adherence["issues"])
self.assertEqual(adherence["summary"]["prepared_svg_count"], 3)
def test_positive_direct_svg_fixture_validates_without_canvas_template(self) -> None:
with tempfile.TemporaryDirectory() as tmpdir:
project = copy_fixture("theme-system-p0/direct-svg", tmpdir)
validation = svglide_theme_validate.validate_project(project)
svglide_theme_validate.write_outputs(project, validation)
adherence = svglide_theme_adherence.validate_project(project)
self.assertEqual(validation["status"], "passed", validation["issues"])
self.assertEqual(adherence["status"], "passed", adherence["issues"])
def test_negative_fixtures_fail_for_unknown_color_low_contrast_and_stale_receipt(self) -> None:
with tempfile.TemporaryDirectory() as tmpdir:
unknown = copy_fixture("theme-system-invalid/unknown-color", tmpdir)
validation = svglide_theme_validate.validate_project(unknown)
svglide_theme_validate.write_outputs(unknown, validation)
unknown_result = svglide_theme_adherence.validate_project(unknown)
low_contrast = copy_fixture("theme-system-invalid/low-contrast", tmpdir)
validation = svglide_theme_validate.validate_project(low_contrast)
svglide_theme_validate.write_outputs(low_contrast, validation)
low_contrast_result = svglide_theme_adherence.validate_project(low_contrast)
stale = copy_fixture("theme-system-invalid/stale-theme-validate", tmpdir)
stale_result = svglide_theme_adherence.validate_project(stale)
self.assertEqual(unknown_result["status"], "failed")
self.assertIn("theme_unknown_color", issue_codes(unknown_result))
self.assertEqual(low_contrast_result["status"], "failed")
self.assertIn("contrast_too_low", issue_codes(low_contrast_result))
self.assertEqual(stale_result["status"], "failed")
self.assertIn("theme_validate_plan_stale", issue_codes(stale_result))
if __name__ == "__main__":
unittest.main()