feat(svglide): land artboard theme system gates

Implement SVGlide artboard Satori scaffolding, ThemeSpec validation/adherence gates, package and pre-submit checks, runner integration, quality gate freshness binding, and regression fixtures.

Tests: python3 -m unittest discover skills/lark-slides/scripts -p '*_test.py'; go test ./cmd/skill ./cmd/api
This commit is contained in:
songtianyi.theo
2026-06-21 21:07:20 +08:00
parent 94c5eb5b25
commit 842e2d44de
191 changed files with 44529 additions and 90 deletions

2
.gitignore vendored
View File

@@ -6,6 +6,8 @@ bin/
# Node
node_modules/
!skills/lark-slides/scripts/artboard_renderer/dist/
!skills/lark-slides/scripts/artboard_renderer/dist/render.mjs
# OS

View File

@@ -7,6 +7,7 @@ import (
"context"
"fmt"
"io"
"net/http"
"regexp"
"strings"
@@ -42,6 +43,7 @@ type APIOptions struct {
JqExpr string
DryRun bool
File string
Headers []string
}
var urlPrefixRe = regexp.MustCompile(`https?://[^/]+(/open-apis/.+)`)
@@ -94,6 +96,7 @@ func NewCmdApiWithContext(ctx context.Context, f *cmdutil.Factory, runF func(*AP
cmd.Flags().StringVarP(&opts.JqExpr, "jq", "q", "", "jq expression to filter JSON output")
cmd.Flags().BoolVar(&opts.DryRun, "dry-run", false, "print request without executing")
cmd.Flags().StringVar(&opts.File, "file", "", "file to upload as multipart/form-data ([field=]path, supports - for stdin)")
cmd.Flags().StringArrayVar(&opts.Headers, "request-header", nil, "internal request header for controlled lanes; repeat key=value, currently only x-tt-env=ppe_pure_svg is allowed")
cmd.ValidArgsFunction = func(_ *cobra.Command, args []string, _ string) ([]string, cobra.ShellCompDirective) {
if len(args) == 0 {
@@ -140,6 +143,13 @@ func buildAPIRequest(opts *APIOptions) (client.RawApiRequest, *cmdutil.FileUploa
Params: params,
As: opts.As,
}
headers, err := parseAPIRequestHeaders(opts.Headers)
if err != nil {
return client.RawApiRequest{}, nil, err
}
if len(headers) > 0 {
request.ExtraOpts = append(request.ExtraOpts, larkcore.WithHeaders(headers))
}
if opts.File != "" {
// File upload path: build formdata.
@@ -187,6 +197,30 @@ func buildAPIRequest(opts *APIOptions) (client.RawApiRequest, *cmdutil.FileUploa
return request, nil, nil
}
func parseAPIRequestHeaders(values []string) (http.Header, error) {
headers := http.Header{}
for _, raw := range values {
item := strings.TrimSpace(raw)
if item == "" {
return nil, output.ErrValidation("--request-header cannot be empty")
}
key, value, ok := strings.Cut(item, "=")
if !ok {
return nil, output.ErrValidation("--request-header %q must use key=value", item)
}
key = strings.TrimSpace(key)
value = strings.TrimSpace(value)
if !strings.EqualFold(key, "x-tt-env") {
return nil, output.ErrValidation("--request-header %q is not supported; only x-tt-env is allowed", key)
}
if value != "ppe_pure_svg" {
return nil, output.ErrValidation("--request-header x-tt-env must be ppe_pure_svg")
}
headers.Set("x-tt-env", value)
}
return headers, nil
}
func apiRun(opts *APIOptions) error {
f := opts.Factory
opts.As = f.ResolveAs(opts.Ctx, opts.Cmd, opts.As)

View File

@@ -88,6 +88,46 @@ func TestApiCmd_BotMode(t *testing.T) {
}
}
func TestApiCmd_RequestHeaderPassesToCall(t *testing.T) {
f, stdout, _, reg := cmdutil.TestFactory(t, &core.CliConfig{
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
})
stub := &httpmock.Stub{
URL: "/open-apis/test",
Body: map[string]interface{}{"code": 0, "msg": "ok", "data": map[string]interface{}{"result": "success"}},
}
reg.Register(stub)
cmd := NewCmdApi(f, nil)
cmd.SetArgs([]string{"GET", "/open-apis/test", "--as", "user", "--request-header", "x-tt-env=ppe_pure_svg"})
if err := cmd.Execute(); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got := stub.CapturedHeaders.Get("x-tt-env"); got != "ppe_pure_svg" {
t.Fatalf("x-tt-env header = %q, want ppe_pure_svg", got)
}
if !strings.Contains(stdout.String(), "success") {
t.Error("expected 'success' in output")
}
}
func TestApiCmd_RequestHeaderRejectsUnsupportedKey(t *testing.T) {
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
})
cmd := NewCmdApi(f, nil)
cmd.SetArgs([]string{"GET", "/open-apis/test", "--as", "user", "--request-header", "authorization=secret"})
err := cmd.Execute()
if err == nil {
t.Fatal("expected unsupported request header error")
}
if !strings.Contains(err.Error(), "only x-tt-env is allowed") {
t.Fatalf("err = %v, want supported-header message", err)
}
}
func TestApiCmd_MissingArgs(t *testing.T) {
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,

View File

@@ -50,7 +50,8 @@ func NewCmdSkill(f *cmdutil.Factory) *cobra.Command {
Short: "Read embedded skill content (list / read)",
Long: "Read agent-readable skill content (SKILL.md and reference files) embedded in " +
"the CLI binary at build time, so it stays in sync with the CLI version. " +
"Machine resources such as assets/ and scripts/ are not embedded.",
"Selected lark-slides prompts, scripts, and artboard renderer package files are also embedded; " +
"runtime dependency folders such as node_modules/ and generated artifacts are not embedded.",
}
// Risk is set on each leaf (GetRisk does not walk parents); the group has none.
cmdutil.DisableAuthCheck(cmd)

View File

@@ -251,6 +251,14 @@ func (ctx *RuntimeContext) CallAPI(method, url string, params map[string]interfa
return HandleApiResult(result, err, "API call failed")
}
// CallAPIWithHeaders is CallAPI plus request-scoped HTTP headers. Keep this for
// shortcuts that need a narrow, audited transport lane rather than global CLI
// header injection.
func (ctx *RuntimeContext) CallAPIWithHeaders(method, url string, params map[string]interface{}, data interface{}, headers http.Header) (map[string]interface{}, error) {
result, err := ctx.callRawWithHeaders(method, url, params, data, headers)
return HandleApiResult(result, err, "API call failed")
}
// CallAPITyped is the typed-only replacement for CallAPI: it performs the same
// SDK request (buildRequest → APIClient.DoAPI → DoSDKRequest, identical
// transport and query model to CallAPI) and returns the "data" object, but
@@ -422,11 +430,19 @@ func (ctx *RuntimeContext) buildRequest(method, url string, params map[string]in
}
func (ctx *RuntimeContext) callRaw(method, url string, params map[string]interface{}, data interface{}) (interface{}, error) {
return ctx.callRawWithHeaders(method, url, params, data, nil)
}
func (ctx *RuntimeContext) callRawWithHeaders(method, url string, params map[string]interface{}, data interface{}, headers http.Header) (interface{}, error) {
ac, err := ctx.getAPIClient()
if err != nil {
return nil, err
}
return ac.CallAPI(ctx.ctx, ctx.buildRequest(method, url, params, data))
req := ctx.buildRequest(method, url, params, data)
if len(headers) > 0 {
req.ExtraOpts = append(req.ExtraOpts, larkcore.WithHeaders(headers))
}
return ac.CallAPI(ctx.ctx, req)
}
// DoAPI executes a raw Lark SDK request with automatic auth handling.
@@ -1000,6 +1016,10 @@ func runShortcut(cmd *cobra.Command, f *cmdutil.Factory, s *Shortcut, botOnly bo
}
}
if wantDryRun, _ := cmd.Flags().GetBool("dry-run"); wantDryRun && s.DryRun != nil {
return runShortcutDryRunLocal(cmd, f, s, botOnly)
}
as, err := resolveShortcutIdentity(cmd, f, s)
if err != nil {
return err
@@ -1050,6 +1070,49 @@ func runShortcut(cmd *cobra.Command, f *cmdutil.Factory, s *Shortcut, botOnly bo
return rctx.outputErr
}
func runShortcutDryRunLocal(cmd *cobra.Command, f *cmdutil.Factory, s *Shortcut, botOnly bool) error {
asFlag, _ := cmd.Flags().GetString("as")
as := core.Identity(strings.TrimSpace(asFlag))
if as == "" || as == "auto" {
as = core.AsUser
}
if botOnly {
as = core.AsBot
}
if !shortcutSupportsIdentity(as, s.AuthTypes) {
return f.CheckIdentity(as, s.AuthTypes)
}
config := &core.CliConfig{}
rctx := newLocalDryRunRuntimeContext(cmd, f, s, config, as, botOnly)
if err := validateEnumFlags(rctx, s.Flags); err != nil {
return err
}
if err := resolveInputFlags(rctx, s.Flags); err != nil {
return err
}
if err := output.ValidateJqFlags(rctx.JqExpr, "", rctx.Format); err != nil {
return err
}
if s.Validate != nil {
if err := s.Validate(rctx.ctx, rctx); err != nil {
return err
}
}
return handleShortcutDryRun(f, rctx, s)
}
func shortcutSupportsIdentity(as core.Identity, authTypes []string) bool {
if len(authTypes) == 0 {
return true
}
for _, raw := range authTypes {
if core.Identity(raw) == as {
return true
}
}
return false
}
func resolveShortcutIdentity(cmd *cobra.Command, f *cmdutil.Factory, s *Shortcut) (core.Identity, error) {
// Step 1: determine identity (--as > default-as > auto-detect).
asFlag, _ := cmd.Flags().GetString("as")
@@ -1104,6 +1167,21 @@ func newRuntimeContext(cmd *cobra.Command, f *cmdutil.Factory, s *Shortcut, conf
return rctx, nil
}
func newLocalDryRunRuntimeContext(cmd *cobra.Command, f *cmdutil.Factory, s *Shortcut, config *core.CliConfig, as core.Identity, botOnly bool) *RuntimeContext {
ctx := cmd.Context()
ctx = cmdutil.ContextWithShortcut(ctx, s.Service+":"+s.Command, uuid.New().String())
rctx := &RuntimeContext{ctx: ctx, Config: config, Cmd: cmd, botOnly: botOnly, resolvedAs: as, Factory: f}
rctx.apiClientFunc = sync.OnceValues(func() (*client.APIClient, error) {
return nil, fmt.Errorf("API client is not available during local dry-run")
})
rctx.botInfoFunc = sync.OnceValues(func() (*BotInfo, error) {
return nil, fmt.Errorf("bot info is not available during local dry-run")
})
rctx.Format = rctx.Str("format")
rctx.JqExpr, _ = cmd.Flags().GetString("jq")
return rctx
}
// stripUTF8BOM removes a leading UTF-8 byte-order mark from content read from a
// file or stdin. A BOM that survives into a CSV cell corrupts the first value
// (e.g. "\ufeffNorth", which then makes a MAXIFS/lookup miss it), and a BOM at the

View File

@@ -7,6 +7,7 @@ import (
"context"
"encoding/json"
"fmt"
"net/http"
"path/filepath"
"strings"
@@ -219,7 +220,11 @@ func buildPresentationXML(title string) string {
}
func createEmptyPresentation(runtime *common.RuntimeContext, title string) (string, int, error) {
data, err := runtime.CallAPI(
return createEmptyPresentationWithHeaders(runtime, title, nil)
}
func createEmptyPresentationWithHeaders(runtime *common.RuntimeContext, title string, headers http.Header) (string, int, error) {
data, err := runtime.CallAPIWithHeaders(
"POST",
"/open-apis/slides_ai/v1/xml_presentations",
nil,
@@ -228,6 +233,7 @@ func createEmptyPresentation(runtime *common.RuntimeContext, title string) (stri
"content": buildPresentationXML(title),
},
},
headers,
)
if err != nil {
return "", 0, err

View File

@@ -6,6 +6,7 @@ package slides
import (
"context"
"fmt"
"net/http"
"strings"
"github.com/larksuite/cli/internal/output"
@@ -35,15 +36,24 @@ var SlidesCreateSVG = common.Shortcut{
Desc: "SVG file path; repeat for multiple pages",
},
{Name: "assets", Desc: "optional assets.json path mapping SVG @path placeholders to uploaded file tokens"},
{Name: "request-header", Type: "string_array", Desc: "internal request header for SVGlide live lanes; repeat key=value, currently only x-tt-env=ppe_pure_svg is allowed"},
},
Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
if err := validateSVGFileInputs(runtime, runtime.StrArray("file")); err != nil {
return err
}
return validateSVGAssetsPath(runtime, runtime.Str("assets"))
if err := validateSVGAssetsPath(runtime, runtime.Str("assets")); err != nil {
return err
}
_, err := parseSVGRequestHeaders(runtime.StrArray("request-header"))
return err
},
DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
title := effectiveTitle(runtime.Str("title"))
requestHeaders, err := parseSVGRequestHeaders(runtime.StrArray("request-header"))
if err != nil {
return common.NewDryRunAPI().Set("error", err.Error())
}
svgs, err := readSVGFiles(runtime, runtime.StrArray("file"))
if err != nil {
return common.NewDryRunAPI().Set("error", err.Error())
@@ -86,10 +96,17 @@ var SlidesCreateSVG = common.Shortcut{
if runtime.IsBot() {
dry.Desc("After creation succeeds in bot mode, the CLI will also try to grant the current CLI user full_access (可管理权限) on the new presentation.")
}
if len(requestHeaders) > 0 {
dry.Set("request_headers", svgRequestHeadersForOutput(requestHeaders))
}
return dry.Set("title", title)
},
Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
title := effectiveTitle(runtime.Str("title"))
requestHeaders, err := parseSVGRequestHeaders(runtime.StrArray("request-header"))
if err != nil {
return err
}
svgs, err := readSVGFiles(runtime, runtime.StrArray("file"))
if err != nil {
return err
@@ -99,7 +116,7 @@ var SlidesCreateSVG = common.Shortcut{
return err
}
presentationID, revisionID, err := createEmptyPresentation(runtime, title)
presentationID, revisionID, err := createEmptyPresentationWithHeaders(runtime, title, requestHeaders)
if err != nil {
return err
}
@@ -110,6 +127,9 @@ var SlidesCreateSVG = common.Shortcut{
if revisionID > 0 {
result["revision_id"] = revisionID
}
if len(requestHeaders) > 0 {
result["request_headers"] = svgRequestHeadersForOutput(requestHeaders)
}
pages, uploaded, err := rewriteSVGImagePlaceholders(runtime, presentationID, svgs, assets)
if err != nil {
@@ -133,11 +153,12 @@ var SlidesCreateSVG = common.Shortcut{
"page %d/%d failed before API call: %v (presentation %s was created; %d slide(s) added; slide_ids=%s)",
i+1, len(pages), err, presentationID, len(slideIDs), strings.Join(slideIDs, ","))
}
slideData, err := runtime.CallAPI(
slideData, err := runtime.CallAPIWithHeaders(
"POST",
slideURL,
map[string]interface{}{"revision_id": -1},
buildCreateSVGBody(content),
requestHeaders,
)
if err != nil {
return output.Errorf(output.ExitAPI, "api_error",
@@ -159,3 +180,35 @@ var SlidesCreateSVG = common.Shortcut{
return nil
},
}
func parseSVGRequestHeaders(values []string) (http.Header, error) {
headers := http.Header{}
for _, raw := range values {
item := strings.TrimSpace(raw)
if item == "" {
return nil, output.ErrValidation("--request-header cannot be empty")
}
key, value, ok := strings.Cut(item, "=")
if !ok {
return nil, output.ErrValidation("--request-header %q must use key=value", item)
}
key = strings.TrimSpace(key)
value = strings.TrimSpace(value)
if !strings.EqualFold(key, "x-tt-env") {
return nil, output.ErrValidation("--request-header %q is not supported; only x-tt-env is allowed", key)
}
if value != "ppe_pure_svg" {
return nil, output.ErrValidation("--request-header x-tt-env must be ppe_pure_svg")
}
headers.Set("x-tt-env", value)
}
return headers, nil
}
func svgRequestHeadersForOutput(headers http.Header) map[string]string {
out := map[string]string{}
if value := headers.Get("x-tt-env"); value != "" {
out["x-tt-env"] = value
}
return out
}

View File

@@ -145,6 +145,80 @@ func TestSlidesCreateSVGExecuteCreatesSlidesInFileOrder(t *testing.T) {
assertSlideCreateBodyContains(t, slideStub2, `<foreignObject slide:role="shape" slide:shape-type="text" x="80" y="80" width="320" height="80">`)
}
func TestSlidesCreateSVGRequestHeaderPassesToCreateAndSlideCalls(t *testing.T) {
dir := t.TempDir()
withSlidesTestWorkingDir(t, dir)
if err := os.WriteFile("page.svg", []byte(testSVGlidePage1), 0o644); err != nil {
t.Fatalf("write page.svg: %v", err)
}
f, stdout, _, reg := cmdutil.TestFactory(t, slidesTestConfig(t, ""))
createStub := &httpmock.Stub{
Method: "POST",
URL: "/open-apis/slides_ai/v1/xml_presentations",
Body: map[string]interface{}{
"code": 0,
"data": map[string]interface{}{
"xml_presentation_id": "pres_header",
"revision_id": 1,
},
},
}
slideStub := &httpmock.Stub{
Method: "POST",
URL: "/open-apis/slides_ai/v1/xml_presentations/pres_header/slide",
Body: map[string]interface{}{"code": 0, "data": map[string]interface{}{"slide_id": "slide_header", "revision_id": 2}},
}
reg.Register(createStub)
reg.Register(slideStub)
registerBatchQueryStub(reg, "pres_header", "https://x.feishu.cn/slides/pres_header")
err := runSlidesCreateSVGShortcut(t, f, stdout, []string{
"+create-svg",
"--file", "page.svg",
"--title", "SVG Header Deck",
"--request-header", "x-tt-env=ppe_pure_svg",
"--as", "user",
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got := createStub.CapturedHeaders.Get("x-tt-env"); got != "ppe_pure_svg" {
t.Fatalf("create x-tt-env = %q, want ppe_pure_svg", got)
}
if got := slideStub.CapturedHeaders.Get("x-tt-env"); got != "ppe_pure_svg" {
t.Fatalf("slide x-tt-env = %q, want ppe_pure_svg", got)
}
data := decodeSlidesCreateEnvelope(t, stdout)
headers, _ := data["request_headers"].(map[string]interface{})
if headers["x-tt-env"] != "ppe_pure_svg" {
t.Fatalf("request_headers = %#v, want x-tt-env", data["request_headers"])
}
}
func TestSlidesCreateSVGRejectsUnsupportedRequestHeader(t *testing.T) {
dir := t.TempDir()
withSlidesTestWorkingDir(t, dir)
if err := os.WriteFile("page.svg", []byte(testSVGlidePage1), 0o644); err != nil {
t.Fatalf("write page.svg: %v", err)
}
f, stdout, _, _ := cmdutil.TestFactory(t, slidesTestConfig(t, ""))
err := runSlidesCreateSVGShortcut(t, f, stdout, []string{
"+create-svg",
"--file", "page.svg",
"--request-header", "authorization=secret",
"--as", "user",
})
if err == nil {
t.Fatal("expected unsupported request header error")
}
if !strings.Contains(err.Error(), "only x-tt-env is allowed") {
t.Fatalf("err = %v, want supported-header message", err)
}
}
func TestSlidesCreateSVGChartMarkerPassesThroughSlideContent(t *testing.T) {
dir := t.TempDir()
withSlidesTestWorkingDir(t, dir)

View File

@@ -0,0 +1,30 @@
# SVGlide Canvas Planner Prompt
## Contract
- Input bundle: `02-plan/slide-plan.json`, `svglide-template-registry.json`, `themes/registry.json`, `svglide-canvas-spec.schema.json`, golden CanvasSpec examples.
- Output schema: `skills/lark-slides/references/svglide-canvas-plan.schema.json`.
- Output path: `02-plan/slide_plan.json`.
- Validation command: `python3 skills/lark-slides/scripts/svglide_planner_contracts.py <project>`.
## Output Rules
Return JSON only. Do not wrap the answer in Markdown fences.
The Canvas Planner turns each slide plan into the final `generation_mode=artboard_satori` `slide_plan.json`. Every slide must include a full `canvas_spec` with:
- `version`
- `canvas`
- `safe_area`
- `template_id`
- `theme_id`
- `theme`
- `content`
- `semantic_elements`
- `quality_constraints`
The output must pass `svglide-plan.schema.json`, `svglide-canvas-plan.schema.json`, CanvasSpec validation, Template Registry binding, Theme Registry binding, and template text-budget/max-items checks before Satori is invoked.
## Forbidden Outputs
Do not output free HTML, CSS, SVG, JSX, TSX, Markdown prose, raw Satori SVG, foreignObject snippets, or arbitrary inline style. Use structured CanvasSpec JSON only.

View File

@@ -0,0 +1,25 @@
# SVGlide Deck Planner Prompt
## Contract
- Input bundle: `user_topic`, `audience`, `target_slide_count`, `source_policy`, `available_template_registry`, `available_theme_registry`.
- Output schema: `skills/lark-slides/references/svglide-deck-plan.schema.json`.
- Output path: `02-plan/deck-plan.json`.
- Validation command: `python3 skills/lark-slides/scripts/svglide_planner_contracts.py <project>`.
## Output Rules
Return JSON only. Do not wrap the answer in Markdown fences.
The Deck Planner defines the narrative system for the whole deck:
- objective
- audience
- target slide count
- narrative arc
- theme direction
- per-slide role, key message, content goal, and visual goal
## Forbidden Outputs
Do not output free HTML, CSS, SVG, JSX, TSX, Markdown prose, base64 image data, or rendered visual markup. Do not create page geometry here. Do not invent numeric claims; mark missing facts as `pending_confirmation`.

View File

@@ -0,0 +1,24 @@
# SVGlide Repair Planner Prompt
## Contract
- Input bundle: validation receipt, target planner JSON, schema issue list, template fit issue list.
- Output schema: `skills/lark-slides/references/svglide-repair-plan.schema.json`.
- Output path: `02-plan/repair-plan.json`.
- Validation command: `python3 skills/lark-slides/scripts/svglide_planner_contracts.py <project>`.
## Output Rules
Return JSON only. Do not wrap the answer in Markdown fences.
The Repair Planner outputs scoped JSON Patch operations only. Each patch must target one precise field, such as:
- `/slides/0/canvas_spec/content/title`
- `/slides/1/canvas_spec/content/right_points/2`
- `/slides/2/canvas_spec/semantic_elements/0/bbox/width`
Every patch must include a short `reason` tied to a validation issue.
## Forbidden Outputs
Do not rewrite the full deck. Do not output `slides`, full `canvas_spec`, full `deck_plan`, free HTML, CSS, SVG, JSX, TSX, Markdown prose, or unscoped patch paths such as `/`, `/slides`, `/slides/0`, or `/slides/0/canvas_spec`.

View File

@@ -0,0 +1,20 @@
# SVGlide Slide Planner Prompt
## Contract
- Input bundle: `02-plan/deck-plan.json`, `svglide-template-registry.json`, `themes/registry.json`, `svglide-layout-archetypes.json`, `svglide-component-registry.json`.
- Output schema: `skills/lark-slides/references/svglide-slide-plan.schema.json`.
- Output path: `02-plan/slide-plan.json`.
- Validation command: `python3 skills/lark-slides/scripts/svglide_planner_contracts.py <project>`.
## Output Rules
Return JSON only. Do not wrap the answer in Markdown fences.
The Slide Planner chooses registered templates and themes for each slide. It may define structured content requirements, but it must not write CanvasSpec yet.
Every slide must choose a `template_id` and `theme_id` from the registries. Keep content short enough for the selected template budgets.
## Forbidden Outputs
Do not output free HTML, CSS, SVG, JSX, TSX, Markdown prose, raw Satori SVG, or unregistered template/theme IDs. Do not bypass Template Registry or Theme Registry.

View File

@@ -0,0 +1,14 @@
module.exports = function (callback) {
callback({
name: 'lark-cli-openapi-ppe-pure-svg',
groupName: 'lark-cli',
rules: [
'/^https:\\/\\/open\\.feishu\\.cn\\/(.*)$/ https://open.feishu-pre.cn/$1',
'https://open.feishu.cn/ reqHeaders://Env=Pre_release',
'https://open.feishu.cn/ reqHeaders://x-tt-env=ppe_pure_svg',
'https://open.feishu-pre.cn/ reqHeaders://Env=Pre_release',
'https://open.feishu-pre.cn/ reqHeaders://x-tt-env=ppe_pure_svg',
'/^https:\\/\\/accounts\\.feishu\\.cn\\/(.*)$/ https://accounts.feishu-pre.cn/$1',
].join('\n'),
});
};

View File

@@ -15,6 +15,7 @@
"skills/lark-slides/references/svglide-lock.contract.md",
"skills/lark-slides/references/svglide-assets.contract.md",
"skills/lark-slides/references/svglide-generate-svg.contract.md",
"skills/lark-slides/references/svglide-artboard-satori.contract.md",
"skills/lark-slides/references/svglide-preview.spec.md",
"skills/lark-slides/references/svglide-checks.checklist.md",
"skills/lark-slides/references/svglide-readback.contract.md",
@@ -31,6 +32,10 @@
"skills/lark-slides/references/svglide-asset-planning.md",
"skills/lark-slides/references/safe-native-v1.profile.json",
"skills/lark-slides/references/svglide-plan.schema.json",
"skills/lark-slides/references/svglide-canvas-spec.schema.json",
"skills/lark-slides/references/svglide-semantic-map.schema.json",
"skills/lark-slides/references/svglide-node-layout-map.schema.json",
"skills/lark-slides/references/svglide-artboard-receipt.schema.json",
"skills/lark-slides/references/svglide-strategy-review.schema.json",
"skills/lark-slides/references/svglide-ppt-master-asset-map.schema.json",
"skills/lark-slides/references/svglide-template-admission.schema.json",
@@ -53,6 +58,12 @@
"skills/lark-slides/scripts/svg_preview_lint.py",
"skills/lark-slides/scripts/svglide_source.py",
"skills/lark-slides/scripts/svglide_assets.py",
"skills/lark-slides/scripts/svglide_artboard_renderer.py",
"skills/lark-slides/scripts/svglide_template_fit_check.py",
"skills/lark-slides/scripts/artboard_renderer/package.json",
"skills/lark-slides/scripts/artboard_renderer/render.mjs",
"skills/lark-slides/scripts/artboard_renderer/templates/README.md",
"skills/lark-slides/scripts/artboard_renderer/themes/dark-clarity.json",
"skills/lark-slides/scripts/svglide_project_runner.py",
"skills/lark-slides/scripts/svglide_strategy_review.py",
"skills/lark-slides/scripts/svglide_prepare.py",
@@ -84,6 +95,8 @@
"skills/lark-slides/references/svg-protocol.md",
"skills/lark-slides/scripts/svglide_source.py",
"skills/lark-slides/scripts/svglide_assets.py",
"skills/lark-slides/scripts/svglide_artboard_renderer.py",
"skills/lark-slides/scripts/svglide_template_fit_check.py",
"skills/lark-slides/scripts/svglide_project_runner.py",
"skills/lark-slides/scripts/svglide_strategy_review.py",
"skills/lark-slides/scripts/svglide_prepare.py",

View File

@@ -0,0 +1,920 @@
# SVGlide Artboard/Satori Full Plan Action And Supervision Guide
Last updated: 2026-06-21
## 0. Strict Full-Plan Mandate
This file is the local action plan for completing the entire `/Users/bytedance/Downloads/PLAN.md`, not only P0, not only a demo deck, and not only the currently unblocked local vertical slice.
Executor and reviewer agents must treat completion as an evidence-based state:
```text
all gates 0-12b DONE
all reviewer verdicts PASS
PLAN.md completion status updated
real live_create/readback evidence present
legacy direct_svg regression still passing
instruction/plan/output adherence evidence present
packaging/distribution decision closed or explicitly rescoped in PLAN.md
```
Anything less than that is `IN_PROGRESS` or `BLOCKED`.
The current execution cursor is:
```text
Gate 12b: Final Full-Plan Acceptance
Status: DONE
Current issue: Gate 12b final reviewer verdict is PASS; current P0/P1 milestone is complete
Next allowed executor task: stop or start a new explicitly scoped follow-up
Next forbidden executor task: claiming P2/future scope as complete
```
Supervisor rule:
- The reviewer subagent must reject any attempt to skip the current cursor.
- The reviewer subagent must reject "looks good" visual evidence unless it is tied to receipts, hashes, commands, and runner output.
- The reviewer subagent must reject fake dry-run, handwritten `.tmp` state, system screenshots, or direct Satori SVG as completion evidence.
- The executor must update this file after every gate status change.
- The executor must send this file, the relevant evidence file, changed file list, and validation commands to the reviewer before claiming a gate is done.
## 1. Goal
The goal is to complete the full plan in:
```text
/Users/bytedance/Downloads/PLAN.md
```
This file is the local execution and supervision contract for executor agents and reviewer subagents. It exists to prevent partial vertical slices, visual demos, or smoke tests from being mistaken for completion of the full plan.
Completion means:
```text
Deck/Slide planning contract
-> CanvasSpec as page source of truth
-> Template Registry / Theme Token / Component Library
-> Satori renderer
-> resvg preview and raster fallback path
-> SatoriToSVGlide compiler
-> existing SVGlide prepare / preview / preflight / reviews / quality_gate
-> dry_run
-> ppe_proof
-> live_create
-> readback
-> instruction / plan / output adherence
-> regression and packaging decision evidence
```
The current state is not full completion. As of 2026-06-21:
```text
Gates 0-8: complete with reviewer PASS
Gate 9: complete with reviewer PASS
Gate 10: complete with reviewer PASS
Gate 11: complete with reviewer PASS
Gate 12a: complete with reviewer PASS
Gate 12b: complete with reviewer PASS
P1: asset system scale-out, prompt/planning, and packaging decision complete with reviewer PASS
P2: not started
```
Current active cursor:
```text
Gate 8 is now DONE/PASS. It was previously blocked because
`xml_presentations.get` returned 5090000 for presentations containing
`svglide-chart-spec-v1` chart markers.
The slide-side fix has now been committed, pushed, built, and deployed to
`creation.slide.nodeserver_pre_release` in `ppe_pure_svg`:
slide branch: feat/svglide-chart-direct-snapshot
slide commit: 8f682ab082f7d86ade966eb2ffc5849827b17dc5
ENV ticket: 2068537756495360000
TCE deployment: 362509781
TCE service: 208677037
deployed main repo: ee/slide/server@1.0.0.1184
service status: running
Fresh Gate 8 readback now passes:
chart-only deck: C5fxszdjrlftMedvShmcOWtinqe
chart-only slide: pvv
combined deck: J35tspvJgltBnsdJpL7chnv6n2f
combined slides: pdd, pdu, pdR
combined checks: page_count, slide_order, blank_page, text_fit, bounds,
chart_markers, image_assets, core_visible_text all passed.
Reviewer verdict: PASS.
Next cursor: current P0/P1 milestone closed with Gate 12b reviewer PASS.
The full PLAN is not complete until Gate 12b also passes review.
```
## 2. Source Of Truth
Primary source:
```text
/Users/bytedance/Downloads/PLAN.md
```
Branch and worktree:
```text
worktree: /Users/bytedance/bd-projects/workspaces/SVGlide/.worktrees/cli-svglide-svg-private
branch: feat/svglide-artboard-satori
```
Important local references:
```text
skills/lark-slides/references/svglide-artboard-p0-goal-lock.md
skills/lark-slides/references/svglide-artboard-satori.contract.md
skills/lark-slides/references/svglide-canvas-spec.schema.json
skills/lark-slides/references/svglide-semantic-map.schema.json
skills/lark-slides/references/svglide-node-layout-map.schema.json
skills/lark-slides/references/svglide-artboard-receipt.schema.json
skills/lark-slides/references/svglide-template-registry.json
skills/lark-slides/scripts/svglide_project_runner.py
skills/lark-slides/scripts/svglide_artboard_renderer.py
skills/lark-slides/scripts/svglide_template_fit_check.py
skills/lark-slides/scripts/svglide_quality_gate.py
skills/lark-slides/scripts/artboard_renderer/
```
If this guide conflicts with `PLAN.md`, `PLAN.md` wins. If `PLAN.md` is too vague for implementation, update the plan or a referenced contract first, then implement.
## 3. Roles
Executor:
- Implements only the next unblocked task.
- Updates receipts, tests, fixtures, and docs together with code changes.
- Reports exact files changed, exact validations run, and remaining gaps.
- Must not mark a gate complete without reviewer evidence.
Reviewer subagent:
- Audits against this guide and `PLAN.md`.
- Challenges missing evidence, vague claims, or shortcut paths.
- Returns `PASS` only when all blocking checks for the current gate pass.
- Returns `BLOCKED` if any required artifact, receipt, freshness check, or verification is missing.
- Owns final gate verdicts. The executor cannot self-promote a gate to reviewer `PASS`.
- Must inspect repo files and receipts, not only executor prose.
Main agent:
- Maintains the plan and task status.
- Dispatches implementation and review.
- Resolves conflicts between executor and reviewer using repo evidence.
- Keeps this file updated whenever a gate changes state.
- Re-routes work back to the executor when the reviewer returns `BLOCKED`.
## 4. Hard Rules
Do not use these shortcuts as completion evidence:
```text
direct_svg demo replacing artboard_satori evidence
handwritten .tmp generator replacing runner integration
QuickLook / qlmanage / system screenshot replacing resvg main path
manual state.json patch replacing runner behavior
relaxed production gate replacing correct receipts
Satori SVG copied directly to live SVG
unregistered template or theme treated as valid
external HTML/CSS library directly used as runtime renderer
quality_gate checking only file existence without hash/freshness
fake lark-cli dry_run treated as live_create/readback evidence
executor-written claim treated as reviewer evidence
```
Temporary diagnostics are allowed, but diagnostic outputs cannot be counted as plan completion.
## 4.1 Supervision Loop
For every gate from Gate 4 onward, use this loop:
```text
1. Executor selects exactly one gate or one blocker inside a gate.
2. Executor updates code/docs/fixtures/receipts for that scope only.
3. Executor runs the minimum validation commands listed in this file.
4. Executor writes or updates a gate evidence file.
5. Main agent sends the exact scope, changed files, commands, and evidence paths to the reviewer subagent.
6. Reviewer returns PASS or BLOCKED using the required format.
7. Main agent updates the status board.
8. If BLOCKED, the next executor task must address the reviewer blocker before moving to another gate.
```
The executor must not skip from a blocked gate to a later gate unless this file records an explicit scope deferral approved by the reviewer.
## 5. Required Reporting Format
Every executor update must use this shape:
```text
Scope:
- ...
Files changed:
- ...
Validation run:
- command:
- result:
- evidence path:
Plan items advanced:
- PLAN.md section:
- previous status:
- new status:
Remaining blockers:
- ...
```
Every reviewer subagent response must use this shape:
```text
Verdict: PASS / BLOCKED
Blocking issues:
- ...
Non-blocking risks:
- ...
Evidence checked:
- file / command / receipt:
Next required action:
- ...
```
## 6. Execution Gates
The executor must proceed in order. A later gate cannot be marked complete while an earlier gate is blocked.
### Gate 0: Baseline And Branch Discipline
Purpose: prove the work starts from the right branch and does not break legacy `direct_svg`.
Required actions:
- Confirm current branch is `feat/svglide-artboard-satori`.
- Confirm worktree is `/Users/bytedance/bd-projects/workspaces/SVGlide/.worktrees/cli-svglide-svg-private`.
- Record dirty files before starting each chunk.
- Run legacy direct SVG regression.
- Confirm no `slide_engine` or `slide` changes are required for P0.
Required evidence:
```text
git status --short --branch
legacy direct_svg runner receipt
legacy quality_gate result
```
Reviewer blocking checks:
- Block if branch/worktree is wrong.
- Block if direct SVG is broken without an explicit PLAN-approved migration.
- Block if unrelated repo changes are included.
### Gate 1: Contract Layer Completion
Purpose: close the engineering contract before adding more visual features.
Required actions:
- Finalize `generation_mode=direct_svg|artboard_satori`.
- Keep existing `generator_mode=script|external` semantics unchanged.
- Make `CanvasSpec` the page source of truth for artboard mode.
- Make `semantic-map/v1` an explicit compiler IR.
- Make `node-layout-map/v1` an explicit Satori/Yoga observation artifact or document the accepted substitute.
- Update generator receipt schema to include artboard receipts and hashes.
- Ensure `artboard_satori` requires legal `canvas_spec` per page.
Required artifacts:
```text
skills/lark-slides/references/svglide-plan.schema.json
skills/lark-slides/references/svglide-canvas-spec.schema.json
skills/lark-slides/references/svglide-semantic-map.schema.json
skills/lark-slides/references/svglide-node-layout-map.schema.json
skills/lark-slides/references/svglide-generator-receipt.schema.json
skills/lark-slides/references/svglide-artboard-receipt.schema.json
skills/lark-slides/references/svglide-artboard-satori.contract.md
```
Reviewer blocking checks:
- Block if `CanvasSpec` is optional in `artboard_satori` mode.
- Block if semantic map and node layout map boundaries are unclear.
- Block if receipt schema does not support hash/freshness verification.
- Block if legacy `direct_svg` plans no longer validate.
### Gate 2: Template, Theme, Component, And Input Quality System
Purpose: prevent Satori from rendering arbitrary low-quality input.
Required actions:
- Provide at least 3 P0 templates:
- `cover-hero`
- `comparison-cards`
- `summary-final`
- Provide at least 3 formal theme tokens in the renderer registry or theme registry.
- Provide a Satori-compatible component layer:
- `Title`
- `Subtitle`
- `Chip`
- `StatCard`
- `ImageFrame`
- Ensure all templates and components use only the approved Satori CSS subset.
- Implement template fit checks for:
- unknown `template_id`
- unknown `theme_id`
- overlong title
- missing required content
- card count overflow
- unsafe text budget
- unsafe bbox or safe-area violation
- Add golden fixtures for each P0 template.
Required artifacts:
```text
skills/lark-slides/references/svglide-template-registry.json
skills/lark-slides/scripts/artboard_renderer/templates/
skills/lark-slides/scripts/artboard_renderer/themes/
skills/lark-slides/scripts/artboard_renderer/components/
skills/lark-slides/scripts/svglide_template_fit_check.py
skills/lark-slides/scripts/fixtures/svglide_artboard/
receipts/template-fit-check.json in fixture runs
```
Reviewer blocking checks:
- Block if only one formal theme exists.
- Block if demo-local themes are not registered.
- Block if templates are hardcoded without registry hashes.
- Block if unknown template/theme does not fail fast.
- Block if fixtures are too narrow to prove 3-template behavior.
### Gate 3: Satori Renderer And resvg Preview
Purpose: prove CanvasSpec can render into preview artifacts through the intended renderer.
Required actions:
- Keep `artboard_renderer` as an isolated Node package for P0.
- Declare `satori` and `@resvg/resvg-js` in `package.json`.
- Keep `pnpm-lock.yaml` committed for the subpackage.
- Render each page to raw Satori SVG.
- Render each page to PNG using `@resvg/resvg-js`.
- Generate contact sheet from PNG artifacts.
- Record renderer runtime details.
- Record font hashes.
- Record input and output hashes.
Required artifacts:
```text
skills/lark-slides/scripts/artboard_renderer/package.json
skills/lark-slides/scripts/artboard_renderer/pnpm-lock.yaml
04-svg/artboard/raw/page-###.satori.svg
04-svg/artboard/page-###.png
05-preview/contact-sheet.png
receipts/artboard-render.json
```
Required validation commands:
```bash
node skills/lark-slides/scripts/artboard_renderer/render.mjs --check-runtime
node skills/lark-slides/scripts/artboard_renderer/dist/render.mjs --check-runtime
```
Reviewer blocking checks:
- Block if `resvg_version` is missing.
- Block if PNGs are produced by QuickLook/system screenshot as the main path.
- Block if Satori SVG is empty or copied directly to live output.
- Block if font hashes are not recorded.
- Block if renderer artifacts are not hash-bound to CanvasSpec/theme/template inputs.
### Gate 4: SatoriToSVGlide Compiler
Purpose: produce editable SVGlide protocol SVG from CanvasSpec and renderer outputs.
Current reviewer verdict: `PASS`.
Resolved blocking issues:
```text
1. The current compiler path still treats raw Satori SVG as the conversion input.
The receipt may say `CanvasSpec`, but the implementation must prove the
generated SVGlide protocol SVG is derived from CanvasSpec / semantic-map /
owned template IR.
2. P0 Gate 4 scope must be explicit. If image/chart mapping is not implemented
in Gate 4, the plan must state that P0 Gate 4 only certifies text/shape
mapping, while image/chart/readback remain Gate 8/P0c work.
```
Required actions:
- Convert using CanvasSpec/semantic map, not by trusting raw Satori SVG as semantic truth.
- Keep raw Satori SVG as preview/layout evidence only.
- Record the actual compiler input artifact and hash, for example:
- `04-svg/artboard/page-###.canvas-template.svg`
- `canvas_template_svg_sha256`
- `compiler_input=CanvasSpecTemplateSVG`
- `satori_svg_usage=preview_only`
- Output current SVGlide protocol:
- `xmlns:slide="https://slides.bytedance.com/ns"`
- valid `slide:role`
- valid text/shape mapping for P0 Gate 4
- Ensure final live SVG does not contain unsupported raw Satori constructs.
- Implement fail-fast or decorative raster fallback for unsupported Satori features.
- Write bridge receipt with input and output hashes.
- If image/chart mapping is deferred, explicitly document the deferral and keep Gate 8 as the owner for chart/image proof.
Required artifacts:
```text
04-svg/page-###.svg
04-svg/artboard/page-###.canvas-template.svg
receipts/satori-bridge.json
```
Reviewer blocking checks:
- Block if final SVG is just raw Satori SVG.
- Block if final SVG is compiled from raw Satori SVG while receipts imply CanvasSpec.
- Block if the compiler input artifact and hash are missing.
- Block if text is not mapped to the current SVGlide text protocol.
- Block if unsupported `filter/mask/clipPath/pattern/foreignObject` paths enter live without explicit fallback policy.
- Block if image/chart mapping is claimed but no fixture/readback evidence exists.
- Block if bridge receipt cannot prove current input/output freshness.
### Gate 5: Runner And Quality Gate Integration
Purpose: make the new path part of the existing stage graph, not a side script.
Required actions:
- `generate_svg` dispatches on `generation_mode`.
- `direct_svg` legacy path remains compatible.
- `artboard_satori` runs CanvasSpec validation, template fit, render, bridge, and generate receipt.
- Per-page render/compile supports bounded concurrency:
- default `max_workers = min(4, page_count)`
- final generated file order must remain page-number stable
- `quality_gate` validates all artboard receipts and hashes.
- `quality_gate` fails on stale prepared SVG.
- `quality_gate` fails when required artboard receipts are missing.
Required artifacts:
```text
skills/lark-slides/scripts/svglide_project_runner.py
skills/lark-slides/scripts/svglide_quality_gate.py
receipts/generate_svg.json
06-check/quality-gate.json
```
Required validation:
```bash
python3 -m unittest discover skills/lark-slides/scripts -p '*_test.py'
```
Reviewer blocking checks:
- Block if `artboard_satori` can pass with missing receipts.
- Block if hash/freshness checks only check file existence.
- Block if prepared SVG can be modified after bridge and still pass.
- Block if page order is nondeterministic under concurrency.
- Block if direct SVG regression tests are not run.
### Gate 6: P0a And P0b Local E2E
Purpose: prove 1-page and 3-page local vertical slices are repeatable.
Required actions:
- Add a complete P0a fixture bundle.
- Add a complete P0b fixture bundle.
- P0a runs to `dry_run`.
- P0b runs at least to `quality_gate`, preferably to `dry_run`.
- Fixture bundles include required project files, not just `slide_plan.json`.
- Evidence must prove:
- artboard mode was used
- all 3 P0 templates were hit
- resvg was used
- quality gate passed
- dry run passed when required
Required fixture paths:
```text
skills/lark-slides/scripts/fixtures/svglide_artboard/
```
Required output evidence:
```text
02-plan/slide_plan.json
04-svg/artboard/raw/page-###.satori.svg
04-svg/artboard/page-###.png
04-svg/page-###.svg
05-preview/contact-sheet.png
06-check/quality-gate.json
07-create/dry-run.json
receipts/canvas-spec-validate.json
receipts/template-fit-check.json
receipts/artboard-render.json
receipts/satori-bridge.json
receipts/generate_svg.json
```
Reviewer blocking checks:
- Block if demo is generated outside the fixture path and cannot be repeated.
- Block if P0b does not hit all required P0 templates.
- Block if P0b uses only one formal theme while plan requires three.
- Block if fixture bypasses runner stages.
### Gate 7: P0c Live Closure
Purpose: prove the new path can create and read back real Slides.
Required actions:
- Prepare a P0c fixture with `07-create/ppe-proof.input.json`.
- Run `dry_run`.
- Run `ppe_proof`.
- Run `live_create`.
- Run `readback`.
- Verify created slide count, page order, core visible text, and no blank pages.
- Verify the online result does not have obvious overflow or missing images.
Required artifacts:
```text
07-create/dry-run.json
07-create/ppe-proof.json
07-create/live-create.json
08-readback/readback-check.json
```
Reviewer blocking checks:
- Block if fake lark-cli is used as live evidence.
- Block if `ppe_proof` inputs do not match current quality gate and dry-run outputs.
- Block if readback is not performed.
- Block if created pages are blank, missing expected text, or out of order.
### Gate 8: Special Cases And Fallback Coverage
Purpose: cover features likely to break the renderer/compiler boundary.
Required actions:
- Add unsupported Satori feature fixture.
- Add chart marker fixture for `svglide-chart-spec-v1`.
- Add image asset fixture with binding and readback.
- Add local raster fallback fixture for an isolated unsupported decoration.
- Prove fail-fast behavior before live for unsupported editable features.
Reviewer blocking checks:
- Block if unsupported features silently pass.
- Block if raster fallback lacks bbox/island receipt.
- Block if chart marker or image asset is not verified through readback.
### Gate 9: P1 Asset System Scale-Out
Purpose: move from P0 templates to a reusable visual system.
Required actions:
- Convert external visual references into owned Satori-compatible templates.
- Use external repos only as inspiration/input data, not runtime dependency.
- Build a source intake inventory before converting assets. Each source family must record:
- `source_path`
- `source_type`
- `extract_fields`
- `conversion_target`
- `acceptance_rule`
- `forbidden_usage`
- Target P1 asset counts from `PLAN.md`:
- 15-25 Canvas Templates
- 10-18 Theme Tokens
- 20-40 Component Variants
- 6-10 Layout Archetypes
- Add abstraction guide evidence for each source family.
- Add template quality fixtures and regression previews.
Allowed reference sources and intake rules:
```text
1. /Users/bytedance/bd-projects/open-design/design-templates/*
Read: template.json, example.html, preview screenshots when available.
Extract: palette, typography, mood, density, occasion, layout skeleton, component combinations.
Convert to: Theme Token, Canvas Template brief, Satori component candidates.
Forbidden: do not run arbitrary HTML/CSS as the SVGlide runtime renderer.
2. /Users/bytedance/bd-projects/open-design/design-templates/html-ppt-zhangzara-*
Read: template.json, example.html, style sample images.
Extract: high-aesthetic style DNA, color hierarchy, display/body type pairing, spacing, poster/editorial/grid traits.
Convert to: deduped strong-style Theme Tokens and a small number of strong-style Canvas Templates.
Forbidden: do not copy all style packs blindly; dedupe and keep only reusable Satori-compatible patterns.
3. /Users/bytedance/bd-projects/ppt-master/examples/examples.json
plus /Users/bytedance/bd-projects/ppt-master/examples/*/{design_spec.md,spec_lock.md,svg_final/*.svg}
Extract: deck rhythm, page archetypes, visual style rules, palette rules, element density, bbox/font-size facts, golden page examples.
Convert to: golden fixtures, style/quality rules, deck rhythm patterns, Canvas Template candidates.
Forbidden: do not use ppt-master SVG as final SVGlide output and do not add ppt-master as a CLI runtime dependency.
4. /Users/bytedance/bd-projects/workspaces/SVGlide/PosterGen
Read: config/poster_config.yaml, config/prompts/*.txt, src/agents/*, resource/*, data/*.
Extract: research-poster layout heuristics, section balancing, color extraction, text-height/min-font rules, logo/affiliation/image placement patterns.
Convert to: special research-poster templates, quality rules, layout heuristics, asset-binding cases.
Forbidden: do not integrate PosterGen's LangGraph/Python generation workflow as the SVGlide runtime.
5. html-ppt-skill
Status: allowed only after the real local path is documented.
Extract: full-deck rhythm, single-page layouts, theme tokens.
Convert to: deck rhythm patterns, Canvas Templates, Theme Tokens.
Forbidden: do not count this source as verified until the source path and inventory evidence exist.
6. Other poster / OG / example sources
Status: allowed only when documented in PLAN.md with source path, license/provenance, intake fields, and forbidden usage.
```
Reviewer blocking checks:
- Block if an external HTML/CSS/SVG library is directly used as runtime renderer.
- Block if a source family has no intake inventory entry.
- Block if a source path is unverified but counted toward P1 asset targets.
- Block if conversion evidence does not show source example -> abstraction record -> CanvasSpec fixture -> registry/theme/component output.
- Block if templates do not map to CanvasSpec schema.
- Block if visual assets do not have golden fixtures.
- Block if generated pages degrade to title-plus-bullets.
### Gate 10: Prompt And Planning Layer
Purpose: turn a user topic into structured data without letting LLM write arbitrary HTML/CSS/SVG.
Required actions:
- Add prompt contracts for:
- Deck Planner
- Slide Planner
- Canvas Planner
- Repair Planner
- Each prompt must declare:
- input bundle
- output schema
- output path
- validation command
- forbidden outputs
- LLM output must be structured JSON, not free HTML/CSS/SVG.
- Planner outputs must pass schema and template fit before Satori.
Reviewer blocking checks:
- Block if prompts produce unrestricted HTML/CSS/SVG.
- Block if planner output bypasses CanvasSpec validation.
- Block if repair prompt rewrites the whole deck instead of scoped JSON Patch where PLAN requires patch behavior.
### Gate 11: Packaging And Distribution Decision
Purpose: settle how Satori/resvg enters CLI distribution.
Required actions:
- Decide whether `artboard_renderer` stays as a skill subpackage, is embedded, or is packaged separately.
- Decide how native `@resvg/resvg-js` dependency is installed for CLI users.
- Decide whether `skills_embed.go` changes are required.
- Add CI or local install validation for macOS arm64/x64.
- Document fallback when Node dependencies are missing.
Reviewer blocking checks:
- Block if the CLI release story requires users to manually clone Satori source.
- Block if native dependency install is undocumented.
- Block if package output cannot reproduce `render.mjs --check-runtime`.
### Gate 12a: Instruction / Plan / Output Adherence
Purpose: ensure explicit user instructions, planner outputs, generated CanvasSpec, and final readback remain consistent before final acceptance.
Required actions:
- Add or verify a durable user instruction source, preferably `00-input/instruction.json`, with at least topic, target slide count, language, audience, must-include, must-avoid, and output requirements.
- Add or verify an adherence checker such as `svglide_instruction_adherence.py`.
- Check that `target_slide_count` is consistent across instruction, `deck-plan.json`, `slide-plan.json`, final `slide_plan.json`, and readback.
- Check that slide count, page order, title/key-message propagation, chosen template/theme, language, required sections, and forbidden items match the instruction and planner chain.
- On failure, require scoped repair output rather than full regeneration by default:
- append missing deck/slide/canvas entries when pages are missing
- patch only affected slide fields or CanvasSpec leaves when localized
- rerun from the smallest affected stage
- allow full replan only when the deck narrative itself is invalid
- Write `06-check/instruction-adherence.json` and `receipts/instruction-adherence.json`.
- Include tests for count mismatch, missing slide-plan/canvas pages, forbidden content, and a passing fixture.
Reviewer blocking checks:
- Block if user instructions are not recorded in a durable project artifact.
- Block if `target_slide_count` can drift from actual planner/readback page count.
- Block if missing pages trigger full regeneration instead of scoped append/patch when localized repair is possible.
- Block if `quality_gate` or final acceptance can pass without instruction adherence evidence.
- Block if final readback does not check page count, order, title/key text, and explicit must-include/must-avoid constraints where applicable.
### Gate 12b: Final Full-Plan Acceptance
Purpose: certify that the whole plan, not only P0, is complete.
Required evidence:
```text
P0a/P0b/P0c passed with receipts
legacy direct_svg regression passed
chart marker readback passed
image asset readback passed
unsupported feature fail-fast passed
template/theme/component scale targets met or PLAN.md explicitly revised
prompt contracts implemented and validated
packaging/distribution decision implemented or explicitly scoped with owner/date
instruction/plan/output adherence receipt passed
all unit/integration/regression tests passed
PLAN.md completion table updated
reviewer subagent final PASS recorded
```
Reviewer final blocking checks:
- Block if any `PLAN.md` P0 success standard is incomplete.
- Block if P1/P2 scope was silently dropped without updating `PLAN.md`.
- Block if live/readback evidence is missing.
- Block if instruction/plan/output adherence evidence is missing or stale.
- Block if final claim depends on fake local dry-run only.
## 7. Status Board
Update this board after each meaningful implementation chunk.
| Gate | Status | Owner | Reviewer verdict | Evidence |
|---:|---|---|---|---|
| 0 Baseline and branch discipline | DONE | executor | PASS | Branch/worktree verified; legacy `direct_svg` baseline ran to `quality_gate` at `/private/tmp/svglide-direct-gate0-9Wl2gp` with `generation_mode=direct_svg` and no `artboard_receipts`; evidence recorded in `svglide-artboard-gate0-gate1-evidence.md` |
| 1 Contract layer completion | DONE | executor | PASS | Plan schema now rejects `artboard_satori` slides without `canvas_spec`; semantic map now emits `elements[]`; PLAN/contract receipt wording aligned to per-page `artboard_receipts` + aggregate `artboard_additional_receipts`; reviewer PASS recorded in `svglide-artboard-gate0-gate1-evidence.md` |
| 2 Template/theme/component/input quality | DONE | executor | PASS | 3 templates + 3 registered themes + component module + `templates/p0-templates.mjs` exist; registry text budgets, golden CanvasSpec fixtures, and safe-area/semantic bbox admission checks added; P0b `/private/tmp/svglide-p0b-gate2-safe-YVT67C` passed template-fit/quality-gate/dry-run; evidence recorded in `svglide-artboard-gate2-evidence.md` |
| 3 Satori renderer and resvg preview | DONE | executor | PASS | `node render.mjs --check-runtime` and `node dist/render.mjs --check-runtime` passed with Satori 0.26.0 / resvg 2.6.2; P0b raw SVG/PNG/contact sheet and receipts verified; evidence recorded in `svglide-artboard-gate3-evidence.md` |
| 4 SatoriToSVGlide compiler | DONE | executor | PASS | Main artboard path now writes `04-svg/artboard/page-###.canvas-template.svg` and compiles final SVGlide SVG from `CanvasSpecTemplateSVG`; raw Satori SVG is `preview_only`; quality gate rejects RawSatori compiler metadata; P0b `/private/tmp/svglide-p0b-gate4-641DXp` passed quality_gate/dry_run; evidence recorded in `svglide-artboard-gate4-evidence.md` |
| 5 Runner and quality gate integration | DONE | executor | PASS | Page jobs now run with bounded `max_workers=min(4,page_count)` and stable sorted receipts; full test suite passed 254 tests; direct_svg `/private/tmp/svglide-direct-gate5-iYPBBA` passed quality_gate; artboard P0b `/private/tmp/svglide-p0b-gate5-qg7PC6` passed dry_run; evidence recorded in `svglide-artboard-gate5-evidence.md` |
| 6 P0a/P0b local E2E | DONE | executor | PASS | P0a `/private/tmp/svglide-p0a-gate6-zNSbw5` ran to dry_run; P0b `/private/tmp/svglide-p0b-gate5-qg7PC6` ran to dry_run; P0b hits `cover-hero/dark-clarity`, `comparison-cards/forest-signal`, `summary-final/warm-editorial`; evidence recorded in `svglide-artboard-gate6-evidence.md` |
| 7 P0c live closure | DONE | executor | PASS | Reviewer PASS: strengthened PPE proof validates Whistle capture/proxy/rule hash/injected headers; fresh P0c `.tmp/svglide-p0c-gate7-live6` ran `dry_run -> ppe_proof -> live_create -> readback`; live deck `MPcnsjAH5l5r2edcpWYcNhFVnVd` created 3 slides `["pbb","pbu","pbe"]`; readback passed page count, slide order, nonblank, text-fit/bounds marker scan, and 22 CanvasSpec visible text fragments; evidence recorded in `svglide-artboard-gate7-evidence.md` |
| 8 Special cases and fallback coverage | DONE | executor | PASS | Reviewer PASS: local Gate 8 evidence `.tmp/svglide-gate8-special-cases-r4/gate8-special-cases.json` passed all 4 cases; image-only and raster-only live/readback pass; previous chart-only and combined readback failures were traced to `creation.slide.nodeserver_pre_release::GetSXSDXml` on stale lane package `ee/slide/server@1.0.0.1149`; slide-side fix is committed and pushed at `8f682ab082f7d86ade966eb2ffc5849827b17dc5`; focused Jest passes 6/6 and strict single-file TypeScript passes; deploy-lane ticket `2068537756495360000` succeeded, TCE deployment `362509781` finished, service `208677037` is running `ee/slide/server@1.0.0.1184`; fresh chart-only readback `.tmp/svglide-gate8-live-chart/08-readback/readback-check.json` passed for deck `C5fxszdjrlftMedvShmcOWtinqe` / slide `pvv`; fresh combined readback `.tmp/svglide-gate8-live/08-readback/readback-check.json` passed for deck `J35tspvJgltBnsdJpL7chnv6n2f` / slides `pdd,pdu,pdR` with chart marker and 2 image assets verified; evidence recorded in `svglide-artboard-gate8-evidence.md`; current P0/P1 milestone is closed by Gate 12b reviewer PASS |
| 9 P1 asset system scale-out | DONE | executor | PASS | Source intake inventory, 15 active templates, 10 themes, 23 component variants, 10 layout archetypes, 15 golden CanvasSpec fixtures, Node/Satori renderer support, Python fallback, and tests are implemented; source intake has required fields plus per-source conversion traceability; `ppt-master` provenance records MIT and is test-guarded; `node --check`, Python compile, JSON parse, renderer source/dist runtime checks, `pnpm --dir ... run build`, focused artboard tests 15/15, scripts regression 265/265, and `git diff --check` all pass; reviewer PASS recorded in `svglide-artboard-gate9-evidence.md` |
| 10 Prompt and planning layer | DONE | executor | PASS | Prompt contracts for Deck/Slide/Canvas/Repair planners, four output schemas, `svglide_planner_contracts.py`, Gate 10 fixture, and tests are implemented; planner contract check validates JSON-only outputs, schema conformance, Slide Planner registry binding, CanvasSpec + registry binding before Satori, and scoped leaf-level repair JSON Patch; focused planner tests 5/5, scripts regression 270/270, renderer source/dist runtime checks, artboard continuation proof, and `git diff --check` pass; reviewer PASS recorded in `svglide-artboard-gate10-evidence.md` |
| 11 Packaging and distribution decision | DONE | executor | PASS | Reviewer PASS: `artboard_renderer` stays as a skill-local Node subpackage; Satori is bundled into `dist/render.mjs`; `@resvg/resvg-js` remains a pinned native runtime dependency; `skills_embed.go` embeds prompts, flat Python scripts, and renderer package resources with a whitelist excluding `node_modules`; package check `status=passed`, source/dist runtime checks pass, package-check tests 4/4 pass, `go test .` passes, embedded skills listing includes renderer and planner prompt resources; reviewer noted macOS x64 is structurally validated through lockfile optional native package coverage, not executed on x64 host; evidence recorded in `svglide-artboard-gate11-evidence.md` |
| 12a Instruction / plan / output adherence | DONE | executor | PASS | Reviewer PASS: durable `.tmp/svglide-p0c-gate7-live6/00-input/instruction.json` and `svglide_instruction_adherence.py` validate instruction -> deck-plan -> slide-plan -> final `slide_plan` -> SVG output -> readback for target slide count, actual `slides[]` count, page order, exact title/key_message, template/theme, language, must_include/must_avoid, explicit constraint surfaces, and readback hash bindings; scoped leaf repair aligned final `slide_plan` key_message fields, then readback was refreshed with current branch CLI and matched the new plan hash; instruction adherence receipt `status=passed`; focused tests 9/9 pass; evidence recorded in `svglide-artboard-gate12a-evidence.md` |
| 12b Final full-plan acceptance | DONE | executor | PASS | Reviewer PASS: final acceptance checker requires Gate 0-12a DONE/PASS, Gate 12a instruction-adherence check/receipt status passed, current instruction/deck-plan/slide-plan/final-plan/output/readback hashes matched, readback binding checks matched, package receipt passed, PLAN scope caveat present, and Gate 12 scope deferrals with owner/date; final acceptance receipt `status=passed`; package check passed; focused final tests 3/3, scripts regression 286/286, `go test .`, and `git diff --check` pass; evidence recorded in `svglide-artboard-gate12-evidence.md` |
Status values:
```text
TODO
IN_PROGRESS
PARTIAL
BLOCKED
DONE
```
Only a reviewer subagent can move `Reviewer verdict` to `PASS`.
## 7.1 Current Next Task
Gate 12b now has reviewer PASS:
```text
Completed Gate 12a outcome:
- Durable instruction capture exists at 00-input/instruction.json in the live/readback project.
- Instruction / plan / output adherence receipt is passed.
- Target slide count, actual slides[] count, page order, exact key_message, language, must_include/must_avoid, explicit constraints, and readback hash bindings are checked.
- Scoped repair was used for final slide_plan key_message drift; full regeneration was not used.
- Reviewer verdict: PASS.
Next required action:
- Stop here unless the user starts a new follow-up scope.
- Do not claim P2/future scope as complete.
```
Gate 8 closure evidence:
```text
1. Service fix deployed:
ENV ticket `2068537756495360000`, TCE deployment `362509781`,
service `208677037`, `ee/slide/server@1.0.0.1184`.
2. Chart-only readback passed:
`.tmp/svglide-gate8-live-chart/08-readback/readback-check.json`.
3. Combined chart + image + raster readback passed:
`.tmp/svglide-gate8-live/08-readback/readback-check.json`.
4. Gate 8 evidence file updated:
`skills/lark-slides/references/svglide-artboard-gate8-evidence.md`.
```
## 8. Subagent Review Prompt
Use this prompt when creating a reviewer subagent:
```text
You are the independent reviewer for SVGlide Artboard/Satori full-plan execution.
Primary source of truth:
/Users/bytedance/Downloads/PLAN.md
Supervision guide:
skills/lark-slides/references/svglide-artboard-full-plan-action.md
Your job is to challenge the executor and prevent partial completion from being called done.
You must inspect actual repo files, changed files, runner outputs, receipts,
schemas, fixtures, and test results. Do not accept executor prose as evidence.
Do not accept demos, screenshots, fake dry-runs, stale receipts, missing hashes,
or direct Satori SVG as completion evidence.
Current execution cursor:
Gate 12b Final Full-Plan Acceptance is DONE/PASS for the current P0/P1 milestone.
P2/future scope remains explicitly not claimed unless PLAN.md and this supervision guide record a new reviewer-approved follow-up completion.
For every review, answer these checks:
1. Is the executor working on the current allowed gate or blocker?
2. Are the claimed changed files in the correct repo/worktree?
3. Are receipts fresh and hash-bound to current inputs?
4. Did the executor run the required validation commands for this gate?
5. Does evidence include real runner/live/readback output where required?
6. Did legacy direct_svg remain compatible where the gate can affect it?
7. Did the executor update this supervision guide and gate evidence file?
8. Is any PLAN.md requirement silently dropped, weakened, or moved later?
Return:
Verdict: PASS / BLOCKED
Blocking issues:
- ...
Non-blocking risks:
- ...
Evidence checked:
- ...
Next required action:
- ...
```
## 9. Minimum Validation Command Set
Run these when relevant to the current gate:
```bash
git status --short --branch
```
```bash
node skills/lark-slides/scripts/artboard_renderer/render.mjs --check-runtime
```
```bash
node skills/lark-slides/scripts/artboard_renderer/dist/render.mjs --check-runtime
```
```bash
python3 -m unittest discover skills/lark-slides/scripts -p '*_test.py'
```
For fixture E2E, use the runner with an explicit fixture path and stage target. Do not count ad hoc `.tmp` projects as the only evidence for a gate.
## 10. Completion Rule
The full plan is not complete until all of the following are true:
```text
all gates 0-12 are DONE
all reviewer verdicts are PASS
PLAN.md completion table is updated
real live_create/readback evidence exists for P0c
legacy direct_svg remains compatible
artboard_satori path has reproducible fixtures and tests
packaging/distribution path is explicit enough for CLI users
```
If any of these are false, the correct status is still `IN_PROGRESS` or `BLOCKED`, not complete.

View File

@@ -0,0 +1,164 @@
# SVGlide Artboard/Satori Gate 0-1 Evidence
Last updated: 2026-06-21
This file records the reviewer-checked evidence for Gate 0 and Gate 1 in `svglide-artboard-full-plan-action.md`.
## Gate 0: Baseline And Branch Discipline
Reviewer verdict: PASS
Direct SVG baseline project:
```text
/private/tmp/svglide-direct-gate0-9Wl2gp
```
Validation command:
```bash
python3 skills/lark-slides/scripts/svglide_project_runner.py run \
/private/tmp/svglide-direct-gate0-9Wl2gp \
--until quality_gate \
--network-policy fixture \
--asset-provider none \
--image-backend none
```
Checked evidence:
```text
receipts/generate_svg.json:
status = passed
generator_mode = script
generation_mode = direct_svg
artboard_receipts absent
06-check/preflight.json:
summary.error_count = 0
06-check/runtime-review.json:
status = passed
summary.error_count = 0
06-check/quality-gate.json:
status = passed
failed_check_count = 0
```
Interpretation:
```text
The legacy direct_svg path still reaches quality_gate and does not depend on artboard_satori receipts.
```
## Gate 1: Contract Layer Completion
Reviewer verdict: PASS
Validated contract changes:
```text
skills/lark-slides/scripts/svglide_schema.py
supports allOf / if / then
skills/lark-slides/references/svglide-plan.schema.json
generation_mode=artboard_satori requires slides[].canvas_spec
skills/lark-slides/references/svglide-semantic-map.schema.json
requires semantic_source and element-level elements[]
skills/lark-slides/scripts/svglide_artboard_renderer.py
writes semantic_map.elements[] from CanvasSpec/template layout nodes
/Users/bytedance/Downloads/PLAN.md
skills/lark-slides/references/svglide-artboard-satori.contract.md
skills/lark-slides/references/svglide-generator-receipt.schema.json
aligned on per-page artboard_receipts plus aggregate artboard_additional_receipts
```
Schema negative probe:
```text
Input: P0b artboard_satori slide_plan with all slides[].canvas_spec removed.
Result:
issue_count = 3
$.slides[0].canvas_spec required property is missing
$.slides[1].canvas_spec required property is missing
$.slides[2].canvas_spec required property is missing
```
P0b evidence project:
```text
/private/tmp/svglide-p0b-gate1-u1w9i4
```
P0b validation command:
```bash
SVGLIDE_LARK_CLI_CMD="python3 /private/tmp/svglide_fake_lark_cli.py" \
python3 skills/lark-slides/scripts/svglide_project_runner.py run \
/private/tmp/svglide-p0b-gate1-u1w9i4 \
--until dry_run \
--network-policy fixture \
--asset-provider none \
--image-backend none
```
Checked evidence:
```text
receipts/generate_svg.json:
status = passed
generator_mode = script
generation_mode = artboard_satori
artboard_receipts length = 3
artboard_additional_receipts length = 3
06-check/quality-gate.json:
status = passed
07-create/dry-run.json:
status = passed
04-svg/artboard/page-001.semantic-map.json:
semantic_source = CanvasSpec
elements length = 9
04-svg/artboard/page-002.semantic-map.json:
semantic_source = CanvasSpec
elements length = 13
04-svg/artboard/page-003.semantic-map.json:
semantic_source = CanvasSpec
elements length = 16
```
Test command:
```bash
PYTHONDONTWRITEBYTECODE=1 python3 -m unittest discover skills/lark-slides/scripts -p '*_test.py'
```
Result:
```text
247 tests passed
```
## Reviewer Result
Reviewer output summary:
```text
Verdict: PASS
Blocking issues:
- None. Previous Gate 0/1 blocking issues are resolved.
Non-blocking risks:
- Worktree is still dirty and must be organized before merge.
- Some runtime evidence is still in /private/tmp; future gates should prefer stable fixtures or evidence docs.
- This PASS only covers Gate 0/1, not P0c live/readback or Gate 2+.
```

View File

@@ -0,0 +1,290 @@
# SVGlide Artboard Gate 10 Evidence
Gate: `Gate 10: Prompt And Planning Layer`
Status: `PASS`
Date: 2026-06-21
Reviewer history:
```text
Initial review: BLOCKED
Blockers:
1. Slide Planner output did not validate template_id/theme_id against Template/Theme Registry.
2. Repair Planner allowed broad object patch paths such as /slides/0/canvas_spec/content.
Fixes:
1. svglide_planner_contracts.py now validates Slide Planner template/theme IDs against active registries and template supported_theme_ids.
2. Repair Planner validation now rejects broad paths for whole content/theme/semantic_elements/quality_constraints/content_requirements objects.
3. Repair Planner validation rejects object/list patch values for add/replace; patches must target scalar leaf values.
4. Focused tests now include negative coverage for unregistered slide-plan template and broad repair object patch.
Current state: re-review requested after fresh validation.
Final reviewer verdict: PASS.
```
## Scope
Gate 10 turns a user topic into structured planner JSON without letting the LLM output arbitrary HTML/CSS/SVG.
Implemented planner chain:
```text
Deck Planner
-> Slide Planner
-> Canvas Planner
-> Repair Planner
```
The planner chain is validated before Satori is invoked:
```text
prompt contracts
-> planner output schemas
-> forbidden free markup scan
-> CanvasSpec validation
-> Template Registry / Theme Registry binding
-> template text_budget / max_items admission
-> scoped JSON Patch repair validation
```
## Prompt Contracts
Contract registry:
```text
skills/lark-slides/references/svglide-planner-prompt-contracts.json
```
Prompt files:
```text
skills/lark-slides/prompts/svglide/deck-planner.prompt.md
skills/lark-slides/prompts/svglide/slide-planner.prompt.md
skills/lark-slides/prompts/svglide/canvas-planner.prompt.md
skills/lark-slides/prompts/svglide/repair-planner.prompt.md
```
Every prompt declares:
```text
input_bundle
output_schema
output_path
validation_command
forbidden_outputs
```
All prompts require JSON-only output and forbid free HTML, free CSS, free SVG, JSX/TSX, Markdown fences, raw Satori SVG, and base64 image data.
## Output Schemas
Schemas:
```text
skills/lark-slides/references/svglide-deck-plan.schema.json
skills/lark-slides/references/svglide-slide-plan.schema.json
skills/lark-slides/references/svglide-canvas-plan.schema.json
skills/lark-slides/references/svglide-repair-plan.schema.json
```
Schema roles:
```text
svglide-deck-plan.schema.json: narrative and deck-level intent only
svglide-slide-plan.schema.json: registered template_id/theme_id selection
svglide-canvas-plan.schema.json: final artboard_satori slide_plan.json with CanvasSpec
svglide-repair-plan.schema.json: scoped JSON Patch operations only
```
## Validation Script
Implemented:
```text
skills/lark-slides/scripts/svglide_planner_contracts.py
```
The script validates:
```text
1. Required four prompt contracts exist.
2. Prompt files exist and declare schema/path/validation command.
3. Prompt contracts include forbidden outputs.
4. Planner outputs exist at contract-declared paths.
5. Planner outputs pass their schemas.
6. Planner outputs do not contain free HTML/CSS/SVG markup patterns.
7. Canvas Planner output also passes existing svglide-plan.schema.json.
8. Every CanvasSpec passes CanvasSpec validation.
9. Every CanvasSpec passes Template Registry / Theme Registry binding.
10. Template required_content, max_items, and text_budget are enforced before Satori.
11. Repair Planner output cannot contain full deck/slides/canvas_spec rewrites.
12. Repair patch paths must be scoped to slides/style_system/art_direction fields.
13. Slide Planner template_id and theme_id must be active registry entries.
14. Slide Planner theme_id must be allowed by the selected template.
15. Repair patch paths must target leaf fields, not whole CanvasSpec content/theme/semantic objects.
```
Output receipts:
```text
06-check/planner-contract-check.json
receipts/planner-contract-check.json
```
## Fixture
Gate 10 fixture:
```text
skills/lark-slides/scripts/fixtures/svglide_artboard/gate10_planner/
```
Planner outputs:
```text
02-plan/deck-plan.json
02-plan/slide-plan.json
02-plan/slide_plan.json
02-plan/repair-plan.json
```
Topic:
```text
冰岛火山研究
```
The fixture proves:
```text
Deck Planner output stays narrative-only.
Slide Planner chooses registered templates/themes.
Canvas Planner emits full artboard_satori slide_plan.json with CanvasSpec.
Repair Planner emits one scoped JSON Patch, not a full-deck rewrite.
```
## Validation Commands
JSON parse:
```bash
python3 -c 'import json, pathlib; paths=list(pathlib.Path("skills/lark-slides/references").glob("svglide-*.json"))+list(pathlib.Path("skills/lark-slides/scripts/fixtures/svglide_artboard/gate10_planner/02-plan").glob("*.json")); [json.load(p.open(encoding="utf-8")) for p in paths]; print(len(paths))'
```
Result:
```text
37 JSON files parsed
```
Python compile:
```bash
env PYTHONPYCACHEPREFIX=/private/tmp/svglide-pycache \
python3 -m py_compile \
skills/lark-slides/scripts/svglide_planner_contracts.py \
skills/lark-slides/scripts/svglide_planner_contracts_test.py
```
Result:
```text
PASS
```
Planner contract fixture:
```bash
env PYTHONPYCACHEPREFIX=/private/tmp/svglide-pycache \
python3 skills/lark-slides/scripts/svglide_planner_contracts.py \
skills/lark-slides/scripts/fixtures/svglide_artboard/gate10_planner \
--pretty
```
Result:
```text
status: passed
prompt_contract_count: 4
planner_output_count: 4
error_count: 0
```
Artboard continuation proof:
```bash
env PYTHONPYCACHEPREFIX=/private/tmp/svglide-pycache \
python3 skills/lark-slides/scripts/svglide_artboard_renderer.py \
skills/lark-slides/scripts/fixtures/svglide_artboard/gate10_planner \
--pretty
```
Result:
```text
status: passed
page_count: 3
max_workers: 3
contact_sheet: 05-preview/contact-sheet.png
```
Focused tests:
```bash
env PYTHONPYCACHEPREFIX=/private/tmp/svglide-pycache \
python3 -m unittest skills/lark-slides/scripts/svglide_planner_contracts_test.py
```
Result:
```text
Ran 5 tests in 0.204s
OK
```
Full scripts regression:
```bash
env PYTHONPYCACHEPREFIX=/private/tmp/svglide-pycache \
python3 -m unittest discover skills/lark-slides/scripts -p '*_test.py'
```
Result:
```text
Ran 270 tests in 15.565s
OK
```
Renderer runtime checks:
```bash
node skills/lark-slides/scripts/artboard_renderer/render.mjs --check-runtime
node skills/lark-slides/scripts/artboard_renderer/dist/render.mjs --check-runtime
```
Result:
```text
PASS
satori_version: 0.26.0
resvg_version: 2.6.2
```
Diff check:
```bash
git diff --check
```
Result:
```text
PASS
```
## Boundary Note
`svglide_template_fit_check.py` is a runner post-generate check that requires `receipts/generate_svg.json`. Gate 10 does not use it directly as the planner gate. Instead, `svglide_planner_contracts.py` performs pre-Satori planner admission by calling CanvasSpec validation and Template/Theme Registry binding, including template required content, `max_items`, and `text_budget`.
The artboard renderer command above is included only to prove that the validated Canvas Planner output can continue into Satori/resvg; it is not a substitute for planner contract validation.

View File

@@ -0,0 +1,240 @@
# SVGlide Artboard Gate 11 Evidence
Status: implemented, pending reviewer verdict
Gate: Packaging and distribution decision
## Scope
Gate 11 closes the distribution question for the Artboard/Satori lane:
- whether `artboard_renderer` stays as a skill subpackage, is embedded, or is packaged separately
- how `@resvg/resvg-js` native dependency is installed
- whether `skills_embed.go` needs changes
- how to validate macOS arm64/x64 install/runtime readiness
- what happens when Node dependencies are missing
## Decision
`artboard_renderer` remains a `lark-slides` skill-local Node subpackage. It is not moved into the root Go CLI package and does not require users to clone Satori source.
Satori is bundled into `dist/render.mjs`. `@resvg/resvg-js` remains a package-managed native runtime dependency pinned in the subpackage lockfile.
`skills_embed.go` is changed to embed a whitelist of prompts, flat Python scripts, and artboard renderer package resources, while excluding `node_modules`, fixtures, and generated project artifacts.
Full decision document:
```text
skills/lark-slides/references/svglide-artboard-packaging-decision.md
```
## Files Changed For Gate 11
```text
skills_embed.go
cmd/skill/skill.go
skills/lark-slides/scripts/artboard_renderer/package.json
skills/lark-slides/scripts/artboard_renderer/render.mjs
skills/lark-slides/scripts/artboard_renderer/dist/render.mjs
skills/lark-slides/scripts/artboard_renderer/templates/README.md
skills/lark-slides/references/svglide-artboard-satori.contract.md
skills/lark-slides/references/svglide-artboard-packaging-decision.md
skills/lark-slides/scripts/svglide_artboard_package_check.py
skills/lark-slides/scripts/svglide_artboard_package_check_test.py
skills/lark-slides/scripts/fixtures/svglide_artboard/gate11_package/06-check/artboard-package-check.json
skills/lark-slides/scripts/fixtures/svglide_artboard/gate11_package/receipts/artboard-package-check.json
```
## Package Check Result
Command:
```bash
env PYTHONPYCACHEPREFIX=/private/tmp/svglide-pycache \
python3 skills/lark-slides/scripts/svglide_artboard_package_check.py \
--output-dir skills/lark-slides/scripts/fixtures/svglide_artboard/gate11_package \
--pretty
```
Result:
```text
status: passed
issues: []
host: Darwin arm64
satori: 0.26.0
@resvg/resvg-js: 2.6.2
manual_satori_source_checkout_required: false
node_modules_embedded_in_go_binary: false
runtime_requires_native_resvg_install: true
```
Receipt paths:
```text
skills/lark-slides/scripts/fixtures/svglide_artboard/gate11_package/06-check/artboard-package-check.json
skills/lark-slides/scripts/fixtures/svglide_artboard/gate11_package/receipts/artboard-package-check.json
```
## Runtime Checks
Source entry:
```bash
node skills/lark-slides/scripts/artboard_renderer/render.mjs --check-runtime
```
Result:
```text
ok: true
renderer: satori-resvg
satori_version: 0.26.0
resvg_version: 2.6.2
font_path: /System/Library/Fonts/Supplemental/Arial Unicode.ttf
```
Published dist entry:
```bash
node skills/lark-slides/scripts/artboard_renderer/dist/render.mjs --check-runtime
```
Result:
```text
ok: true
renderer: satori-resvg
satori_version: 0.26.0
resvg_version: 2.6.2
font_path: /System/Library/Fonts/Supplemental/Arial Unicode.ttf
```
## Build Check
Command:
```bash
pnpm --dir skills/lark-slides/scripts/artboard_renderer run build
```
Result:
```text
passed
dist/render.mjs rebuilt
```
`dist/render.mjs` no longer requires `satori/package.json`. It uses version constants and dynamically loads `@resvg/resvg-js`, so missing native resvg can be caught with a clear install instruction.
## Go Embed Check
Command:
```bash
go test .
```
Result:
```text
passed
```
The first sandboxed run was blocked by Go build cache writes under `~/Library/Caches/go-build`; the verified run used approved escalated execution for the cache write.
Embedded artboard renderer listing:
```bash
env GOCACHE=/private/tmp/svglide-gocache \
go run . skills list lark-slides/scripts/artboard_renderer
```
Result includes:
```text
components/
dist/
package.json
pnpm-lock.yaml
render.mjs
templates/
themes/
```
Embedded prompt listing:
```bash
env GOCACHE=/private/tmp/svglide-gocache \
go run . skills list lark-slides/prompts/svglide
```
Result includes:
```text
canvas-planner.prompt.md
deck-planner.prompt.md
repair-planner.prompt.md
slide-planner.prompt.md
```
## Unit Tests
Command:
```bash
env PYTHONPYCACHEPREFIX=/private/tmp/svglide-pycache \
python3 -m unittest skills/lark-slides/scripts/svglide_artboard_package_check_test.py
```
Result:
```text
4 tests passed
```
Full scripts regression:
```bash
env PYTHONPYCACHEPREFIX=/private/tmp/svglide-pycache \
python3 -m unittest discover skills/lark-slides/scripts -p '*_test.py'
```
Result:
```text
274 tests passed
```
Whitespace check:
```bash
git diff --check
```
Result:
```text
passed
```
## Fallback Policy
If Node, Satori source dependencies, or native resvg dependencies are missing, generation must fail before live create.
Repair command:
```bash
pnpm --dir skills/lark-slides/scripts/artboard_renderer install --frozen-lockfile
node skills/lark-slides/scripts/artboard_renderer/dist/render.mjs --check-runtime
```
No-network environments must use a prewarmed pnpm store, CI-installed dependencies, or a packaged platform dependency layer. Generation must not auto-fetch packages while producing a deck.
## Reviewer Checklist
- Confirm no manual Satori source checkout is required.
- Confirm native `@resvg/resvg-js` install is documented and pinned.
- Confirm package output reproduces `render.mjs --check-runtime` for source and dist.
- Confirm `skills_embed.go` includes the required skill resources but not `node_modules`.
- Confirm `slides +create-svg` boundary remains final-SVG only.

View File

@@ -0,0 +1,216 @@
# SVGlide Artboard Gate 12b Evidence
Status: DONE / reviewer PASS
Gate: Final Full-Plan Acceptance
Reviewer verdict:
```text
Verdict: PASS
Blocking issues: none
Non-blocking risk: Gate 12b PASS only covers the current P0/P1 implemented milestone through Gate 12a reviewer PASS.
```
## Scope
Gate 12b accepts the current implemented milestone:
```text
P0 technical vertical slice
P0 live/readback closure
Gate 8 special cases
P1 asset scale-out
P1 planner prompt contracts
P1 packaging/distribution decision
Gate 12a instruction / plan / output / readback adherence
```
It does not claim the complete high-quality PPT generation system with actual model-driven topic-to-deck automation. That follow-up scope is explicit in:
```text
skills/lark-slides/references/svglide-artboard-gate12-scope.md
```
## Gate Status
The supervision board records all prerequisite gates as DONE/PASS:
```text
Gate 0: DONE/PASS
Gate 1: DONE/PASS
Gate 2: DONE/PASS
Gate 3: DONE/PASS
Gate 4: DONE/PASS
Gate 5: DONE/PASS
Gate 6: DONE/PASS
Gate 7: DONE/PASS
Gate 8: DONE/PASS
Gate 9: DONE/PASS
Gate 10: DONE/PASS
Gate 11: DONE/PASS
Gate 12a: DONE/PASS
```
Source:
```text
skills/lark-slides/references/svglide-artboard-full-plan-action.md
```
## Final Acceptance Check
Command:
```bash
env PYTHONPYCACHEPREFIX=/private/tmp/svglide-pycache \
python3 skills/lark-slides/scripts/svglide_artboard_final_acceptance.py \
--output-dir skills/lark-slides/scripts/fixtures/svglide_artboard/gate12_final \
--pretty
```
Result:
```text
status: passed
issues: []
accepted_milestone: P0/P1 artboard_satori implementation through Gate 12a reviewer PASS
not_claimed: complete high-quality PPT generation system with actual model-driven topic-to-deck loop
```
Receipt paths:
```text
skills/lark-slides/scripts/fixtures/svglide_artboard/gate12_final/06-check/final-acceptance-check.json
skills/lark-slides/scripts/fixtures/svglide_artboard/gate12_final/receipts/final-acceptance-check.json
```
## Gate 12a Binding
Final acceptance now verifies Gate 12a explicitly:
```text
.tmp/svglide-p0c-gate7-live6/06-check/instruction-adherence.json
.tmp/svglide-p0c-gate7-live6/receipts/instruction-adherence.json
status: passed
issues: []
```
Freshness checks:
```text
instruction hash: matched
deck-plan hash: matched
slide-plan hash: matched
final slide_plan hash: matched
output SVG page hashes: matched
readback-check hash: matched
xml-presentations-get raw readback hash: matched
```
Readback binding checks:
```text
plan_sha256: matched
quality_gate_sha256: matched
dry_run_sha256: matched
ppe_proof_sha256: matched
live_create_sha256: matched
```
## Package Receipt
Command:
```bash
env PYTHONPYCACHEPREFIX=/private/tmp/svglide-pycache \
python3 skills/lark-slides/scripts/svglide_artboard_package_check.py \
--output-dir skills/lark-slides/scripts/fixtures/svglide_artboard/gate11_package \
--pretty
```
Result:
```text
status: passed
source runtime check: passed
dist runtime check: passed
Satori: 0.26.0
resvg: 2.6.2
node_modules embedded in Go binary: false
```
## Follow-Up Scope
Explicit follow-up items are recorded in `svglide-artboard-gate12-scope.md`:
```text
1. Real Topic Model Loop
2. Semantic Map Compiler IR
3. True Node Layout Observation
4. Real macOS x64 Runtime Validation
```
Each item has an owner, target date, and a "Not claimed" declaration.
## Validation Commands
Focused final acceptance tests:
```bash
env PYTHONPYCACHEPREFIX=/private/tmp/svglide-pycache \
python3 -m unittest skills/lark-slides/scripts/svglide_artboard_final_acceptance_test.py
```
Result:
```text
3 tests passed
```
Full scripts regression:
```bash
env PYTHONPYCACHEPREFIX=/private/tmp/svglide-pycache \
python3 -m unittest discover skills/lark-slides/scripts -p '*_test.py'
```
Result:
```text
286 tests passed
```
Go root tests:
```bash
go test .
```
Result:
```text
passed
```
Whitespace check:
```bash
git diff --check
```
Result:
```text
passed
```
## Reviewer Checklist
- Confirm Gate 0-12a are DONE/PASS in the action guide.
- Confirm final acceptance check receipt has `status=passed`.
- Confirm final acceptance cannot pass without Gate 12a reviewer PASS and current instruction-adherence receipt.
- Confirm PLAN.md does not claim complete high-quality PPT generation.
- Confirm P2/future scope is explicit with owner/date.
- Confirm live/readback evidence remains tied to Gate 7/Gate 8 and Gate 12a readback binding, not a fake local dry-run.
- Confirm Gate 12b final PASS, if granted, is scoped to this implemented milestone.

View File

@@ -0,0 +1,130 @@
# SVGlide Artboard Gate 12 Scope
Status: final acceptance scope for current implementation milestone
## Accepted Milestone
Gate 12 acceptance covers the implemented SVGlide Artboard/Satori milestone through Gate 12a:
```text
P0 technical vertical slice
P0 live/readback special cases
P1 template/theme/component scale-out
P1 planner prompt contracts
P1 packaging/distribution decision
Gate 12a instruction / plan / output / readback adherence
```
This milestone proves:
- `generation_mode=artboard_satori` can run through the existing SVGlide control plane.
- Satori output is preview/layout evidence, not the direct live SVG semantic source.
- Final live input remains SVGlide protocol SVG.
- `ppe_pure_svg` readback covers chart marker and image asset cases.
- Template/theme/component assets meet the current P1 minimum scale.
- Planner prompts produce structured JSON contracts instead of free HTML/CSS/SVG.
- `artboard_renderer` has a releaseable skill-local packaging story.
- Instruction, planner output, generated SVG, and readback evidence are bound by Gate 12a adherence receipts.
## Not Claimed
Not claimed: complete high-quality PPT generation system with actual model-driven topic-to-deck loop.
Not claimed in this milestone:
- actual model invocation from arbitrary user topic to Deck Plan / Slide Plan / CanvasSpec
- automated visual repair loop that calls a model and writes scoped JSON Patch
- full semantic-map-as-compiler-IR implementation for every element type
- true Satori `onNodeDetected` layout observation wired as `node-layout-map/v1`
- production CI coverage on a real macOS x64 host
These are not silently dropped. They are explicit follow-up scope.
## Follow-Up Items
### 1. Real Topic Model Loop
Owner: SVGlide artboard follow-up executor
Target date: 2026-06-28
Scope:
```text
user topic
-> model-generated deck-plan.json
-> model-generated slide-plan.json
-> model-generated canvas-plan.json
-> planner contract validation
-> template/theme binding
-> artboard_satori render
-> quality_gate
-> preview repair loop if needed
```
Acceptance:
- At least one real topic deck runs from model output, not handwritten fixtures.
- All planner outputs pass schema and registry binding.
- Repair prompt is used only as scoped JSON Patch.
### 2. Semantic Map Compiler IR
Owner: SVGlide artboard follow-up executor
Target date: 2026-06-28
Scope:
```text
CanvasSpec.semantic_elements
-> semantic-map/v1 elements with bbox/role/source_ref/style
-> compiler consumes semantic-map/v1 as element-level IR
-> final SVGlide text/image/chart/shape roles are generated from semantic-map
```
Acceptance:
- `semantic-map/v1` is no longer only a summary receipt.
- compiler receipts include `input_semantic_hash`.
- semantic review can compare visible text/source refs against element-level semantic map.
### 3. True Node Layout Observation
Owner: SVGlide artboard follow-up executor
Target date: 2026-06-28
Scope:
```text
Satori renderer layout observation
-> node-layout-map/v1
-> element_id alignment
-> drift check against semantic-map bbox
```
Acceptance:
- `node-layout-map/v1` records measured layout, not only static template geometry.
- quality gate blocks material layout drift before live create.
### 4. Real macOS x64 Runtime Validation
Owner: SVGlide release/CI owner
Target date: 2026-06-28
Scope:
```text
macOS x64 host
-> pnpm --dir skills/lark-slides/scripts/artboard_renderer install --frozen-lockfile
-> node dist/render.mjs --check-runtime
-> package check receipt
```
Acceptance:
- x64 host runtime check confirms `@resvg/resvg-js-darwin-x64` loads and renders PNG.
- result is attached to package evidence or CI artifact.

View File

@@ -0,0 +1,238 @@
# SVGlide Artboard Gate 12a Evidence
Status: DONE / reviewer PASS
Gate: Instruction / Plan / Output Adherence
Reviewer verdict:
```text
Verdict: PASS
Blocking issues: none
Non-blocking risk: Gate 12a PASS does not mean Gate 12b or full PLAN completion.
```
## Scope
Gate 12a verifies that a durable user instruction remains consistent through:
```text
00-input/instruction.json
-> 02-plan/deck-plan.json
-> 02-plan/slide-plan.json
-> 02-plan/slide_plan.json
-> 04-svg/page-###.svg
-> 08-readback/readback-check.json + xml-presentations-get.json
```
It does not enter Gate 12b final acceptance.
## Files Added Or Updated
```text
skills/lark-slides/scripts/svglide_instruction_adherence.py
skills/lark-slides/scripts/svglide_instruction_adherence_test.py
.tmp/svglide-p0c-gate7-live6/00-input/instruction.json
.tmp/svglide-p0c-gate7-live6/02-plan/deck-plan.json
.tmp/svglide-p0c-gate7-live6/02-plan/slide-plan.json
.tmp/svglide-p0c-gate7-live6/02-plan/slide_plan.json
.tmp/svglide-p0c-gate7-live6/08-readback/readback-check.json
.tmp/svglide-p0c-gate7-live6/08-readback/xml-presentations-get.json
.tmp/svglide-p0c-gate7-live6/06-check/instruction-adherence.json
.tmp/svglide-p0c-gate7-live6/receipts/instruction-adherence.json
```
Removed stale Gate10-only evidence:
```text
skills/lark-slides/scripts/fixtures/svglide_artboard/gate10_planner/01-project/instruction-lock.json
skills/lark-slides/scripts/fixtures/svglide_artboard/gate10_planner/06-check/instruction-adherence.json
skills/lark-slides/scripts/fixtures/svglide_artboard/gate10_planner/receipts/instruction-adherence.json
```
## Durable Instruction Capture
Project:
```text
.tmp/svglide-p0c-gate7-live6
```
Instruction artifact:
```text
.tmp/svglide-p0c-gate7-live6/00-input/instruction.json
```
Captured requirements:
```text
topic: SVGlide Artboard P0c live closure
language: zh-CN
audience: SVGlide engineers
target_slide_count: 3
must_include:
- 画板链路 P0b
- 三页 fixture 验证模板系统、质量门禁和 dry-run 编排。
- 新旧链路差异
- 核心变化是把美学标准从自由生成迁移到受控设计系统。
- 下一步进入真实 Satori
- 先稳定多页画板系统,再替换底层 renderer 并推进 live/readback。
must_avoid:
- 自由 HTML
- 自由 CSS
- 自由 SVG
- 系统截图
- 编造具体数值
output_requirements:
- generation_mode=artboard_satori
- final_format=SVGlide protocol SVG
- live_create_required=true
- readback_required=true
- page_order_locked=true
repair_policy:
- default_on_missing_page=scoped_append_page
- default_on_local_mismatch=scoped_leaf_patch
```
Explicit constraints are surface-aware:
```text
plan evidence:
- artboard_satori
- SVGlide protocol SVG
- 不输出自由 HTML/CSS/SVG
- 不使用系统截图
- readback / 页数 / 顺序 / 关键文本
output/readback evidence:
- Satori
- resvg
- readback
```
## Adherence Check
Scoped repair performed before the final check:
```text
target: .tmp/svglide-p0c-gate7-live6/02-plan/slide_plan.json
patch type: leaf-only key_message repair
patched fields:
- /slides/0/key_message
- /slides/1/key_message
- /slides/2/key_message
reason: align final slide_plan key_message with instruction, deck-plan, slide-plan, SVG output, and readback visible key text
full regeneration: not used
```
Readback was then refreshed with the current branch CLI so the readback binding hashes the scoped-repaired plan:
```bash
env SVGLIDE_LARK_CLI_CMD='go run .' PYTHONPYCACHEPREFIX=/private/tmp/svglide-pycache \
python3 skills/lark-slides/scripts/svglide_readback.py \
.tmp/svglide-p0c-gate7-live6 \
--pretty
```
Result:
```text
status: passed
input_binding.plan_sha256: 3aa06d0877a587f8b62d0c8934fcc03ec8e34823c00c725f4e16c3d44ceb3bb7
readback page_count / slide_order / core_visible_text: passed
```
Command:
```bash
env PYTHONPYCACHEPREFIX=/private/tmp/svglide-pycache \
python3 skills/lark-slides/scripts/svglide_instruction_adherence.py \
.tmp/svglide-p0c-gate7-live6 \
--pretty
```
Result:
```text
status: passed
issues: []
target_slide_count: 3
deck_plan.slide_count: 3
slide_plan.slide_count: 3
final slide_plan.slide_count: 3
output_pages: 3
readback.slide_count: 3
readback.slide_ids: pbb, pbu, pbe
must_include_missing: []
must_avoid_present: []
constraint_checks: all missing_text={}
repair_recommendations: no_repair_needed
```
Readback binding freshness:
```text
plan_sha256 matched current 02-plan/slide_plan.json
quality_gate_sha256 matched current 06-check/quality-gate.json
dry_run_sha256 matched current 07-create/dry-run.json
ppe_proof_sha256 matched current 07-create/ppe-proof.json
live_create_sha256 matched current 07-create/live-create.json
```
Receipt paths:
```text
.tmp/svglide-p0c-gate7-live6/06-check/instruction-adherence.json
.tmp/svglide-p0c-gate7-live6/receipts/instruction-adherence.json
```
## What The Checker Enforces
- `00-input/instruction.json` exists and uses `svglide-instruction/v1`.
- `target_slide_count` matches deck-plan, slide-plan, final `slide_plan`, SVG output page count, readback page count, and readback slide id count.
- Actual `slides[]` length in deck-plan, slide-plan, and final `slide_plan` matches the instruction, so a stale target field cannot hide missing pages.
- Page order matches instruction, deck-plan, slide-plan, and final `slide_plan`.
- Per-page title, exact `key_message`, required text, template id, and theme id propagate through planner outputs, final `slide_plan`, SVG output, and readback visible text.
- `zh-CN` output/readback pages contain CJK visible text.
- `must_include` is present in readback.
- `must_avoid` is absent from SVG output and readback.
- Explicit constraints are checked on their declared evidence surfaces: plan, output, and/or readback.
- Readback hash bindings match current plan, quality gate, dry-run, PPE proof, and live-create artifacts when those bindings are present.
- Repair plans must target scoped `/slides/...` leaf paths; broad deck rewrites or object/list replacement values fail.
## Focused Tests
Command:
```bash
env PYTHONPYCACHEPREFIX=/private/tmp/svglide-pycache \
python3 -m unittest skills/lark-slides/scripts/svglide_instruction_adherence_test.py
```
Result:
```text
9 tests passed
```
Covered cases:
```text
1. text matching ignores spacing.
2. broad object/list repair patch is rejected.
3. happy path writes both check and receipt.
4. deck-plan slides[] count drift fails with scoped append/delete recommendation.
5. missing final CanvasSpec page fails.
6. missing slide-plan page fails with scoped append/delete recommendation.
7. final slide_plan key_message drift fails even when readback binding is fresh.
8. forbidden text in SVG output/readback fails.
9. stale readback input_binding plan hash fails.
```
## Reviewer Checklist
- Confirm `00-input/instruction.json` is the durable instruction artifact for Gate 12a.
- Confirm the receipt hashes instruction, deck-plan, slide-plan, final `slide_plan`, SVG pages, and readback artifacts.
- Confirm readback checks page count, order, title/key text, language, must_include, and must_avoid.
- Confirm missing pages/local mismatches are mapped to scoped repair recommendations, not full regeneration.
- Confirm Gate 12a reviewer PASS is recorded and is not treated as Gate 12b final acceptance.

View File

@@ -0,0 +1,126 @@
# SVGlide Artboard/Satori Gate 2 Evidence
Last updated: 2026-06-21
Gate: Template, Theme, Component, And Input Quality System
Reviewer verdict: PASS
## Artifacts
Templates:
```text
skills/lark-slides/references/svglide-template-registry.json
skills/lark-slides/scripts/artboard_renderer/templates/p0-templates.mjs
```
Themes:
```text
skills/lark-slides/scripts/artboard_renderer/themes/registry.json
skills/lark-slides/scripts/artboard_renderer/themes/dark-clarity.json
skills/lark-slides/scripts/artboard_renderer/themes/forest-signal.json
skills/lark-slides/scripts/artboard_renderer/themes/warm-editorial.json
```
Components:
```text
skills/lark-slides/scripts/artboard_renderer/components/primitives.mjs
```
Golden CanvasSpec fixtures:
```text
skills/lark-slides/scripts/fixtures/svglide_artboard/golden/cover-hero.canvas-spec.json
skills/lark-slides/scripts/fixtures/svglide_artboard/golden/comparison-cards.canvas-spec.json
skills/lark-slides/scripts/fixtures/svglide_artboard/golden/summary-final.canvas-spec.json
```
## Checked Requirements
```text
3 active templates:
cover-hero
comparison-cards
summary-final
3 active formal themes:
dark-clarity
forest-signal
warm-editorial
Component exports:
Title
Subtitle
Chip
StatCard
ImageFrame
Input quality fail-fast:
unknown template_id
unknown theme_id
missing required content
card count overflow
text budget exceeded
semantic bbox outside safe_area
semantic bbox outside canvas
```
## Runtime Evidence
P0b evidence project:
```text
/private/tmp/svglide-p0b-gate2-safe-YVT67C
```
Validation command:
```bash
SVGLIDE_LARK_CLI_CMD="python3 /private/tmp/svglide_fake_lark_cli.py" \
python3 skills/lark-slides/scripts/svglide_project_runner.py run \
/private/tmp/svglide-p0b-gate2-safe-YVT67C \
--until dry_run \
--network-policy fixture \
--asset-provider none \
--image-backend none
```
Result:
```text
template-fit = passed
quality_gate = passed
dry_run = passed
page 1 = cover-hero / dark-clarity
page 2 = comparison-cards / forest-signal
page 3 = summary-final / warm-editorial
```
Test command:
```bash
python3 -m unittest discover skills/lark-slides/scripts -p '*_test.py'
```
Result:
```text
251 tests passed
```
## Reviewer Result
```text
Verdict: PASS
Blocking issues:
- None. The previous safe bbox / safe-area admission blocker is resolved.
Non-blocking risks:
- Worktree is still dirty.
- P0b evidence is under /private/tmp; keep this evidence doc for stable audit.
```

View File

@@ -0,0 +1,103 @@
# SVGlide Artboard/Satori Gate 3 Evidence
Last updated: 2026-06-21
Gate: Satori Renderer And resvg Preview
Reviewer verdict: PASS
## Runtime Package
```text
skills/lark-slides/scripts/artboard_renderer/package.json
skills/lark-slides/scripts/artboard_renderer/pnpm-lock.yaml
skills/lark-slides/scripts/artboard_renderer/render.mjs
skills/lark-slides/scripts/artboard_renderer/dist/render.mjs
```
Declared runtime dependencies:
```text
satori = 0.26.0
@resvg/resvg-js = 2.6.2
```
Runtime checks:
```bash
node skills/lark-slides/scripts/artboard_renderer/render.mjs --check-runtime
node skills/lark-slides/scripts/artboard_renderer/dist/render.mjs --check-runtime
```
Result:
```text
satori_version = 0.26.0
resvg_version = 2.6.2
renderer = satori-resvg
```
## P0b Evidence
Evidence project:
```text
/private/tmp/svglide-p0b-gate2-safe-YVT67C
```
Artifacts:
```text
04-svg/artboard/raw/page-001.satori.svg
04-svg/artboard/raw/page-002.satori.svg
04-svg/artboard/raw/page-003.satori.svg
04-svg/artboard/page-001.png
04-svg/artboard/page-002.png
04-svg/artboard/page-003.png
05-preview/contact-sheet.png
receipts/artboard-render.json
04-svg/artboard/page-001.receipt.json
04-svg/artboard/page-002.receipt.json
04-svg/artboard/page-003.receipt.json
```
Reviewer-checked facts:
```text
raw Satori SVGs exist and are non-empty
page PNGs exist and are non-empty
contact sheet exists
artboard-render receipt status = passed
per-page receipts include template_id/theme_id
per-page receipts include satori_version/resvg_version
per-page receipts include font_hashes
per-page receipts include Satori SVG, PNG, metadata, node-layout, and SVGlide SVG hashes
raw Satori SVG is not copied directly to final live SVG
no QuickLook/system screenshot/Playwright/Chromium/Puppeteer path is used in renderer code
```
Test command:
```bash
python3 -m unittest discover skills/lark-slides/scripts -p '*_test.py'
```
Result:
```text
251 tests passed
```
## Reviewer Result
```text
Verdict: PASS
Blocking issues:
- None.
Non-blocking risks:
- Worktree is dirty and renderer files are untracked.
- dist/render.mjs externalizes @resvg/resvg-js; packaging belongs to a later gate.
- Runtime evidence is under /private/tmp; keep this evidence doc for audit.
```

View File

@@ -0,0 +1,226 @@
# SVGlide Artboard Gate 4 Evidence
Date: 2026-06-21
Gate: `Gate 4: SatoriToSVGlide Compiler`
Reviewer status before this evidence: `BLOCKED`
Reviewer verdict after re-review: `PASS`
## Scope
This evidence addresses the reviewer blocker that the final SVGlide protocol SVG
was compiled from raw Satori SVG while receipt metadata claimed `CanvasSpec`.
The implementation now keeps raw Satori SVG as preview/layout evidence only. The
final `04-svg/page-###.svg` is compiled from a CanvasSpec-owned template artifact:
```text
04-svg/artboard/page-###.canvas-template.svg
```
P0 Gate 4 scope is explicitly text/shape only. Image asset binding and
`svglide-chart-spec-v1` chart marker proof remain Gate 8/P0c work.
## Code And Contract Changes
Changed files:
```text
skills/lark-slides/scripts/svglide_artboard_renderer.py
skills/lark-slides/scripts/svglide_artboard_renderer_test.py
skills/lark-slides/scripts/svglide_quality_gate.py
skills/lark-slides/scripts/svglide_quality_gate_test.py
skills/lark-slides/references/svglide-artboard-receipt.schema.json
skills/lark-slides/references/svglide-artboard-satori.contract.md
skills/lark-slides/references/svglide-artboard-full-plan-action.md
/Users/bytedance/Downloads/PLAN.md
```
Key implementation points:
```text
compile_satori_svg_to_svglide(...)
remains available for raw Satori SVG fail-fast tests and reports:
semantic_source = SatoriSVG
compiler_input = RawSatoriSVG
satori_svg_usage = compiler_input
compile_canvas_template_svg_to_svglide(...)
is the artboard_satori main path and reports:
semantic_source = CanvasSpec
compiler_input = CanvasSpecTemplateSVG
satori_svg_usage = preview_only
```
`render_project(...)` now writes and compiles from:
```text
04-svg/artboard/page-###.canvas-template.svg
```
Per-page artboard receipts now bind:
```text
canvas_template_svg
canvas_template_svg_sha256
compiler_input
compiler_input_sha256
compiler.semantic_source = CanvasSpec
compiler.compiler_input = CanvasSpecTemplateSVG
compiler.satori_svg_usage = preview_only
```
`receipts/satori-bridge.json` page entries now bind:
```text
canvas_template_svg
canvas_template_svg_sha256
compiler_input
compiler_input_sha256
compiler_input_type = CanvasSpecTemplateSVG
satori_svg_usage = preview_only
```
`quality_gate` now rejects:
```text
missing or stale canvas_template_svg
missing or stale compiler_input
compiler_input_type != CanvasSpecTemplateSVG
satori_svg_usage != preview_only
artboard receipt compiler_input != CanvasSpecTemplateSVG
artboard receipt compiler_input path not equal to canvas_template_svg
```
## Validation
Targeted unit tests:
```bash
python3 -m unittest \
skills/lark-slides/scripts/svglide_artboard_renderer_test.py \
skills/lark-slides/scripts/svglide_quality_gate_test.py
```
Result:
```text
Ran 35 tests in 1.708s
OK
```
Full unit test suite:
```bash
python3 -m unittest discover skills/lark-slides/scripts -p '*_test.py'
```
Result:
```text
Ran 252 tests in 9.955s
OK
```
Whitespace check:
```bash
git diff --check
```
Result:
```text
passed with no output
```
## P0b Fixture Evidence
Fresh P0b project:
```text
/private/tmp/svglide-p0b-gate4-641DXp
```
Command:
```bash
SVGLIDE_LARK_CLI_CMD="python3 /private/tmp/svglide_fake_lark_cli.py" \
python3 skills/lark-slides/scripts/svglide_project_runner.py run \
/private/tmp/svglide-p0b-gate4-641DXp \
--until dry_run \
--network-policy fixture \
--asset-provider none \
--image-backend none
```
Result:
```text
current_stage = dry_run
generate_svg = passed
prepare = passed
preflight = passed
quality_gate = passed
dry_run = passed
```
Critical receipt check:
```text
page-001: cover-hero / dark-clarity
compiler.semantic_source = CanvasSpec
compiler.compiler_input = CanvasSpecTemplateSVG
compiler.satori_svg_usage = preview_only
compiler_input = 04-svg/artboard/page-001.canvas-template.svg
page-002: comparison-cards / forest-signal
compiler.semantic_source = CanvasSpec
compiler.compiler_input = CanvasSpecTemplateSVG
compiler.satori_svg_usage = preview_only
compiler_input = 04-svg/artboard/page-002.canvas-template.svg
page-003: summary-final / warm-editorial
compiler.semantic_source = CanvasSpec
compiler.compiler_input = CanvasSpecTemplateSVG
compiler.satori_svg_usage = preview_only
compiler_input = 04-svg/artboard/page-003.canvas-template.svg
```
Bridge receipt check:
```text
receipts/satori-bridge.json pages[0..2]:
compiler_input_type = CanvasSpecTemplateSVG
satori_svg_usage = preview_only
```
Artifacts present:
```text
04-svg/artboard/page-001.canvas-template.svg
04-svg/artboard/page-002.canvas-template.svg
04-svg/artboard/page-003.canvas-template.svg
04-svg/artboard/raw/page-001.satori.svg
04-svg/artboard/raw/page-002.satori.svg
04-svg/artboard/raw/page-003.satori.svg
04-svg/page-001.svg
04-svg/page-002.svg
04-svg/page-003.svg
06-check/quality-gate.json
07-create/dry-run.json
```
## Remaining Non-Gate-4 Scope
This evidence does not claim completion of:
```text
Gate 7 P0c live_create/readback
Gate 8 image asset binding/readback
Gate 8 chart marker/readback
Gate 8 raster fallback fixture
Gate 9+ P1/P2 scale-out work
```

View File

@@ -0,0 +1,209 @@
# SVGlide Artboard Gate 5 Evidence
Date: 2026-06-21
Gate: `Gate 5: Runner And Quality Gate Integration`
Reviewer status before this evidence: `PENDING`
Reviewer verdict after re-review: `PASS`
## Scope
This evidence covers runner integration, artboard receipt freshness checks,
bounded page concurrency, stable page ordering, and legacy `direct_svg`
compatibility.
## Code Changes
Changed files:
```text
skills/lark-slides/scripts/svglide_artboard_renderer.py
skills/lark-slides/scripts/svglide_artboard_renderer_test.py
skills/lark-slides/scripts/svglide_quality_gate.py
skills/lark-slides/scripts/svglide_quality_gate_test.py
```
Key implementation points:
```text
svglide_project_runner.py
already dispatches `generate_svg` by `generation_mode`.
`direct_svg` keeps the existing generator script/external path.
`artboard_satori` calls `svglide_artboard_renderer.py` and then template-fit.
svglide_artboard_renderer.py
now runs page jobs with max_workers = min(4, page_count).
page results are sorted by page number before aggregate receipts/contact sheet.
`artboard-render` and `satori-bridge` summaries record max_workers.
svglide_quality_gate.py
validates aggregate artboard receipts.
validates per-page artboard receipts against schema.
validates raw Satori preview, PNG, render metadata, canvas-template compiler input,
semantic-map, node-layout-map, and final SVGlide hashes.
rejects RawSatoriSVG compiler metadata and stale compiler-input artifacts.
```
## Validation
Full unit test suite:
```bash
python3 -m unittest discover skills/lark-slides/scripts -p '*_test.py'
```
Result:
```text
Ran 254 tests in 10.234s
OK
```
Focused tests:
```bash
python3 -m unittest skills/lark-slides/scripts/svglide_artboard_renderer_test.py
```
Result:
```text
Ran 14 tests in 1.722s
OK
```
```bash
python3 -m unittest skills/lark-slides/scripts/svglide_quality_gate_test.py
```
Result:
```text
Ran 23 tests in 0.441s
OK
```
```bash
python3 -m unittest skills/lark-slides/scripts/svglide_project_runner_test.py
```
Result:
```text
Ran 35 tests in 3.917s
OK
```
Whitespace check:
```bash
git diff --check
```
Result:
```text
passed with no output
```
## Legacy direct_svg Regression
Fresh direct_svg evidence project:
```text
/private/tmp/svglide-direct-gate5-iYPBBA
```
Command:
```bash
python3 skills/lark-slides/scripts/svglide_project_runner.py run \
/private/tmp/svglide-direct-gate5-iYPBBA \
--until quality_gate \
--network-policy fixture \
--asset-provider none \
--image-backend none
```
Result:
```text
generation_mode = direct_svg
generator_mode = script
artboard_receipts absent
quality_gate = passed
failed_check_count = 0
```
## artboard_satori P0b Regression
Fresh P0b evidence project:
```text
/private/tmp/svglide-p0b-gate5-qg7PC6
```
Command:
```bash
SVGLIDE_LARK_CLI_CMD="python3 /private/tmp/svglide_fake_lark_cli.py" \
python3 skills/lark-slides/scripts/svglide_project_runner.py run \
/private/tmp/svglide-p0b-gate5-qg7PC6 \
--until dry_run \
--network-policy fixture \
--asset-provider none \
--image-backend none
```
Result:
```text
current_stage = dry_run
generate_svg = passed
prepare = passed
preflight = passed
quality_gate = passed
dry_run = passed
```
Concurrency and ordering evidence:
```text
receipts/artboard-render.json:
summary.max_workers = 3
pages = [1, 2, 3]
receipts/satori-bridge.json:
summary.max_workers = 3
pages = [1, 2, 3]
receipts/generate_svg.json:
artboard_receipts = [
04-svg/artboard/page-001.receipt.json,
04-svg/artboard/page-002.receipt.json,
04-svg/artboard/page-003.receipt.json
]
```
Freshness and negative tests now cover:
```text
missing artboard receipts
stale raw Satori SVG
stale canvas-template compiler input
invalid RawSatoriSVG compiler metadata
artboard receipt schema failure
stale prepared SVG before dry_run
```
## Remaining Scope
This evidence does not claim:
```text
P0c live_create/readback
Gate 8 chart/image/raster fallback proof
Gate 9+ P1/P2 asset and planning scale-out
```

View File

@@ -0,0 +1,172 @@
# SVGlide Artboard Gate 6 Evidence
Date: 2026-06-21
Gate: `Gate 6: P0a And P0b Local E2E`
Reviewer status before this evidence: `PENDING`
Reviewer verdict after re-review: `PASS`
## Scope
This evidence proves the checked-in P0a and P0b artboard fixtures are repeatable
through the real runner path, not ad hoc scripts.
Gate 6 does not claim `live_create` or `readback`; those remain Gate 7.
## P0a Fresh Run
Fixture source:
```text
skills/lark-slides/scripts/fixtures/svglide_artboard/p0a-cover
```
Fresh project:
```text
/private/tmp/svglide-p0a-gate6-zNSbw5
```
Command:
```bash
SVGLIDE_LARK_CLI_CMD="python3 /private/tmp/svglide_fake_lark_cli.py" \
python3 skills/lark-slides/scripts/svglide_project_runner.py run \
/private/tmp/svglide-p0a-gate6-zNSbw5 \
--until dry_run \
--network-policy fixture \
--asset-provider none \
--image-backend none
```
Result:
```text
current_stage = dry_run
generate_svg = passed
prepare = passed
preflight = passed
quality_gate = passed
dry_run = passed
```
Template/theme hit:
```text
page 1 = cover-hero / dark-clarity
```
Required artifacts present:
```text
04-svg/artboard/raw/page-001.satori.svg
04-svg/artboard/page-001.png
04-svg/artboard/page-001.canvas-template.svg
04-svg/artboard/page-001.semantic-map.json
04-svg/artboard/page-001.node-layout-map.json
04-svg/artboard/page-001.receipt.json
04-svg/page-001.svg
05-preview/contact-sheet.png
06-check/quality-gate.json
07-create/dry-run.json
receipts/generate_svg.json
```
## P0b Fresh Run
Fixture source:
```text
skills/lark-slides/scripts/fixtures/svglide_artboard/p0b-three-page
```
Fresh project:
```text
/private/tmp/svglide-p0b-gate5-qg7PC6
```
Command:
```bash
SVGLIDE_LARK_CLI_CMD="python3 /private/tmp/svglide_fake_lark_cli.py" \
python3 skills/lark-slides/scripts/svglide_project_runner.py run \
/private/tmp/svglide-p0b-gate5-qg7PC6 \
--until dry_run \
--network-policy fixture \
--asset-provider none \
--image-backend none
```
Result:
```text
current_stage = dry_run
generate_svg = passed
prepare = passed
preflight = passed
quality_gate = passed
dry_run = passed
```
Template/theme hit list:
```text
page 1 = cover-hero / dark-clarity
page 2 = comparison-cards / forest-signal
page 3 = summary-final / warm-editorial
```
Runner and receipt evidence:
```text
receipts/generate_svg.json:
generation_mode = artboard_satori
artboard_receipts = [
04-svg/artboard/page-001.receipt.json,
04-svg/artboard/page-002.receipt.json,
04-svg/artboard/page-003.receipt.json
]
receipts/artboard-render.json:
summary.max_workers = 3
pages = [1, 2, 3]
receipts/satori-bridge.json:
summary.max_workers = 3
pages = [1, 2, 3]
06-check/quality-gate.json:
status = passed
failed_check_count = 0
07-create/dry-run.json:
status = passed
```
## Validation Context
The same code state also passed:
```text
python3 -m unittest discover skills/lark-slides/scripts -p '*_test.py'
Ran 254 tests
OK
git diff --check
passed with no output
```
## Remaining Scope
This evidence does not claim:
```text
Gate 7 P0c live_create/readback
Gate 8 chart marker readback
Gate 8 image asset readback
Gate 8 raster fallback fixture
Gate 9+ P1/P2 scale-out work
```

View File

@@ -0,0 +1,287 @@
# SVGlide Artboard Gate 7 Evidence
Gate: `Gate 7: P0c Live Closure`
Status: `PASS`
Date: 2026-06-21
## Scope
This gate closes the P0c live path with the real `slides +create-svg` command:
```text
dry_run -> ppe_proof -> live_create -> readback
```
The fresh evidence project is:
```text
.tmp/svglide-p0c-gate7-live6
```
## Root Cause From Earlier Blocker
Earlier Gate 7 attempts failed with:
```text
[4001000] nodeServer invalid param
slides_added: 0
```
The failed receipts are kept for audit:
```text
.tmp/svglide-p0c-gate7-live4/07-create/live-create.json
.tmp/svglide-live-probes/probe-e2e-with-header.json
.tmp/svglide-live-probes/probe-e2e-no-header.json
```
The cause was not the artboard SVG output itself. The local process was not reaching the required PPE pure-SVG service lane. The successful lane requires:
```text
Whistle capture mode
open.feishu.cn -> open.feishu-pre.cn
Env=Pre_release
x-tt-env=ppe_pure_svg
HTTP_PROXY/HTTPS_PROXY=http://127.0.0.1:8899
```
The Whistle rule is now tracked in the CLI worktree:
```text
skills/lark-slides/references/ppe-pure-svg.whistle.js
sha256: 641d01be2b2ea6b7a57a21302ee45cf10cc60d247132f50681966b88481a8487
```
## Code Changes For Gate 7
Request-scoped PPE header propagation:
```text
shortcuts/common/runner.go
shortcuts/slides/slides_create.go
shortcuts/slides/slides_create_svg.go
shortcuts/slides/slides_create_svg_test.go
```
Runner and proof integration:
```text
skills/lark-slides/scripts/svglide_project_runner.py
skills/lark-slides/scripts/svglide_project_runner_test.py
skills/lark-slides/scripts/svglide_ppe_proof.py
skills/lark-slides/scripts/svglide_ppe_proof_test.py
skills/lark-slides/scripts/svglide_readback.py
skills/lark-slides/scripts/svglide_readback_test.py
skills/lark-slides/scripts/fixtures/svglide_artboard/p0c-live/07-create/ppe-proof.input.json
```
Implemented behavior:
```text
ppe-proof.input.json
-> validates quality_gate and dry_run hashes
-> validates Whistle proxy/capture/rule hash/injected headers
-> ppe-proof.json
-> runner live_create command
-> slides +create-svg --request-header x-tt-env=ppe_pure_svg
-> RuntimeContext.CallAPIWithHeaders
-> presentation create and slide add API calls
```
Readback now checks:
```text
created slide count
actual readback page count from xml_presentation.content
created slide order
blank page markers
text-fit and bounds markers
CanvasSpec visible text fragments
```
## Fresh P0c Command
```bash
SVGLIDE_LARK_CLI_CMD="env GOCACHE=/private/tmp/svglide-gocache HTTP_PROXY=http://127.0.0.1:8899 HTTPS_PROXY=http://127.0.0.1:8899 http_proxy=http://127.0.0.1:8899 https_proxy=http://127.0.0.1:8899 go run ." \
python3 skills/lark-slides/scripts/svglide_project_runner.py run \
.tmp/svglide-p0c-gate7-live6 \
--until readback \
--network-policy fixture \
--asset-provider none \
--image-backend none
```
Result:
```text
current_stage: readback
dry_run: passed
ppe_proof: passed
live_create: passed
readback: passed
```
## PPE Proof Receipt
Receipt:
```text
.tmp/svglide-p0c-gate7-live6/07-create/ppe-proof.json
```
Key evidence:
```text
status: passed
quality_gate_sha256: 072996e2b8a2c84d6d0efae0e0e23659438d7b8d1d129de3d520b40c13511844
dry_run_sha256: 9886b4fbc0d039827eca6bca99d5e8f7ddfe69ac20c78f9a8c30afc9cb713bd0
proxy.mode: whistle
proxy.capture: true
proxy.http_proxy: http://127.0.0.1:8899
proxy.https_proxy: http://127.0.0.1:8899
proxy.rewrite_host: open.feishu-pre.cn
proxy.rule_file: skills/lark-slides/references/ppe-pure-svg.whistle.js
proxy.rule_sha256: 641d01be2b2ea6b7a57a21302ee45cf10cc60d247132f50681966b88481a8487
proxy.inject_headers.Env: Pre_release
proxy.inject_headers.x-tt-env: ppe_pure_svg
headers.x-tt-env: ppe_pure_svg
summary.error_count: 0
```
## Live Create Receipt
Receipt:
```text
.tmp/svglide-p0c-gate7-live6/07-create/live-create.json
```
Key evidence:
```text
status: passed
returncode: 0
xml_presentation_id: MPcnsjAH5l5r2edcpWYcNhFVnVd
slides_added: 3
slide_ids: ["pbb", "pbu", "pbe"]
revision_id: 4
request_headers.x-tt-env: ppe_pure_svg
url: https://www.feishu.cn/slides/MPcnsjAH5l5r2edcpWYcNhFVnVd
```
Prepared SVG hashes were recorded in the live receipt:
```text
04-svg/prepared/page-001.svg: 6d35ebfe7ec500472a9fd5ab69af230d1a73aea40b2f5f6bc6f02f5a58a0900f
04-svg/prepared/page-002.svg: 9c623b3138cab6f0dcf68fb3cdfc68e9747f11ef5a8ba9f048a7a2360524e799
04-svg/prepared/page-003.svg: 13d7fc19310ac89a699f1fdf85597628b1a8428aa55468262c9a920644bf4e24
```
## Readback Receipt
Receipt:
```text
.tmp/svglide-p0c-gate7-live6/08-readback/readback-check.json
```
Key evidence:
```text
status: passed
xml_presentation_id: MPcnsjAH5l5r2edcpWYcNhFVnVd
expected_slide_count: 3
created_slide_count: 3
page_count: passed, expected 3, actual 3
slide_ids: passed, actual 3
slide_order: passed, expected ["pbb", "pbu", "pbe"], actual ["pbb", "pbu", "pbe"]
blank_page: passed
text_fit: passed
bounds: passed
core_visible_text: passed, expected 22, missing []
failed_checks: []
```
Raw readback content was saved at:
```text
.tmp/svglide-p0c-gate7-live6/08-readback/xml-presentations-get.json
```
## Validation Passed
```bash
python3 -m unittest skills/lark-slides/scripts/svglide_ppe_proof_test.py
```
Result:
```text
Ran 4 tests
OK
```
```bash
python3 -m unittest skills/lark-slides/scripts/svglide_readback_test.py
```
Result:
```text
Ran 9 tests
OK
```
```bash
python3 -m unittest skills/lark-slides/scripts/svglide_project_runner_test.py
```
Result:
```text
Ran 36 tests
OK
```
```bash
python3 -m unittest discover skills/lark-slides/scripts -p '*_test.py'
```
Result:
```text
Ran 259 tests
OK
```
```bash
env GOCACHE=/private/tmp/svglide-gocache go test ./shortcuts/common ./shortcuts/slides
```
Result:
```text
ok github.com/larksuite/cli/shortcuts/common
ok github.com/larksuite/cli/shortcuts/slides
```
## Reviewer Decision Needed
Reviewer verdict:
```text
Verdict: PASS
Blocking issues: None.
```
Gate 7 is accepted because the reviewer confirmed:
```text
1. The PPE lane proof is explicit enough and hash-bound.
2. The fresh P0c run used real lark-cli/go run, not fake dry-run.
3. live_create created 3 pages.
4. readback proves page count, page order, nonblank pages, and core CanvasSpec visible text.
5. The earlier blocker is resolved by the documented Whistle/PPE lane setup.
```

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,422 @@
# SVGlide Artboard Gate 9 Evidence
Gate: `Gate 9: P1 Asset System Scale-Out`
Status: `PASS`
Date: 2026-06-21
Reviewer history:
```text
Initial review: BLOCKED
Blocker: source intake lacked required Gate 9 fields and per-source conversion traceability.
Fix: source intake now includes required fields and conversion_records[] for every source family; tests now assert these fields.
Second review: BLOCKED
Blocker: ppt-master provenance incorrectly said no local LICENSE found.
Fix: ppt-master provenance now records local MIT LICENSE, and tests assert that it does not regress.
Current state: narrow re-review requested after fresh validation.
Final reviewer verdict: PASS.
```
## Scope
Gate 9 moves the P0 artboard path from a minimal 3-template vertical slice to a reusable P1 visual asset system:
```text
external examples as inspiration only
-> source intake inventory
-> layout archetypes
-> component variants
-> template registry
-> theme registry
-> golden CanvasSpec fixtures
-> Satori/resvg render regression
```
No external HTML/CSS/SVG library is used as a runtime renderer.
## Source Intake Inventory
Implemented:
```text
skills/lark-slides/references/svglide-p1-source-intake.json
```
Reviewer blocker fix:
```text
The intake file now records the required Gate 9 fields for every source family:
source_path
source_type
extract_fields
conversion_target
acceptance_rule
forbidden_usage
source_hash_or_version
license_or_provenance
```
Covered source families:
```text
open-design html-ppt examples
open-design retro quarterly review
open-design zhangzara design templates
ppt-master examples
PosterGen poster-generation rules
```
Policy recorded in the inventory:
```text
runtime_import: forbidden
usage: inspiration source only
conversion_path: source example -> visual archetype -> CanvasSpec schema fields -> Template Registry -> Theme Token
```
Verified source versions/provenance:
```text
open-design: git 2aadac07c, origin https://github.com/nexu-io/open-design.git, Apache-2.0
ppt-master: git 45d9a79, origin https://github.com/hugohe3/ppt-master.git, MIT, inspiration-only
PosterGen: git 8a54325, origin https://github.com/Y-Research-SBU/PosterGen.git, MIT
```
Per-source conversion traceability is recorded in `conversion_records[]`.
Each record links:
```text
source_examples
-> abstraction_record
-> canvas_spec_fields
-> registry_output.templates/themes/components/layouts/golden_fixtures
-> acceptance_rule
```
Examples:
```text
open-design html-ppt-weekly-report
-> dense report pages
-> agenda-list / metric-dashboard
-> agenda-list.canvas-spec.json / metric-dashboard.canvas-spec.json
open-design zhangzara editorial/cartesian examples
-> editorial tri-tone and grid discipline
-> section-title / image-feature / roadmap-lanes / architecture-blueprint
-> editorial-tritone / swiss-red / blueprint-technical
ppt-master attention/kubernetes/swiss/glass/newspaper examples
-> technical narrative, blueprint, comparison, data, quote abstractions
-> process-flow / architecture-blueprint / comparison-cards / data-story / quote-focus
PosterGen config and prompt rules
-> research poster three-column layout and bounded section rules
-> research-poster / paper-research / PosterSection
```
## P1 Asset Counts
Implemented assets:
```text
Canvas Templates: 15 active templates
Theme Tokens: 10 active themes
Component Variants: 23 active variants
Layout Archetypes: 10 active archetypes
Golden CanvasSpec fixtures: 15, one per active template
```
Template registry:
```text
skills/lark-slides/references/svglide-template-registry.json
```
Active template IDs:
```text
cover-hero
comparison-cards
summary-final
section-title
agenda-list
timeline-steps
process-flow
metric-dashboard
quote-focus
image-feature
research-poster
data-story
risk-alert
roadmap-lanes
architecture-blueprint
```
Each template declares:
```text
renderer_id
layout_family
required_content
optional_content
max_items
text_budget
supported_theme_ids
```
Theme registry:
```text
skills/lark-slides/scripts/artboard_renderer/themes/registry.json
```
Active theme IDs:
```text
dark-clarity
forest-signal
warm-editorial
blueprint-technical
editorial-tritone
cobalt-grid
finance-dark
swiss-red
glass-neon
paper-research
```
Layout archetype registry:
```text
skills/lark-slides/references/svglide-layout-archetypes.json
```
Component registry:
```text
skills/lark-slides/references/svglide-component-registry.json
```
## Renderer Changes
Node/Satori renderer:
```text
skills/lark-slides/scripts/artboard_renderer/templates/p0-templates.mjs
skills/lark-slides/scripts/artboard_renderer/dist/render.mjs
```
The renderer now supports all 15 active templates.
Python fallback and admission control:
```text
skills/lark-slides/scripts/svglide_artboard_renderer.py
```
Changes:
```text
SUPPORTED_TEMPLATES expanded from 3 to 15
unsupported template message no longer says P0-only
generic P1 local fallback added for Node/Satori-disabled execution
```
## Golden Fixtures
Golden fixture directory:
```text
skills/lark-slides/scripts/fixtures/svglide_artboard/golden/
```
New P1 fixtures:
```text
section-title.canvas-spec.json
agenda-list.canvas-spec.json
timeline-steps.canvas-spec.json
process-flow.canvas-spec.json
metric-dashboard.canvas-spec.json
quote-focus.canvas-spec.json
image-feature.canvas-spec.json
research-poster.canvas-spec.json
data-story.canvas-spec.json
risk-alert.canvas-spec.json
roadmap-lanes.canvas-spec.json
architecture-blueprint.canvas-spec.json
```
Existing P0 fixtures retained:
```text
cover-hero.canvas-spec.json
comparison-cards.canvas-spec.json
summary-final.canvas-spec.json
```
Every active template has one golden CanvasSpec fixture. Each fixture includes:
```text
version
canvas
safe_area
template_id
theme_id
theme
content
semantic_elements
quality_constraints
```
## Tests Added Or Updated
Updated:
```text
skills/lark-slides/scripts/svglide_artboard_renderer_test.py
```
Coverage:
```text
active theme count >= 10
required P1 themes registered
active template count >= 15
required P1 templates registered
all active templates allow all active themes
component registry active variants >= 20
layout archetypes >= 8
source intake runtime_import == forbidden
source intake required fields are present for every source
source intake conversion_records link examples to registry outputs and golden fixtures
every active template has a valid golden CanvasSpec
all active golden fixtures render through Satori/resvg into PNG and SVGlide SVG
bounded max_workers remains 4 for 15-page render
```
## Validation Commands
JSON parse:
```bash
python3 -c 'import json, pathlib; paths=list(pathlib.Path("skills/lark-slides/references").glob("svglide-*.json"))+list(pathlib.Path("skills/lark-slides/scripts/artboard_renderer/themes").glob("*.json"))+list(pathlib.Path("skills/lark-slides/scripts/fixtures/svglide_artboard/golden").glob("*.json")); [json.load(p.open(encoding="utf-8")) for p in paths]; print(len(paths))'
```
Result:
```text
54 JSON files parsed
```
Node template syntax:
```bash
node --check skills/lark-slides/scripts/artboard_renderer/templates/p0-templates.mjs
```
Result:
```text
PASS
```
Python syntax:
```bash
env PYTHONPYCACHEPREFIX=/private/tmp/svglide-pycache \
python3 -m py_compile \
skills/lark-slides/scripts/svglide_artboard_renderer.py \
skills/lark-slides/scripts/svglide_artboard_renderer_test.py
```
Result:
```text
PASS
```
Renderer build:
```bash
pnpm --dir skills/lark-slides/scripts/artboard_renderer run build
```
Result:
```text
PASS
dist/render.mjs 925.5kb
```
Renderer runtime checks:
```bash
node skills/lark-slides/scripts/artboard_renderer/render.mjs --check-runtime
node skills/lark-slides/scripts/artboard_renderer/dist/render.mjs --check-runtime
```
Result:
```text
PASS
satori_version: 0.26.0
resvg_version: 2.6.2
font_path: /System/Library/Fonts/Supplemental/Arial Unicode.ttf
```
Focused artboard tests:
```bash
env PYTHONPYCACHEPREFIX=/private/tmp/svglide-pycache \
python3 -m unittest skills/lark-slides/scripts/svglide_artboard_renderer_test.py
```
Result:
```text
Ran 15 tests in 5.301s after provenance blocker fix.
OK
```
Scripts regression:
```bash
env PYTHONPYCACHEPREFIX=/private/tmp/svglide-pycache \
python3 -m unittest discover skills/lark-slides/scripts -p '*_test.py'
```
Result:
```text
Ran 265 tests in 15.270s after provenance blocker fix.
OK
```
Diff check:
```bash
git diff --check
```
Result:
```text
PASS
```
## Known Remaining Scope
Gate 9 does not complete:
```text
Gate 10 prompt/planning layer
Gate 11 packaging/distribution decision
Gate 12 final full-plan acceptance
```
Gate 9 reviewer verdict is `PASS`.

View File

@@ -0,0 +1,278 @@
# SVGlide Artboard/Satori P0 Goal Lock
## 1. Current Objective
当前唯一目标是按 `/Users/bytedance/Downloads/PLAN.md` 跑通一个可重复的 `artboard_satori` 竖切,而不是继续做新的 `direct_svg` 视觉 demo。
目标链路:
```text
3 页 artboard_satori fixture
-> 命中 3 个 Template Registry 模板
-> Satori 渲染
-> resvg 输出 PNG/contact sheet
-> compiler 输出 SVGlide protocol SVG
-> quality_gate + dry_run 通过
```
## 2. Source Of Truth
实现和审查必须优先对齐以下文件:
```text
/Users/bytedance/Downloads/PLAN.md
skills/lark-slides/references/svglide-artboard-satori.contract.md
skills/lark-slides/references/svglide-canvas-spec.schema.json
skills/lark-slides/references/svglide-renderer-registry.json
skills/lark-slides/scripts/svglide_project_runner.py
skills/lark-slides/scripts/svglide_artboard_renderer.py
skills/lark-slides/scripts/artboard_renderer/package.json
```
如果本文件与 `PLAN.md` 冲突,以 `PLAN.md` 为主;如果 `PLAN.md` 表述仍不够工程化,先补本文件或相关 contract再进入实现。
## 3. Hard No-Go Rules
接下来不允许用以下方式制造“看起来完成”:
```text
不再用 direct_svg 做新的主验证
不再手写 .tmp/.../generate_svg.py 作为主路径
不再用 QuickLook / qlmanage 作为主截图路径
不先放松 production 门禁来制造通过
不降低或删除任何既有 production gateartboard 只能增加检查
不新增无法被 Template Registry / renderer registry 追踪的模板或 renderer
不手工 patch state.json 作为常规 runner 操作
不把 Satori 输出 SVG 当作 SVGlide 语义真源
不把外部 HTML/CSS 库直接接入运行时
```
允许临时诊断,但诊断结果不能作为 P0 验收证据。
## 4. Required Vertical Slice
P0 竖切必须至少包含 3 页 fixture
| Page | Required template | Purpose |
| --- | --- | --- |
| 1 | `cover-hero` | 证明封面模板和主题 token 生效 |
| 2 | `comparison-cards` | 证明多内容块模板、文本预算和 template fit 生效 |
| 3 | `summary-final` | 证明收束页模板、可见文本追溯和多页一致性生效 |
每页必须满足:
```text
generation_mode = artboard_satori
canvas_spec.version 存在
canvas_spec.template_id 来自 Template Registry
canvas_spec.theme 或 theme_id 存在
canvas_spec.content.title 存在
visible_text / semantic elements 可追溯
artboard receipt 存在
```
P0 当前只验收到 `quality_gate + dry_run``ppe_proof / live_create / readback` 属于 P0c 后续闭环,不是本竖切的通过条件;但本竖切不能做任何会阻碍 P0c 的协议或 receipt 设计。
## 5. Dependency Lock
`skills/lark-slides/scripts/artboard_renderer/package.json` 必须显式声明:
```text
satori
@resvg/resvg-js
```
P0 主路径:
```text
Satori SVG -> @resvg/resvg-js -> PNG preview / contact sheet
```
QuickLook、Chromium、Playwright 只能作为 fallback 或调试工具,不能作为 P0 默认路径。
receipt 必须记录:
```text
node_version
satori_version
resvg_version
font_hashes
template_id
theme_id
input_canvas_spec_hash
output_satori_svg_hash
output_svglide_svg_hash
output_png_hash
```
## 6. Template Registry And Theme Lock
模板和主题不能用“等价来源”模糊带过。P0 必须有明确 registry/theme 证据:
```text
skills/lark-slides/references/svglide-template-registry.json
skills/lark-slides/scripts/artboard_renderer/templates/*.mjs
skills/lark-slides/scripts/artboard_renderer/themes/*.mjs
```
如果 P0 选择项目级 registry也必须落盘为
```text
02-plan/template-registry.json
02-plan/theme-registry.json
```
并且必须满足:
```text
未知 template_id fail-fast
未知 theme_id fail-fast
template_id 必须来自 registry
theme_id 必须来自 registry
template registry hash 写入 template-fit receipt
theme registry hash 写入 artboard-render receipt
generate_svg receipt 汇总 registry/theme hash
quality_gate 校验 registry/theme hash fresh
```
禁止临时在 renderer 代码里硬编码一个未登记模板并把它算作“命中模板”。
## 7. Acceptance Evidence
P0 通过时必须能提供以下证据:
```text
02-plan/slide_plan.json
02-plan/template-registry.json 或 skills/lark-slides/references/svglide-template-registry.json
02-plan/theme-registry.json 或 skills/lark-slides/scripts/artboard_renderer/themes/*.mjs
04-svg/artboard/raw/page-###.satori.svg
04-svg/artboard/page-###.png
04-svg/page-###.svg
05-preview/contact-sheet.png
06-check/canvas-spec-validate.json
06-check/template-fit.json
06-check/quality-gate.json
07-create/dry-run.json
receipts/canvas-spec-validate.json
receipts/template-fit-check.json
receipts/artboard-render.json
receipts/satori-bridge.json
receipts/generate_svg.json
receipts/dry_run.json
```
并且报告中必须能证明:
```text
真的走 artboard_satori
真的命中 3 个模板
真的使用 @resvg/resvg-js
quality_gate passed
dry_run passed
没有依赖 direct_svg 旁路
没有依赖 QuickLook 作为主路径
```
## 8. Quality Gate Hard Requirements
`quality_gate` 通过必须证明以下 hash/freshness 关系,不允许只检查文件存在:
```text
generate_svg.receipt.generation_mode == artboard_satori
generate_svg.receipt.artboard_receipts 非空且页数匹配
canvas-spec-validate receipt hash == 当前 slide_plan canvas_spec hash
template-fit receipt hash == 当前 CanvasSpec + Template Registry hash
artboard-render receipt hash == 当前 CanvasSpec + Theme Registry + font inputs hash
satori-bridge receipt hash == 当前 CanvasSpec + semantic-map + node-layout-map + raw Satori SVG hash
prepared SVG hash == satori-bridge 输出 SVGlide SVG hash
PNG/contact-sheet hash == artboard-render 或 preview artifact 记录值
dry_run 输入 prepared files hash == quality_gate prepared_files hash
```
`quality_gate` 还必须保证:
```text
direct_svg receipt 不能作为 artboard_satori 验收证据
缺少 resvg_version 时失败
缺少 template_id/theme_id 时失败
缺少 template registry/theme registry hash 时失败
缺少 node-layout-map 或 drift 超阈值时失败
```
任何为了让 P0 通过而删除、降级或绕过既有 production 检查的改动,都视为偏离目标。
## 9. Stage Discipline
每次阶段汇报只回答四件事:
```text
做了什么
改了哪些文件
跑了哪些验证
离 P0 竖切还差什么
```
不在 P0 竖切完成前扩展到:
```text
完整 Deck Planner
外部美学资产大规模抽取
自动 repair loop
线上 live_create
CLI binary/npm 分发方案
复杂图表
真实图片资产
```
## 10. Subagent Supervisor Checklist
独立监督者每次审查必须检查:
```text
1. 是否偏离当前唯一目标
2. 是否偷偷回到 direct_svg
3. 是否绕开 Template Registry
4. 是否缺少 CanvasSpec / template_id / theme_id
5. 是否没有安装或调用 @resvg/resvg-js
6. 是否用 QuickLook / browser 截图冒充主路径
7. 是否为了过门禁而放松 production gate
8. 是否手工 patch state.json 代替 runner 能力
9. 是否有可重复 fixture 和测试证据
10. 是否能映射回 PLAN.md 的 P0/P1 条目
11. 是否具备 canvas-spec/template-fit/artboard-render/satori-bridge 的具体 receipt
12. quality_gate 是否检查 hash/freshness而不是只检查文件存在
13. registry/theme 是否 hash-bound 且未知 id fail-fast
```
审查输出格式:
```text
Verdict: PASS / BLOCKED
Blocking issues:
- ...
Non-blocking risks:
- ...
Evidence checked:
- ...
Required next action:
- ...
```
## 11. First Implementation Steps
推荐执行顺序:
```text
1. 给 artboard_renderer 加 @resvg/resvg-js并生成 PNG/contact sheet
2. 补最小 Template Registry包含 cover-hero / comparison-cards / summary-final
3. 补 3 页 artboard_satori fixture 的 CanvasSpec
4. 让 generate_svg receipt 汇总 artboard receipts 和 PNG 输出
5. quality_gate 检查 artboard receipts fresh
6. 跑到 dry_run
```
任何下一步如果不能落到以上 6 项之一,默认先不做。

View File

@@ -0,0 +1,125 @@
# SVGlide Artboard Packaging Decision
Status: Gate 11 implemented, pending reviewer verdict
## Decision
`artboard_renderer` stays as a `lark-slides` skill-local Node subpackage:
```text
skills/lark-slides/scripts/artboard_renderer/
package.json
pnpm-lock.yaml
render.mjs
dist/render.mjs
templates/
themes/
components/
```
The root CLI remains a Go binary. Satori/resvg are not added as Go modules and are not installed in the root CLI package.
## What Is Embedded
`skills_embed.go` now embeds a whitelist:
```text
skills/*/SKILL.md
skills/*/references
skills/*/routes
skills/*/scenes
skills/*/prompts
skills/*/scripts/*.py
skills/*/scripts/artboard_renderer/*.mjs
skills/*/scripts/artboard_renderer/package.json
skills/*/scripts/artboard_renderer/pnpm-lock.yaml
skills/*/scripts/artboard_renderer/components
skills/*/scripts/artboard_renderer/dist
skills/*/scripts/artboard_renderer/templates
skills/*/scripts/artboard_renderer/themes
```
This makes `lark-cli skills read/list` version-match the binary for prompts, Python scripts, and renderer package resources.
## What Is Not Embedded
The Go binary must not embed:
```text
node_modules/
fixtures/
runtime project artifacts
assets/
.tmp/
```
`@resvg/resvg-js` is native and platform-specific. It must be installed by the package manager or supplied by a platform dependency layer, not committed or broadly embedded.
## Dependency Installation
Required local or CI install command:
```bash
pnpm --dir skills/lark-slides/scripts/artboard_renderer install --frozen-lockfile
```
Required runtime checks:
```bash
node skills/lark-slides/scripts/artboard_renderer/render.mjs --check-runtime
node skills/lark-slides/scripts/artboard_renderer/dist/render.mjs --check-runtime
python3 skills/lark-slides/scripts/svglide_artboard_package_check.py --pretty
```
The subpackage pins:
```text
satori: 0.26.0
@resvg/resvg-js: 2.6.2
```
The lockfile must include macOS arm64 and x64 optional native packages:
```text
@resvg/resvg-js-darwin-arm64
@resvg/resvg-js-darwin-x64
```
## Runtime Contract
Satori is bundled into `dist/render.mjs`. The release path must not require users to clone a sibling Satori source repository.
`@resvg/resvg-js` remains the required native runtime dependency. Missing Node or resvg dependencies must fail before live create.
Operator repair command:
```bash
pnpm --dir skills/lark-slides/scripts/artboard_renderer install --frozen-lockfile
node skills/lark-slides/scripts/artboard_renderer/dist/render.mjs --check-runtime
```
No-network environments must not auto-fetch packages during generation. They need a prewarmed pnpm store, CI-installed dependencies, or a packaged platform dependency layer.
## CLI Boundary
`slides +create-svg` still consumes final SVGlide SVG files only. It does not run Satori, resvg, Python generation, or planner prompts.
The artboard generation path remains an agent/skill pipeline:
```text
Planner JSON
-> CanvasSpec
-> artboard_renderer Satori/resvg preview
-> SatoriToSVGlide compiler
-> existing SVGlide prepare/quality_gate/dry_run/live_create/readback
```
## Validation Evidence
Gate 11 evidence is recorded in:
```text
skills/lark-slides/references/svglide-artboard-gate11-evidence.md
skills/lark-slides/scripts/fixtures/svglide_artboard/gate11_package/06-check/artboard-package-check.json
skills/lark-slides/scripts/fixtures/svglide_artboard/gate11_package/receipts/artboard-package-check.json
```

View File

@@ -0,0 +1,73 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://larksuite.local/svglide-artboard-receipt.schema.json",
"title": "SVGlide artboard render receipt",
"type": "object",
"required": [
"version",
"stage",
"status",
"page",
"canvas_spec_path",
"canvas_spec_sha256",
"template_id",
"theme_id",
"template_registry",
"template_registry_sha256",
"theme_registry",
"theme_registry_sha256",
"satori_version",
"resvg_version",
"font_hashes",
"satori_svg",
"satori_svg_sha256",
"png",
"png_sha256",
"render_metadata",
"render_metadata_sha256",
"canvas_template_svg",
"canvas_template_svg_sha256",
"compiler_input",
"compiler_input_sha256",
"semantic_map",
"semantic_map_sha256",
"node_layout_map",
"node_layout_map_sha256",
"svglide_svg",
"svglide_svg_sha256"
],
"additionalProperties": true,
"properties": {
"version": {"const": "svglide-artboard-receipt/v1"},
"stage": {"const": "generate_svg"},
"status": {"enum": ["passed", "failed"]},
"page": {"type": "integer", "minimum": 1},
"canvas_spec_path": {"type": "string", "minLength": 1},
"canvas_spec_sha256": {"type": "string", "minLength": 1},
"template_id": {"type": "string", "minLength": 1},
"theme_id": {"type": "string", "minLength": 1},
"template_registry": {"type": "string", "minLength": 1},
"template_registry_sha256": {"type": "string", "minLength": 1},
"theme_registry": {"type": "string", "minLength": 1},
"theme_registry_sha256": {"type": "string", "minLength": 1},
"satori_version": {"type": "string", "minLength": 1},
"resvg_version": {"type": "string", "minLength": 1},
"font_hashes": {"type": "array", "minItems": 1},
"satori_svg": {"type": "string", "minLength": 1},
"satori_svg_sha256": {"type": "string", "minLength": 1},
"png": {"type": "string", "minLength": 1},
"png_sha256": {"type": "string", "minLength": 1},
"render_metadata": {"type": "string", "minLength": 1},
"render_metadata_sha256": {"type": "string", "minLength": 1},
"canvas_template_svg": {"type": "string", "minLength": 1},
"canvas_template_svg_sha256": {"type": "string", "minLength": 1},
"compiler_input": {"type": "string", "minLength": 1},
"compiler_input_sha256": {"type": "string", "minLength": 1},
"semantic_map": {"type": "string", "minLength": 1},
"semantic_map_sha256": {"type": "string", "minLength": 1},
"node_layout_map": {"type": "string", "minLength": 1},
"node_layout_map_sha256": {"type": "string", "minLength": 1},
"svglide_svg": {"type": "string", "minLength": 1},
"svglide_svg_sha256": {"type": "string", "minLength": 1}
}
}

View File

@@ -0,0 +1,153 @@
# SVGlide Artboard / Satori Contract
P0a adds an `artboard_satori` generation mode inside `generate_svg`.
This mode is intentionally narrow:
- `CanvasSpec` is the semantic source of truth.
- A CanvasSpec-owned template artifact / semantic map is the direct compiler input for SVGlide private SVG.
- Raw Satori SVG is preview/layout evidence only; it is not the semantic source for final live SVG.
- `SVGLIDE_ARTBOARD_USE_NODE_SATORI=1` enables the Node adapter. Published skills must use the bundled adapter at `skills/lark-slides/scripts/artboard_renderer/dist/render.mjs`.
- Published CLI/skill resources must not require a sibling Satori source checkout; Satori is bundled into `dist/render.mjs`.
- The native `@resvg/resvg-js` package is still a runtime dependency and must be installed from the locked skill subpackage before `artboard_satori` runs.
- Live input remains SVGlide protocol SVG.
- The downstream chain stays unchanged: `prepare -> preview -> preflight -> quality_gate -> dry_run`.
## Boundary
Do not pass arbitrary Satori SVG to `slides +create-svg`.
The compiler must receive CanvasSpec-derived template IR, then produce SVGlide protocol SVG with private `slide:*` markers. P0 records this with per-page artboard receipts in `artboard_receipts` and aggregate child receipts in `artboard_additional_receipts`.
Default offline chain:
```text
CanvasSpec
-> Satori-compatible template renderer
-> 04-svg/artboard/page-xxx.canvas-template.svg
-> compile_canvas_template_svg_to_svglide
-> 04-svg/page-xxx.svg
```
Optional true Satori chain:
```text
CanvasSpec
-> skills/lark-slides/scripts/artboard_renderer/dist/render.mjs
-> bundled Satori runtime
-> 04-svg/artboard/raw/page-xxx.satori.svg
-> resvg PNG preview
-> CanvasSpec template artifact remains compiler input
-> compile_canvas_template_svg_to_svglide
-> 04-svg/page-xxx.svg
```
Local development may run the unbundled source entry
`skills/lark-slides/scripts/artboard_renderer/render.mjs` after
`pnpm install --frozen-lockfile`. That path is not the release contract.
## P0 Supported Surface
P0 supports three templates:
- `template_id=cover-hero`
- `template_id=comparison-cards`
- `template_id=summary-final`
P0 has two Satori source modes:
- default offline mode: Python emits deterministic Satori-compatible SVG from the controlled template renderer
- real Satori mode: `skills/lark-slides/scripts/artboard_renderer/dist/render.mjs` renders CanvasSpec through bundled Satori runtime. It must not require a sibling Satori source repository, but it does require package-managed `@resvg/resvg-js` native dependencies.
## Adapter Packaging
The CLI repository is a Go binary plus separately installed skills. Satori is a
Node dependency and must not be added as a Go module.
Release shape:
```text
skills/lark-slides/scripts/artboard_renderer/
package.json # dependency declaration; satori and @resvg/resvg-js are exact registry versions
pnpm-lock.yaml # pinned build input
render.mjs # source entry for local development
dist/render.mjs # published runtime entry; Satori is bundled, resvg stays native
templates/
themes/
components/
```
Build command:
```bash
cd skills/lark-slides/scripts/artboard_renderer
pnpm install --frozen-lockfile
pnpm run build
node dist/render.mjs --check-runtime
```
Gate 11 packaging validation:
```bash
python3 skills/lark-slides/scripts/svglide_artboard_package_check.py --pretty
```
Runtime rules:
- Published skills call `dist/render.mjs`.
- The adapter may fall back to source `render.mjs` only in local development.
- `satori` must be a fixed npm version in `package.json` and lockfile, not a `file:` dependency.
- `@resvg/resvg-js` must be a fixed npm version in `package.json` and lockfile; macOS arm64/x64 optional native packages must be present in `pnpm-lock.yaml`.
- `node_modules/` must not be committed or embedded into the Go binary. Release packaging must install dependencies for the target host or provide a preinstalled platform dependency layer.
- If Node or resvg dependencies are missing, fail before live create and run `pnpm --dir skills/lark-slides/scripts/artboard_renderer install --frozen-lockfile`, then rerun `node skills/lark-slides/scripts/artboard_renderer/dist/render.mjs --check-runtime`.
- If no usable system font exists, set `SVGLIDE_SATORI_FONT_PATH` to a local `.ttf` or `.otf` font.
P0 native output surface:
- `rect slide:role="shape"`
- `circle slide:role="shape"`
- `foreignObject slide:role="shape" slide:shape-type="text"`
P0 Gate 4 certifies text/shape mapping only. Image asset binding and
`svglide-chart-spec-v1` chart markers remain separate Gate 8/P0c proof items and
must not be claimed as complete from Gate 4 evidence.
P0 fail-fast surface for final SVGlide output:
- remote assets
- remote fonts
- WOFF2 fonts
- `filter` / `fe*`
- `mask`
- `clipPath`
- `pattern`
- CSS animation / transition
- `%`, `em`, `rem`, or `calc(...)` geometry
- root-level SVG text in final SVGlide output
## Required Receipts
For `generation_mode=artboard_satori`, `receipts/generate_svg.json` must include:
- existing generator receipt fields: `generated_files`, `page_receipts`, `plan_sha256`, `evidence_sha256`, `asset_manifest_sha256`, `source_receipt_sha256`
- `generation_mode: "artboard_satori"`
- `artboard_receipts`: ordered per-page receipt paths, for example `04-svg/artboard/page-001.receipt.json`
- `artboard_additional_receipts`: aggregate receipt paths, currently `receipts/canvas-spec-validate.json`, `receipts/artboard-render.json`, and `receipts/satori-bridge.json`
- `template_fit_check`: `06-check/template-fit.json`
- `canvas_spec_validate`: `06-check/canvas-spec-validate.json`
- `artboard_render_receipt`: `receipts/artboard-render.json`
- `satori_bridge_receipt`: `receipts/satori-bridge.json`
Each per-page artboard receipt must bind:
- CanvasSpec hash
- raw Satori preview SVG hash
- CanvasSpec template SVG hash
- compiler input path and hash
- renderer mode (`local-static` or `satori-node`)
- compiler mode (`CanvasSpecTemplateSVG` / `preview_only`)
- semantic map hash
- node layout map hash
- final SVGlide SVG hash
`quality_gate` rejects missing, failed, or stale artboard receipts.

View File

@@ -0,0 +1,72 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://larksuite.local/svglide-canvas-plan.schema.json",
"title": "SVGlide Canvas Planner Output",
"type": "object",
"additionalProperties": true,
"required": ["schema_version", "route", "generation_mode", "plan_path", "quality_gates", "art_direction", "style_preset", "style_selection_reason", "style_system", "loaded_rule_set", "slides"],
"properties": {
"schema_version": {"const": "svglide-canvas-plan/v1"},
"route": {"const": "svglide-svg"},
"generation_mode": {"const": "artboard_satori"},
"plan_path": {"type": "string", "minLength": 1},
"style_preset": {"type": "string", "minLength": 1},
"style_selection_reason": {"type": "string", "minLength": 1},
"style_system": {"type": "object"},
"loaded_rule_set": {"type": "array", "minItems": 1, "items": {"type": "string", "minLength": 1}},
"quality_gates": {
"type": "object",
"required": ["no_text_overflow", "no_debug_guides", "no_xml_like_pages"],
"properties": {
"no_text_overflow": {"const": true},
"no_debug_guides": {"const": true},
"no_xml_like_pages": {"const": true}
},
"additionalProperties": true
},
"art_direction": {
"type": "object",
"required": ["cover_treatment", "section_divider_treatment", "closing_treatment", "deck_motif", "svg_native_moments"],
"properties": {
"cover_treatment": {"type": "string", "minLength": 1},
"section_divider_treatment": {"type": "string", "minLength": 1},
"closing_treatment": {"type": "string", "minLength": 1},
"deck_motif": {"type": "string", "minLength": 1},
"svg_native_moments": {"type": "array", "minItems": 3, "items": {"type": "string", "minLength": 1}}
},
"additionalProperties": true
},
"slides": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"required": ["page", "title", "key_message", "renderer_id", "layout_family", "visual_recipe", "visual_intent", "visual_focal_point", "visual_signature", "svg_effects", "required_primitives", "svg_primitives", "xml_like_risk", "content_density_contract", "risk_flags", "source_policy", "canvas_spec"],
"properties": {
"page": {"type": "integer", "minimum": 1},
"title": {"type": "string", "minLength": 1},
"key_message": {"type": "string", "minLength": 1},
"renderer_id": {"type": "string", "minLength": 1},
"layout_family": {"type": "string", "minLength": 1},
"visual_recipe": {"type": "string", "minLength": 1},
"visual_intent": {"type": "string", "minLength": 1},
"visual_focal_point": {"type": "string", "minLength": 1},
"visual_signature": {"type": "string", "minLength": 1},
"svg_effects": {"type": "array", "items": {"type": "string", "minLength": 1}},
"required_primitives": {"type": "array", "items": {"type": "string", "minLength": 1}},
"svg_primitives": {"type": "array", "items": {"type": "string", "minLength": 1}},
"xml_like_risk": {"type": "string", "minLength": 1},
"content_density_contract": {"type": "string", "minLength": 1},
"risk_flags": {"type": "array", "items": {"type": "string"}},
"source_policy": {"type": "string", "minLength": 1},
"canvas_spec": {
"type": "object",
"required": ["version", "canvas", "safe_area", "template_id", "theme_id", "theme", "content", "semantic_elements", "quality_constraints"],
"additionalProperties": true
}
},
"additionalProperties": true
}
}
}
}

View File

@@ -0,0 +1,82 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://larksuite.local/svglide-canvas-spec.schema.json",
"title": "SVGlide CanvasSpec",
"type": "object",
"required": ["version", "canvas", "safe_area", "template_id", "theme_id", "theme", "content", "semantic_elements", "quality_constraints"],
"additionalProperties": true,
"properties": {
"version": {"const": "svglide-canvas-spec/v1"},
"canvas": {
"type": "object",
"required": ["width", "height", "viewBox"],
"additionalProperties": true,
"properties": {
"width": {"const": 960},
"height": {"const": 540},
"viewBox": {"const": "0 0 960 540"}
}
},
"template_id": {"type": "string", "minLength": 1},
"theme_id": {"type": "string", "minLength": 1},
"safe_area": {
"type": "object",
"required": ["x", "y", "width", "height"],
"additionalProperties": true,
"properties": {
"x": {"type": "number"},
"y": {"type": "number"},
"width": {"type": "number"},
"height": {"type": "number"}
}
},
"theme": {"type": "object", "additionalProperties": true},
"content": {"type": "object", "additionalProperties": true},
"semantic_elements": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"required": ["element_id", "kind", "role", "source_ref", "bbox"],
"additionalProperties": true,
"properties": {
"element_id": {"type": "string", "minLength": 1},
"kind": {"type": "string", "minLength": 1},
"role": {"type": "string", "minLength": 1},
"source_ref": {"type": "string", "minLength": 1},
"bbox": {
"type": "object",
"required": ["x", "y", "width", "height"],
"additionalProperties": true,
"properties": {
"x": {"type": "number"},
"y": {"type": "number"},
"width": {"type": "number"},
"height": {"type": "number"}
}
}
}
}
},
"quality_constraints": {
"type": "object",
"required": ["min_font_size", "safe_area"],
"additionalProperties": true,
"properties": {
"min_font_size": {"type": "number"},
"max_title_lines": {"type": "integer", "minimum": 1},
"safe_area": {
"type": "object",
"required": ["x", "y", "width", "height"],
"additionalProperties": true,
"properties": {
"x": {"type": "number"},
"y": {"type": "number"},
"width": {"type": "number"},
"height": {"type": "number"}
}
}
}
}
}
}

View File

@@ -0,0 +1,33 @@
{
"schema_version": "svglide-component-registry/v1",
"runtime_contract": {
"format": "Satori-compatible JSX/render tree",
"allowed_styles": ["static flex layout", "absolute positioning", "solid backgrounds", "font tokens", "bounded text boxes"],
"forbidden_styles": ["CSS animation", "filter effects", "external stylesheets", "arbitrary HTML passthrough"]
},
"components": [
{"id": "Title", "status": "active", "role": "primary text hierarchy", "source": "components/primitives.mjs"},
{"id": "Subtitle", "status": "active", "role": "secondary text hierarchy", "source": "components/primitives.mjs"},
{"id": "TextBlock", "status": "active", "role": "bounded text surface", "source": "components/primitives.mjs"},
{"id": "Badge", "status": "active", "role": "eyebrow or metadata label", "source": "components/primitives.mjs"},
{"id": "Chip", "status": "active", "role": "short categorical tag", "source": "components/primitives.mjs"},
{"id": "StatCard", "status": "active", "role": "numbered takeaway card", "source": "components/primitives.mjs"},
{"id": "ImageFrame", "status": "active", "role": "bounded visual placeholder", "source": "components/primitives.mjs"},
{"id": "MetricTile", "status": "active", "role": "dashboard metric block", "source": "templates/p0-templates.mjs"},
{"id": "TimelineNode", "status": "active", "role": "milestone marker", "source": "templates/p0-templates.mjs"},
{"id": "ProcessStep", "status": "active", "role": "pipeline step card", "source": "templates/p0-templates.mjs"},
{"id": "QuoteBlock", "status": "active", "role": "large quotation layout", "source": "templates/p0-templates.mjs"},
{"id": "RiskBanner", "status": "active", "role": "severity and risk grouping", "source": "templates/p0-templates.mjs"},
{"id": "RoadmapLane", "status": "active", "role": "horizontal roadmap row", "source": "templates/p0-templates.mjs"},
{"id": "ArchitectureNode", "status": "active", "role": "system node block", "source": "templates/p0-templates.mjs"},
{"id": "Callout", "status": "active", "role": "emphasis panel", "source": "templates/p0-templates.mjs"},
{"id": "SectionHeader", "status": "active", "role": "compact page heading cluster", "source": "templates/p0-templates.mjs"},
{"id": "FigurePlaceholder", "status": "active", "role": "image or diagram placeholder", "source": "templates/p0-templates.mjs"},
{"id": "LegendDot", "status": "active", "role": "comparison and chart marker", "source": "templates/p0-templates.mjs"},
{"id": "IndexNumber", "status": "active", "role": "ordered list number", "source": "templates/p0-templates.mjs"},
{"id": "DividerRule", "status": "active", "role": "section or timeline divider", "source": "templates/p0-templates.mjs"},
{"id": "PosterSection", "status": "active", "role": "research poster content block", "source": "templates/p0-templates.mjs"},
{"id": "BarMark", "status": "active", "role": "simple data story bar", "source": "templates/p0-templates.mjs"},
{"id": "AccentShape", "status": "active", "role": "bounded decorative visual signal", "source": "templates/p0-templates.mjs"}
]
}

View File

@@ -0,0 +1,54 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://larksuite.local/svglide-deck-plan.schema.json",
"title": "SVGlide Deck Planner Output",
"type": "object",
"additionalProperties": false,
"required": ["schema_version", "topic", "audience", "objective", "target_slide_count", "narrative_arc", "theme_direction", "constraints", "slides"],
"properties": {
"schema_version": {"const": "svglide-deck-plan/v1"},
"topic": {"type": "string", "minLength": 1},
"audience": {"type": "string", "minLength": 1},
"objective": {"type": "string", "minLength": 1},
"target_slide_count": {"type": "integer", "minimum": 1},
"narrative_arc": {"type": "array", "minItems": 3, "items": {"type": "string", "minLength": 1}},
"theme_direction": {
"type": "object",
"additionalProperties": false,
"required": ["preferred_theme_ids", "visual_identity", "tone"],
"properties": {
"preferred_theme_ids": {"type": "array", "minItems": 1, "items": {"type": "string", "minLength": 1}},
"visual_identity": {"type": "string", "minLength": 1},
"tone": {"type": "string", "minLength": 1}
}
},
"constraints": {
"type": "object",
"additionalProperties": false,
"required": ["generation_mode", "source_policy", "forbidden_outputs"],
"properties": {
"generation_mode": {"const": "artboard_satori"},
"source_policy": {"type": "string", "minLength": 1},
"forbidden_outputs": {"type": "array", "minItems": 4, "items": {"type": "string", "minLength": 1}}
}
},
"slides": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"additionalProperties": false,
"required": ["page", "title", "role", "key_message", "content_goal", "visual_goal", "allowed_template_ids"],
"properties": {
"page": {"type": "integer", "minimum": 1},
"title": {"type": "string", "minLength": 1},
"role": {"type": "string", "minLength": 1},
"key_message": {"type": "string", "minLength": 1},
"content_goal": {"type": "string", "minLength": 1},
"visual_goal": {"type": "string", "minLength": 1},
"allowed_template_ids": {"type": "array", "minItems": 1, "items": {"type": "string", "minLength": 1}}
}
}
}
}
}

View File

@@ -19,6 +19,7 @@
"stage": {"const": "generate_svg"},
"status": {"enum": ["passed", "failed"]},
"generator_mode": {"enum": ["script", "external"]},
"generation_mode": {"enum": ["direct_svg", "artboard_satori"]},
"generated_files": {
"type": "array",
"minItems": 1,
@@ -33,11 +34,47 @@
}
},
"page_receipts": {"type": "array", "minItems": 1, "items": {"type": "string", "minLength": 1}},
"artboard_receipts": {"type": "array", "minItems": 1, "items": {"type": "string", "minLength": 1}},
"artboard_additional_receipts": {"type": "array", "minItems": 3, "items": {"type": "string", "minLength": 1}},
"canvas_spec_validate": {"type": "string", "minLength": 1},
"artboard_render_receipt": {"type": "string", "minLength": 1},
"satori_bridge_receipt": {"type": "string", "minLength": 1},
"template_fit_check": {"type": "string", "minLength": 1},
"contact_sheet": {
"type": "object",
"required": ["path", "sha256"],
"additionalProperties": true,
"properties": {
"path": {"type": "string", "minLength": 1},
"sha256": {"type": "string", "minLength": 1}
}
},
"plan_sha256": {"type": "string", "minLength": 1},
"evidence_sha256": {"type": "string", "minLength": 1},
"asset_manifest_sha256": {"type": "string", "minLength": 1},
"source_receipt_sha256": {"type": "string", "minLength": 1},
"lock_sha256": {"type": ["string", "null"]},
"generator_script_sha256": {"type": ["string", "null"]}
}
},
"allOf": [
{
"if": {
"properties": {
"generation_mode": {"const": "artboard_satori"}
},
"required": ["generation_mode"]
},
"then": {
"required": [
"artboard_receipts",
"artboard_additional_receipts",
"canvas_spec_validate",
"artboard_render_receipt",
"satori_bridge_receipt",
"template_fit_check",
"contact_sheet"
]
}
}
]
}

View File

@@ -0,0 +1,15 @@
{
"schema_version": "svglide-layout-archetypes/v1",
"archetypes": [
{"id": "hero-cover", "status": "active", "templates": ["cover-hero"], "purpose": "deck opening page with one dominant message and controlled chips"},
{"id": "section-divider", "status": "active", "templates": ["section-title"], "purpose": "chapter transition and narrative pacing"},
{"id": "agenda-list", "status": "active", "templates": ["agenda-list"], "purpose": "ordered content map for multi-page decks"},
{"id": "two-column-comparison", "status": "active", "templates": ["comparison-cards"], "purpose": "before-after or option comparison"},
{"id": "timeline-horizontal", "status": "active", "templates": ["timeline-steps"], "purpose": "time or milestone sequence"},
{"id": "process-flow", "status": "active", "templates": ["process-flow"], "purpose": "pipeline and dependency explanation"},
{"id": "metric-dashboard", "status": "active", "templates": ["metric-dashboard", "data-story"], "purpose": "small set of quantitative highlights"},
{"id": "image-text-split", "status": "active", "templates": ["image-feature"], "purpose": "one visual anchor with supporting interpretation"},
{"id": "research-poster-3col", "status": "active", "templates": ["research-poster"], "purpose": "dense research summary with balanced sections"},
{"id": "architecture-blueprint", "status": "active", "templates": ["architecture-blueprint", "roadmap-lanes", "risk-alert", "quote-focus", "summary-final"], "purpose": "systems, plans, risks, quotes, and closing pages"}
]
}

View File

@@ -0,0 +1,38 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://larksuite.local/svglide-node-layout-map.schema.json",
"title": "SVGlide node layout map",
"type": "object",
"required": ["version", "page", "source", "drift", "nodes"],
"additionalProperties": true,
"properties": {
"version": {"const": "svglide-node-layout-map/v1"},
"page": {"type": "integer", "minimum": 1},
"source": {"type": "string", "minLength": 1},
"drift": {
"type": "object",
"required": ["status", "max_px"],
"additionalProperties": true,
"properties": {
"status": {"type": "string", "minLength": 1},
"max_px": {"type": "number"}
}
},
"nodes": {
"type": "array",
"items": {
"type": "object",
"required": ["id", "kind", "x", "y", "width", "height"],
"additionalProperties": true,
"properties": {
"id": {"type": "string", "minLength": 1},
"kind": {"type": "string", "minLength": 1},
"x": {"type": "number"},
"y": {"type": "number"},
"width": {"type": "number"},
"height": {"type": "number"}
}
}
}
}
}

View File

@@ -0,0 +1,516 @@
# SVGlide 新西兰风光本地预览复盘与改进计划
## 背景
本次验证目标是基于主题“新西兰风光”生成一份可本地预览的 SVGlide deck不做简单冒烟而是尽量产出一套能体现真实视觉水平的多页结果。
最终产物为 5 页 SVG-native deck
1. 封面:新西兰风光
2. 南岛:雪山、峡湾与冰川湖
3. 北岛:火山、地热与海岸
4. 7 日风光路线节奏
5. 四个风景关键词收束
最终链路通过到 `dry_run`
```text
plan
-> assets
-> generate_svg
-> prepare
-> preview
-> preflight
-> preview_lint
-> aesthetic_review
-> chart_verify
-> semantic_review
-> runtime_review
-> visual_distinctness_review
-> quality_gate
-> dry_run
```
本地项目路径:
```text
.tmp/nz-scenery-svglide
```
关键报告:
```text
.tmp/nz-scenery-svglide/06-check/preflight.json
.tmp/nz-scenery-svglide/06-check/preview-lint.json
.tmp/nz-scenery-svglide/06-check/semantic-review.json
.tmp/nz-scenery-svglide/06-check/runtime-review.json
.tmp/nz-scenery-svglide/06-check/quality-gate.json
.tmp/nz-scenery-svglide/receipts/dry_run.json
```
## 结论
这次耗时偏长的主因不是 runner 执行慢,而是:
```text
缺少模板化生成能力
+ 缺少自动契约补齐
+ 缺少门禁自动修复
+ 在本地探索阶段跑了接近 production 的严格门禁
```
最终 runner 实际执行耗时是秒级到几十秒级;主要时间花在人工写 SVG、人工调版式、人工补 `slide_plan.json`、人工补项目级 `renderer-registry.json`、手工处理状态重跑。
## 时间拆解
| 阶段 | 现象 | 结论 |
| --- | --- | --- |
| 项目初始化 | `init` 在 20:54:19 完成 | 很快,不是瓶颈 |
| source 准备 | `source` 在 20:57:14 完成 | 主要是人工写 source notes/evidence |
| SVG 生成脚本编写 | 手写 5 页 direct SVG generator | 最大耗时来源 |
| preview/preflight 修复 | 被文本高度、卡片容器、文本重叠打回 | 模板没有文本预算和布局约束 |
| semantic review 修复 | 可见文字没有完整追溯到 plan/source | 计划层和产物层没有自动对齐 |
| runtime review 修复 | 自定义 renderer_id 未登记 | 缺少项目级 renderer registry 自动生成 |
| final dry_run | `quality_gate``dry_run` 最终 passed | 链路可以跑通,但前置人工摩擦太多 |
| 本地视觉截图 | 环境缺 Playwright/resvg/CairoSVG最后用 QuickLook | 预览渲染工具链不稳定 |
## 具体卡点
### 1. `preview_lint` 对文本框高度敏感
失败表现:
```text
preview_text_overflow_risk
preview_big_number_box_tight
```
触发页面:
- 第 1 页:`南岛``北岛``路线`
- 第 4 页:标题和阶段标签
- 第 5 页:标题、关键词、说明文本
根因:
手写 SVG 时没有统一的文本预算函数,只是凭视觉估计设置 `foreignObject``width/height`。门禁按保守行高判断后,短文本也可能被判为有溢出风险。
判断:
规则本身不应删除,因为 PPT 导出后文本被裁切是严重问题。但探索阶段不应要求人工逐个调框。
改进:
- 模板层内置 `fitTextBox()` 预算。
- 所有 text component 默认给足 `line-height * lines + padding`
- `preview_lint``draft_preview` profile 下将轻微高度不足降级为 warning。
- runner 增加自动修复建议或自动扩高能力。
### 2. `preflight` 拦截了第 5 页卡片文本重叠
失败表现:
```text
text_bbox_overlap
text_container_overflow
```
根因:
为了修 `preview_lint`,人工增大了关键词卡片内文本框高度,但没有同步增大卡片容器,也没有重新计算卡片内部 y 坐标。
判断:
这类门禁不算太紧。文本重叠和容器越界在真实 PPT 中会直接翻车,应该阻断。
改进:
- 卡片模板必须用布局函数生成,而不是人工坐标。
- 卡片容器高度由内容反推。
- 文本变更后自动触发布局重算。
- `preflight` 输出更精确的元素位置和最近容器,减少人工定位时间。
### 3. `semantic_review` 要求可见文字追溯到 plan/source
失败表现:
```text
visible_text_not_in_plan_or_source
body_point_not_chinese
content_body_points_too_many
```
根因:
SVG 中出现了大量设计文案和英文页签:
```text
SCENIC ATLAS
SOUTH
NORTH
ROUTE
TAKEAWAY
Day 1
Tekapo 蓝
SVGlide 本地预览...
```
这些文案没有在 `slide_plan.json` 或 source evidence 中完整登记。
判断:
规则方向是对的。PPT 生成链路需要保证“计划层负责内容,渲染层不私自发明业务文案”。但当前执行方式太依赖人工。
改进:
- Slide Planner 需要输出 `visible_text` 字段。
- Template Renderer 需要把实际渲染文本反写为 text inventory。
- semantic review 应比较 `planned_visible_text``rendered_visible_text`,而不是让人手工补。
- 英文装饰标签应有明确字段,例如 `decorative_labels`,并允许白名单。
- 对纯设计辅助文案区分 `content_text``decorative_text`
### 4. 修改 plan 后 receipt/hash 级联失效
失败表现:
```text
plan confirmation plan_sha256 does not match current plan
assets manifest plan_sha256 does not match current project files
```
根因:
为了修 semantic review 修改了 `slide_plan.json`,但 `plan-confirmation.json`、assets manifest、generate receipt 等下游文件仍指向旧 hash。
判断:
hash 校验是必要的,但当前恢复方式太手工。
改进:
- runner 增加 `reset --from <stage>`
- runner 增加 `rerun --from <stage> --until <stage>`
- 当 plan hash 改变时,自动标记下游 stage stale。
- 对本地预览提供 `--auto-confirm-plan`,仅限非 live create。
- state 文件不要靠人工 patch。
### 5. `runtime_review` 要求 renderer_id 登记
失败表现:
```text
renderer_unknown
```
触发 renderer
```text
nz_scenic_cover
nz_south_island_panels
nz_north_island_map
nz_route_timeline
nz_closing_keywords
```
根因:
为了让这套 deck 的页面类型更语义化,使用了项目自定义 `renderer_id`,但一开始没有生成项目级:
```text
02-plan/renderer-registry.json
```
判断:
这个规则也不应删除。renderer registry 是后续运行时可追踪、模板资产可治理的基础。
改进:
- Slide Planner 选模板时自动生成项目级 renderer registry。
- Template Registry 中的模板 id 自动映射到 renderer id。
- 对 direct SVG prototype 支持临时 renderer registry。
- `runtime_review` 在报错时输出建议 registry stub。
### 6. 本地预览渲染工具链不稳定
失败表现:
- node_repl 的 Playwright 调用失败。
- 本地没有 `playwright`
- 本地没有 `rsvg-convert`
- 本地没有 `cairosvg`
- `sips` 不能直接转换当前 SVG。
- 最后使用 macOS QuickLook `qlmanage` 生成缩略图。
判断:
这不是主链路的核心问题,但严重影响“让我看看实际效果”的体验。
改进:
- CLI/skill 内置稳定预览截图工具。
- 优先选项Playwright 渲染 `preview.html`
- SVG 单页截图选项resvg。
- 产物应自动生成:
```text
05-preview/preview.html
05-preview/contact-sheet.png
05-preview/page-001.png
...
```
## 门禁是否太紧
不是简单的“太紧”。
更准确的判断:
```text
production 门禁是合理的
但不应该直接用于 draft preview 的人工探索阶段
```
建议分三档:
### `draft_preview`
用于本地快速看效果。
只阻断:
- SVG 无法解析
- 页面空白
- 文本严重重叠
- 关键元素明显出画布
- preview.html 无法生成
降级为 warning
- 轻微文本框高度不足
- 装饰元素轻微越过 safe area
- 文案未完全补齐 text inventory
- renderer registry 缺失但可推断
### `review_preview`
用于准备给人评审。
阻断:
- 文本溢出风险
- 容器越界
- 页面视觉重复度过高
- 语义和计划明显不一致
- renderer_id 未登记
允许 warning
- 非核心装饰标签未完全追溯
- 少量安全区装饰 warning
### `production_live`
用于真实创建线上 Slides。
阻断全部关键门禁:
- generator receipt
- preflight
- preview_lint
- aesthetic_review
- semantic_review
- runtime_review
- visual_distinctness_review
- quality_gate
- dry_run
## 根因归纳
本次慢点可以归纳为四类:
### 1. 设计生成没有模板系统
现在实际是“人工画 5 页”,不是“主题输入后模板系统生成 5 页”。
缺失能力:
- Template Registry
- Theme Token
- CanvasSpec
- Satori-compatible / SVG-native component library
- 文本自动布局
- 页面类型自动映射
### 2. 计划层和渲染层没有自动闭环
渲染层实际出现的可见文本、renderer id、layout family、装饰标签都需要回到计划层或 registry 中。但现在是人工补。
缺失能力:
- visible_text 自动生成
- text inventory 自动比对
- renderer registry 自动生成
- plan hash 级联失效管理
### 3. 门禁只有拦截,缺少自动修复
当前门禁能发现问题,但不会给 runner 一个可执行修复动作。
缺失能力:
- 自动扩高 text box
- 自动增大 card container
- 自动移动重叠文本
- 自动生成 registry stub
- 自动重跑 stale stages
### 4. 本地预览体验没有产品化
产物存在,但“快速看效果”还依赖手工打开 HTML、手工截图、手工拼图。
缺失能力:
- 自动打开预览
- 自动生成 contact sheet
- 自动截图
- preview report 中显示封面/缩略图
## 改进计划
### P0减少同类返工
目标:下一次同类 5 页本地预览不再因为文本高度、registry、hash 手工处理卡住。
任务:
1. runner 增加 `reset --from <stage>`
2. runner 增加 `--auto-confirm-plan`,仅允许非 live 阶段使用。
3. direct_svg/artboard 生成时自动写 `visible_text` 或 text inventory。
4. 生成项目级 `02-plan/renderer-registry.json`
5. preview_lint 对 `draft_preview` 支持轻微文本高度 warning。
6. preflight 报告补充 bbox 坐标和最近容器信息。
验收:
```text
主题输入后,人工只改一次 generator 或模板runner 能自动从 stale stage 续跑到 dry_run。
```
### P1模板化常见页面
目标:不要再手写 5 页 direct SVG。
先做 5 个模板:
```text
cover_hero
three_scenic_cards
map_and_notes
route_timeline
keyword_wall
```
每个模板必须内置:
- schema 输入
- theme token
- text budget
- safe area
- visible_text 输出
- renderer registry metadata
- preview fixture
验收:
```text
输入主题“新西兰风光”后5 页 deck 由模板组合生成,人工不需要逐页调坐标。
```
### P2质量门禁 profile 化
目标:本地探索和线上创建使用不同严格度。
新增或明确三档:
```text
draft_preview
review_preview
production_live
```
验收:
```text
draft_preview 能在不牺牲严重质量底线的前提下快速给出可看预览;
production_live 仍保持严格创建标准。
```
### P3自动修复闭环
目标:门禁发现的问题可以被 runner 自动修复一部分。
优先自动修:
- text box height
- card container height
- renderer registry stub
- visible_text 补全建议
- stale receipt reset
验收:
```text
preview_lint/preflight 常见问题不需要人工 patch SVG 坐标。
```
### P4本地预览产品化
目标:跑完后自动给用户可视化结果。
产物:
```text
05-preview/preview.html
05-preview/contact-sheet.png
05-preview/page-001.png
05-preview/page-002.png
...
```
实现建议:
- HTML 截图Playwright
- SVG 单页截图resvg
- fallbackQuickLook 仅作为 macOS fallback不作为主链路
验收:
```text
runner dry_run 后自动输出 contact sheet用户无需手工打开浏览器也能先看一眼。
```
## 建议的目标耗时
| 阶段 | 当前体感 | 改进后目标 |
| --- | --- | --- |
| 主题到 draft preview | 10-20 分钟 | 1-3 分钟 |
| draft preview 到 review preview | 多轮人工修 | 3-5 分钟 |
| review preview 到 production dry_run | 手工补契约 | 1 分钟以内 |
| 单次 runner 重跑 | 秒级到几十秒 | 秒级到几十秒 |
## 下一步建议
优先级最高的不是放松所有门禁,而是把“探索生成”和“线上创建”分层。
建议先做:
```text
1. runner reset/rerun 能力
2. draft_preview profile
3. 自动 visible_text
4. 自动项目级 renderer-registry
5. 5 个基础模板
```
做完这 5 件事后,同样的“新西兰风光 5 页预览”应该可以从人工 10-20 分钟降到 1-3 分钟,并且不会牺牲最终 production_live 的质量门禁。

View File

@@ -0,0 +1,225 @@
{
"schema_version": "svglide-source-intake/v1",
"policy": {
"runtime_import": "forbidden",
"usage": "external repositories are inspiration and measurement sources only; runtime output must be CanvasSpec plus in-repo Satori-compatible templates",
"conversion_path": "source example -> visual archetype -> CanvasSpec schema fields -> Template Registry -> Theme Token",
"acceptance_gate": "a source family is counted only when it has required intake fields and at least one conversion record pointing to owned registry output and golden CanvasSpec fixtures"
},
"sources": [
{
"id": "open-design-html-ppt",
"source_path": "/Users/bytedance/bd-projects/open-design/design-templates",
"source_type": "html_css_examples",
"source_hash_or_version": "git:2aadac07c",
"license_or_provenance": "origin https://github.com/nexu-io/open-design.git; local LICENSE is Apache-2.0; used as inspiration only",
"extract_fields": ["palette", "typography", "mood", "density", "occasion", "layout skeleton", "component combinations"],
"conversion_target": ["Theme Token", "Canvas Template", "Satori component candidate", "Layout Archetype"],
"acceptance_rule": "no source HTML/CSS enters runtime; every adopted pattern must map to a registry id and at least one golden CanvasSpec fixture",
"forbidden_usage": ["direct HTML runtime import", "arbitrary CSS passthrough", "client-side animation", "external stylesheet dependency"],
"verified_examples": [
"/Users/bytedance/bd-projects/open-design/design-templates/html-ppt-weekly-report",
"/Users/bytedance/bd-projects/open-design/design-templates/html-ppt-product-launch"
],
"conversion_records": [
{
"source_examples": ["html-ppt-weekly-report"],
"abstraction_record": "dense report pages with compact headers, numbered rows, and metric grouping",
"canvas_spec_fields": ["content.items", "content.metrics", "semantic_elements.title", "quality_constraints.safe_area"],
"registry_output": {
"templates": ["agenda-list", "metric-dashboard"],
"themes": ["finance-dark", "swiss-red"],
"components": ["IndexNumber", "MetricTile", "SectionHeader"],
"layouts": ["agenda-list", "metric-dashboard"],
"golden_fixtures": ["agenda-list.canvas-spec.json", "metric-dashboard.canvas-spec.json"]
},
"acceptance_rule": "fixture validates required content, max_items, text_budget, safe_area, and renders through Satori/resvg"
},
{
"source_examples": ["html-ppt-product-launch"],
"abstraction_record": "launch narrative pages with hero, image-text split, and short supporting points",
"canvas_spec_fields": ["content.title", "content.subtitle", "content.points", "content.caption"],
"registry_output": {
"templates": ["cover-hero", "image-feature", "summary-final"],
"themes": ["glass-neon", "dark-clarity"],
"components": ["Chip", "ImageFrame", "FigurePlaceholder", "Callout"],
"layouts": ["hero-cover", "image-text-split"],
"golden_fixtures": ["cover-hero.canvas-spec.json", "image-feature.canvas-spec.json", "summary-final.canvas-spec.json"]
},
"acceptance_rule": "image is represented as a bounded placeholder in CanvasSpec; no source DOM or CSS is copied"
}
]
},
{
"id": "open-design-retro-quarterly-review",
"source_path": "/Users/bytedance/bd-projects/open-design/skills/html-ppt-retro-quarterly-review",
"source_type": "html_css_case_study",
"source_hash_or_version": "git:2aadac07c",
"license_or_provenance": "origin https://github.com/nexu-io/open-design.git; local LICENSE is Apache-2.0; used as inspiration only",
"extract_fields": ["report structure", "metric hierarchy", "risk sectioning", "timeline rhythm", "executive density"],
"conversion_target": ["Canvas Template", "Component Variant", "Layout Archetype"],
"acceptance_rule": "patterns must become bounded CanvasSpec arrays with max_items/text_budget checks and golden fixtures",
"forbidden_usage": ["runtime dependency", "unbounded responsive layout", "copied JS interaction", "copied CSS animation"],
"verified_examples": [
"/Users/bytedance/bd-projects/open-design/skills/html-ppt-retro-quarterly-review"
],
"conversion_records": [
{
"source_examples": ["html-ppt-retro-quarterly-review"],
"abstraction_record": "quarterly review flow with metric overview, timeline, risk callout, and closing summary",
"canvas_spec_fields": ["content.metrics", "content.events", "content.risks", "content.takeaways"],
"registry_output": {
"templates": ["metric-dashboard", "timeline-steps", "risk-alert", "summary-final"],
"themes": ["finance-dark", "warm-editorial"],
"components": ["MetricTile", "TimelineNode", "RiskBanner", "StatCard"],
"layouts": ["metric-dashboard", "timeline-horizontal", "architecture-blueprint"],
"golden_fixtures": ["metric-dashboard.canvas-spec.json", "timeline-steps.canvas-spec.json", "risk-alert.canvas-spec.json", "summary-final.canvas-spec.json"]
},
"acceptance_rule": "all sections use arrays with explicit max_items and render in the 15-page golden regression"
}
]
},
{
"id": "open-design-zhangzara",
"source_path": "/Users/bytedance/bd-projects/open-design/design-templates/html-ppt-zhangzara-*",
"source_type": "design_templates",
"source_hash_or_version": "git:2aadac07c",
"license_or_provenance": "origin https://github.com/nexu-io/open-design.git; local LICENSE is Apache-2.0; used as inspiration only",
"extract_fields": ["grid system", "editorial color balance", "paper texture metaphor", "display/body contrast", "card spacing"],
"conversion_target": ["Theme Token", "Layout Archetype", "Canvas Template"],
"acceptance_rule": "dedupe strong-style packs into reusable Satori-compatible tokens and fixtures; do not count duplicate style variants",
"forbidden_usage": ["copying generated markup", "uncontrolled visual effects", "blindly copying all style packs", "texture/image runtime dependency"],
"verified_examples": [
"/Users/bytedance/bd-projects/open-design/design-templates/html-ppt-zhangzara-editorial-tri-tone",
"/Users/bytedance/bd-projects/open-design/design-templates/html-ppt-zhangzara-cartesian",
"/Users/bytedance/bd-projects/open-design/design-templates/html-ppt-zhangzara-pin-and-paper"
],
"conversion_records": [
{
"source_examples": ["html-ppt-zhangzara-editorial-tri-tone"],
"abstraction_record": "editorial tri-tone palette and strong asymmetrical section page",
"canvas_spec_fields": ["content.eyebrow", "content.title", "content.subtitle"],
"registry_output": {
"templates": ["section-title", "image-feature"],
"themes": ["editorial-tritone", "paper-research"],
"components": ["SectionHeader", "DividerRule", "FigurePlaceholder"],
"layouts": ["section-divider", "image-text-split"],
"golden_fixtures": ["section-title.canvas-spec.json", "image-feature.canvas-spec.json"]
},
"acceptance_rule": "theme colors are explicit hex tokens and fixture text remains within text_budget"
},
{
"source_examples": ["html-ppt-zhangzara-cartesian", "html-ppt-zhangzara-pin-and-paper"],
"abstraction_record": "grid and paper-note discipline converted to bounded rows, lanes, and blueprint nodes",
"canvas_spec_fields": ["content.lanes", "content.nodes", "semantic_elements.title"],
"registry_output": {
"templates": ["roadmap-lanes", "architecture-blueprint"],
"themes": ["swiss-red", "blueprint-technical"],
"components": ["RoadmapLane", "ArchitectureNode", "DividerRule"],
"layouts": ["architecture-blueprint"],
"golden_fixtures": ["roadmap-lanes.canvas-spec.json", "architecture-blueprint.canvas-spec.json"]
},
"acceptance_rule": "roadmap and blueprint output remains editable SVG shapes/text, not rasterized source art"
}
]
},
{
"id": "ppt-master-examples",
"source_path": "/Users/bytedance/bd-projects/workspaces/SVGlide/ppt-master/examples",
"source_type": "pptx_svg_examples",
"source_hash_or_version": "git:45d9a79",
"license_or_provenance": "origin https://github.com/hugohe3/ppt-master.git; local LICENSE is MIT; inspiration-only with no copied SVG/runtime dependency",
"extract_fields": ["deck rhythm", "page archetypes", "visual style rules", "palette rules", "element density", "bbox/font-size facts"],
"conversion_target": ["Canvas Template", "Theme Token", "Golden Fixture", "Quality Rule"],
"acceptance_rule": "ppt-master SVG may inform measurements but must not be emitted as final SVGlide output or imported at runtime",
"forbidden_usage": ["direct SVG runtime import", "PPT generation workflow import", "copying svg_final as live SVG", "adding ppt-master as CLI dependency"],
"verified_examples": [
"/Users/bytedance/bd-projects/workspaces/SVGlide/ppt-master/examples/ppt169_attention_is_all_you_need/design_spec.md",
"/Users/bytedance/bd-projects/workspaces/SVGlide/ppt-master/examples/ppt169_swiss_grid_systems/design_spec.md",
"/Users/bytedance/bd-projects/workspaces/SVGlide/ppt-master/examples/ppt169_glassmorphism_demo",
"/Users/bytedance/bd-projects/workspaces/SVGlide/ppt-master/examples/ppt169_kubernetes_blueprint_2026/design_spec.md",
"/Users/bytedance/bd-projects/workspaces/SVGlide/ppt-master/examples/ppt169_brutalist_ai_newspaper_2026/design_spec.md"
],
"conversion_records": [
{
"source_examples": ["ppt169_attention_is_all_you_need", "ppt169_kubernetes_blueprint_2026"],
"abstraction_record": "technical narrative pages with architecture nodes, process stages, and research framing",
"canvas_spec_fields": ["content.steps", "content.nodes", "content.sections", "semantic_elements.title"],
"registry_output": {
"templates": ["process-flow", "architecture-blueprint", "research-poster"],
"themes": ["blueprint-technical", "cobalt-grid"],
"components": ["ProcessStep", "ArchitectureNode", "PosterSection"],
"layouts": ["process-flow", "architecture-blueprint", "research-poster-3col"],
"golden_fixtures": ["process-flow.canvas-spec.json", "architecture-blueprint.canvas-spec.json", "research-poster.canvas-spec.json"]
},
"acceptance_rule": "all outputs are owned templates and CanvasSpec fixtures; raw ppt-master SVG is preview reference only"
},
{
"source_examples": ["ppt169_swiss_grid_systems", "ppt169_glassmorphism_demo", "ppt169_brutalist_ai_newspaper_2026"],
"abstraction_record": "Swiss grid, glass neon contrast, and editorial newspaper density distilled into reusable theme tokens and comparison/data layouts",
"canvas_spec_fields": ["content.left_points", "content.right_points", "content.metrics", "content.callout"],
"registry_output": {
"templates": ["comparison-cards", "data-story", "quote-focus"],
"themes": ["swiss-red", "glass-neon", "editorial-tritone"],
"components": ["LegendDot", "BarMark", "QuoteBlock", "Callout"],
"layouts": ["two-column-comparison", "metric-dashboard"],
"golden_fixtures": ["comparison-cards.canvas-spec.json", "data-story.canvas-spec.json", "quote-focus.canvas-spec.json"]
},
"acceptance_rule": "fixture content is short enough for text_budget and renderer uses Satori-compatible static style only"
}
]
},
{
"id": "postergen",
"source_path": "/Users/bytedance/bd-projects/workspaces/SVGlide/PosterGen",
"source_type": "poster_generation_rules",
"source_hash_or_version": "git:8a54325",
"license_or_provenance": "origin https://github.com/Y-Research-SBU/PosterGen.git; local LICENSE is MIT; used as inspiration only",
"extract_fields": ["three-column poster", "title area ratio", "section balance", "logo-aware palette", "font-size constraints", "text-height measurement"],
"conversion_target": ["Research Poster Template", "Quality Rule", "Layout Archetype", "Theme Token"],
"acceptance_rule": "poster heuristics must be encoded as bounded sections, max_items, text_budget, and golden fixture render checks",
"forbidden_usage": ["LangGraph workflow runtime", "Python poster agents as production dependency", "GPT image generation workflow import", "web UI runtime import"],
"verified_examples": [
"/Users/bytedance/bd-projects/workspaces/SVGlide/PosterGen/config/poster_config.yaml",
"/Users/bytedance/bd-projects/workspaces/SVGlide/PosterGen/config/prompts/layout_balancer.txt",
"/Users/bytedance/bd-projects/workspaces/SVGlide/PosterGen/config/prompts/extract_color_from_figure.txt",
"/Users/bytedance/bd-projects/workspaces/SVGlide/PosterGen/config/prompts/spatial_content_planner.txt"
],
"conversion_records": [
{
"source_examples": ["config/poster_config.yaml", "layout_balancer.txt", "spatial_content_planner.txt"],
"abstraction_record": "research poster with three bounded columns, title/authors area, balanced sections, and explicit section max count",
"canvas_spec_fields": ["content.title", "content.authors", "content.sections", "content.key_visual"],
"registry_output": {
"templates": ["research-poster"],
"themes": ["paper-research"],
"components": ["PosterSection", "FigurePlaceholder", "SectionHeader"],
"layouts": ["research-poster-3col"],
"golden_fixtures": ["research-poster.canvas-spec.json"]
},
"acceptance_rule": "sections max at 6, title/authors are explicit fields, and the fixture renders through the golden regression"
},
{
"source_examples": ["extract_color_from_figure.txt"],
"abstraction_record": "color extraction idea converted to fixed theme-token selection rather than arbitrary color generation",
"canvas_spec_fields": ["theme_id", "theme.colors"],
"registry_output": {
"templates": ["image-feature", "data-story"],
"themes": ["paper-research", "glass-neon"],
"components": ["FigurePlaceholder", "BarMark"],
"layouts": ["image-text-split", "metric-dashboard"],
"golden_fixtures": ["image-feature.canvas-spec.json", "data-story.canvas-spec.json"]
},
"acceptance_rule": "LLM/planner must choose a registered theme_id; arbitrary CSS colors are not accepted as runtime style"
}
]
}
],
"p1_abstractions": {
"template_count": 15,
"theme_count": 10,
"layout_archetype_count": 10,
"component_variant_count": 23,
"golden_fixture_count": 15
}
}

View File

@@ -7,6 +7,7 @@
"properties": {
"route": {"const": "svglide-svg"},
"output_mode": {"const": "svglide-svg"},
"generation_mode": {"enum": ["direct_svg", "artboard_satori"]},
"page_count": {"type": "integer", "minimum": 1},
"target_slide_count": {"type": "integer", "minimum": 1},
"fallback_policy": {"enum": ["strict-native", "auto"]},
@@ -144,6 +145,10 @@
"content_density_contract": {"type": "string", "minLength": 1},
"risk_flags": {"type": "array", "items": {"type": "string"}},
"source_policy": {"type": "string", "minLength": 1},
"canvas_spec": {
"type": "object",
"additionalProperties": true
},
"asset_contract": {}
},
"additionalProperties": true
@@ -163,5 +168,112 @@
"anyOf": [
{"required": ["route"]},
{"required": ["output_mode"]}
],
"allOf": [
{
"if": {
"properties": {
"generation_mode": {"const": "artboard_satori"}
},
"required": ["generation_mode"]
},
"then": {
"properties": {
"slides": {
"items": {
"required": ["canvas_spec"],
"properties": {
"canvas_spec": {
"type": "object",
"required": ["version", "canvas", "safe_area", "template_id", "theme_id", "theme", "content", "semantic_elements", "quality_constraints"],
"properties": {
"version": {"const": "svglide-canvas-spec/v1"},
"canvas": {
"type": "object",
"required": ["width", "height", "viewBox"],
"properties": {
"width": {"const": 960},
"height": {"const": 540},
"viewBox": {"const": "0 0 960 540"}
},
"additionalProperties": true
},
"template_id": {"type": "string", "minLength": 1},
"theme_id": {"type": "string", "minLength": 1},
"safe_area": {
"type": "object",
"required": ["x", "y", "width", "height"],
"properties": {
"x": {"type": "number"},
"y": {"type": "number"},
"width": {"type": "number"},
"height": {"type": "number"}
},
"additionalProperties": true
},
"theme": {"type": "object"},
"content": {
"type": "object",
"required": ["title"],
"properties": {
"title": {"type": "string", "minLength": 1}
},
"additionalProperties": true
},
"semantic_elements": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"required": ["element_id", "kind", "role", "source_ref", "bbox"],
"properties": {
"element_id": {"type": "string", "minLength": 1},
"kind": {"type": "string", "minLength": 1},
"role": {"type": "string", "minLength": 1},
"source_ref": {"type": "string", "minLength": 1},
"bbox": {
"type": "object",
"required": ["x", "y", "width", "height"],
"properties": {
"x": {"type": "number"},
"y": {"type": "number"},
"width": {"type": "number"},
"height": {"type": "number"}
},
"additionalProperties": true
}
},
"additionalProperties": true
}
},
"quality_constraints": {
"type": "object",
"required": ["min_font_size", "safe_area"],
"properties": {
"min_font_size": {"type": "number"},
"max_title_lines": {"type": "integer", "minimum": 1},
"safe_area": {
"type": "object",
"required": ["x", "y", "width", "height"],
"properties": {
"x": {"type": "number"},
"y": {"type": "number"},
"width": {"type": "number"},
"height": {"type": "number"}
},
"additionalProperties": true
}
},
"additionalProperties": true
}
},
"additionalProperties": true
}
}
}
}
}
}
}
]
}

View File

@@ -0,0 +1,45 @@
{
"schema_version": "svglide-planner-prompt-contracts/v1",
"contracts": [
{
"id": "deck-planner",
"stage": "deck_planner",
"prompt_path": "skills/lark-slides/prompts/svglide/deck-planner.prompt.md",
"input_bundle": ["user_topic", "audience", "target_slide_count", "source_policy", "available_template_registry", "available_theme_registry"],
"output_schema": "skills/lark-slides/references/svglide-deck-plan.schema.json",
"output_path": "02-plan/deck-plan.json",
"validation_command": "python3 skills/lark-slides/scripts/svglide_planner_contracts.py <project>",
"forbidden_outputs": ["free_html", "free_css", "free_svg", "jsx_or_tsx", "markdown_fence", "base64_image_data"]
},
{
"id": "slide-planner",
"stage": "slide_planner",
"prompt_path": "skills/lark-slides/prompts/svglide/slide-planner.prompt.md",
"input_bundle": ["deck-plan.json", "svglide-template-registry.json", "themes/registry.json", "svglide-layout-archetypes.json", "svglide-component-registry.json"],
"output_schema": "skills/lark-slides/references/svglide-slide-plan.schema.json",
"output_path": "02-plan/slide-plan.json",
"validation_command": "python3 skills/lark-slides/scripts/svglide_planner_contracts.py <project>",
"forbidden_outputs": ["free_html", "free_css", "free_svg", "jsx_or_tsx", "markdown_fence", "unregistered_template_id"]
},
{
"id": "canvas-planner",
"stage": "canvas_planner",
"prompt_path": "skills/lark-slides/prompts/svglide/canvas-planner.prompt.md",
"input_bundle": ["slide-plan.json", "svglide-template-registry.json", "themes/registry.json", "svglide-canvas-spec.schema.json", "golden CanvasSpec examples"],
"output_schema": "skills/lark-slides/references/svglide-canvas-plan.schema.json",
"output_path": "02-plan/slide_plan.json",
"validation_command": "python3 skills/lark-slides/scripts/svglide_planner_contracts.py <project>",
"forbidden_outputs": ["free_html", "free_css", "free_svg", "jsx_or_tsx", "markdown_fence", "raw_satori_svg"]
},
{
"id": "repair-planner",
"stage": "repair_planner",
"prompt_path": "skills/lark-slides/prompts/svglide/repair-planner.prompt.md",
"input_bundle": ["validation receipt", "target planner JSON", "schema issue list", "template fit issue list"],
"output_schema": "skills/lark-slides/references/svglide-repair-plan.schema.json",
"output_path": "02-plan/repair-plan.json",
"validation_command": "python3 skills/lark-slides/scripts/svglide_planner_contracts.py <project>",
"forbidden_outputs": ["full_deck_rewrite", "free_html", "free_css", "free_svg", "markdown_fence", "unscoped_patch"]
}
]
}

View File

@@ -15,6 +15,111 @@
"runtime_module": "svglide_gen_runtime.py",
"allowed_style_presets": ["*"]
},
{
"id": "artboard_cover_hero",
"status": "active",
"family": "cover",
"runtime_module": "svglide_artboard_renderer.py",
"allowed_style_presets": ["*"]
},
{
"id": "artboard_comparison",
"status": "active",
"family": "comparison",
"runtime_module": "svglide_artboard_renderer.py",
"allowed_style_presets": ["*"]
},
{
"id": "artboard_section_title",
"status": "active",
"family": "closing",
"runtime_module": "svglide_artboard_renderer.py",
"allowed_style_presets": ["*"]
},
{
"id": "artboard_satori.cover-hero",
"status": "active",
"family": "cover",
"runtime_module": "svglide_artboard_renderer.py",
"allowed_style_presets": ["*"]
},
{
"id": "artboard_satori.comparison-cards",
"status": "active",
"family": "comparison",
"runtime_module": "svglide_artboard_renderer.py",
"allowed_style_presets": ["*"]
},
{
"id": "artboard_satori.image-feature",
"status": "active",
"family": "image_text",
"runtime_module": "svglide_artboard_renderer.py",
"allowed_style_presets": ["*"]
},
{
"id": "artboard_satori.data-story",
"status": "active",
"family": "data_story",
"runtime_module": "svglide_artboard_renderer.py",
"allowed_style_presets": ["*"]
},
{
"id": "artboard_satori.summary-final",
"status": "active",
"family": "closing",
"runtime_module": "svglide_artboard_renderer.py",
"allowed_style_presets": ["*"]
},
{
"id": "artboard_satori.agenda-list",
"status": "active",
"family": "agenda",
"runtime_module": "svglide_artboard_renderer.py",
"allowed_style_presets": ["*"]
},
{
"id": "artboard_satori.timeline-steps",
"status": "active",
"family": "timeline",
"runtime_module": "svglide_artboard_renderer.py",
"allowed_style_presets": ["*"]
},
{
"id": "artboard_satori.architecture-blueprint",
"status": "active",
"family": "architecture",
"runtime_module": "svglide_artboard_renderer.py",
"allowed_style_presets": ["*"]
},
{
"id": "artboard_satori.process-flow",
"status": "active",
"family": "process",
"runtime_module": "svglide_artboard_renderer.py",
"allowed_style_presets": ["*"]
},
{
"id": "artboard_satori.metric-dashboard",
"status": "active",
"family": "dashboard",
"runtime_module": "svglide_artboard_renderer.py",
"allowed_style_presets": ["*"]
},
{
"id": "artboard_satori.risk-alert",
"status": "active",
"family": "risk",
"runtime_module": "svglide_artboard_renderer.py",
"allowed_style_presets": ["*"]
},
{
"id": "artboard_satori.roadmap-lanes",
"status": "active",
"family": "roadmap",
"runtime_module": "svglide_artboard_renderer.py",
"allowed_style_presets": ["*"]
},
{
"id": "chart",
"status": "active",

View File

@@ -0,0 +1,28 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://larksuite.local/svglide-repair-plan.schema.json",
"title": "SVGlide Repair Planner Output",
"type": "object",
"additionalProperties": false,
"required": ["schema_version", "target_plan_path", "change_reason", "patches"],
"properties": {
"schema_version": {"const": "svglide-repair-plan/v1"},
"target_plan_path": {"type": "string", "minLength": 1},
"change_reason": {"type": "string", "minLength": 1},
"patches": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"additionalProperties": false,
"required": ["op", "path", "reason"],
"properties": {
"op": {"enum": ["add", "replace", "remove", "test"]},
"path": {"type": "string", "minLength": 1},
"value": {},
"reason": {"type": "string", "minLength": 1}
}
}
}
}
}

View File

@@ -0,0 +1,43 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://larksuite.local/svglide-semantic-map.schema.json",
"title": "SVGlide semantic map",
"type": "object",
"required": ["version", "page", "template_id", "semantic_source", "elements"],
"additionalProperties": true,
"properties": {
"version": {"const": "svglide-semantic-map/v1"},
"page": {"type": "integer", "minimum": 1},
"template_id": {"type": "string", "minLength": 1},
"theme_id": {"type": ["string", "null"]},
"semantic_source": {"type": "string", "minLength": 1},
"content_keys": {"type": "array", "items": {"type": "string"}},
"elements": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"required": ["element_id", "kind", "role", "bbox"],
"additionalProperties": true,
"properties": {
"element_id": {"type": "string", "minLength": 1},
"kind": {"type": "string", "minLength": 1},
"role": {"type": "string", "minLength": 1},
"source_ref": {"type": ["string", "null"]},
"text": {"type": ["string", "null"]},
"bbox": {
"type": "object",
"required": ["x", "y", "width", "height"],
"additionalProperties": true,
"properties": {
"x": {"type": "number"},
"y": {"type": "number"},
"width": {"type": "number"},
"height": {"type": "number"}
}
}
}
}
}
}
}

View File

@@ -0,0 +1,40 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://larksuite.local/svglide-slide-plan.schema.json",
"title": "SVGlide Slide Planner Output",
"type": "object",
"additionalProperties": false,
"required": ["schema_version", "deck_plan_ref", "generation_mode", "slides"],
"properties": {
"schema_version": {"const": "svglide-slide-plan/v1"},
"deck_plan_ref": {
"type": "object",
"additionalProperties": false,
"required": ["path"],
"properties": {
"path": {"type": "string", "minLength": 1},
"sha256": {"type": "string", "minLength": 1}
}
},
"generation_mode": {"const": "artboard_satori"},
"slides": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"additionalProperties": false,
"required": ["page", "title", "key_message", "template_id", "theme_id", "content_requirements", "visual_role", "source_policy"],
"properties": {
"page": {"type": "integer", "minimum": 1},
"title": {"type": "string", "minLength": 1},
"key_message": {"type": "string", "minLength": 1},
"template_id": {"type": "string", "minLength": 1},
"theme_id": {"type": "string", "minLength": 1},
"content_requirements": {"type": "object", "additionalProperties": true},
"visual_role": {"type": "string", "minLength": 1},
"source_policy": {"type": "string", "minLength": 1}
}
}
}
}
}

View File

@@ -14,6 +14,7 @@
"skills/lark-slides/references/svglide-lock.contract.md",
"skills/lark-slides/references/svglide-assets.contract.md",
"skills/lark-slides/references/svglide-generate-svg.contract.md",
"skills/lark-slides/references/svglide-artboard-satori.contract.md",
"skills/lark-slides/references/svglide-preview.spec.md",
"skills/lark-slides/references/svglide-checks.checklist.md",
"skills/lark-slides/references/svglide-readback.contract.md",
@@ -30,6 +31,10 @@
"skills/lark-slides/references/svglide-asset-planning.md",
"skills/lark-slides/references/safe-native-v1.profile.json",
"skills/lark-slides/references/svglide-plan.schema.json",
"skills/lark-slides/references/svglide-canvas-spec.schema.json",
"skills/lark-slides/references/svglide-semantic-map.schema.json",
"skills/lark-slides/references/svglide-node-layout-map.schema.json",
"skills/lark-slides/references/svglide-artboard-receipt.schema.json",
"skills/lark-slides/references/svglide-strategy-review.schema.json",
"skills/lark-slides/references/svglide-ppt-master-asset-map.schema.json",
"skills/lark-slides/references/svglide-template-admission.schema.json",
@@ -52,6 +57,12 @@
"skills/lark-slides/scripts/svg_preview_lint.py",
"skills/lark-slides/scripts/svglide_source.py",
"skills/lark-slides/scripts/svglide_assets.py",
"skills/lark-slides/scripts/svglide_artboard_renderer.py",
"skills/lark-slides/scripts/svglide_template_fit_check.py",
"skills/lark-slides/scripts/artboard_renderer/package.json",
"skills/lark-slides/scripts/artboard_renderer/render.mjs",
"skills/lark-slides/scripts/artboard_renderer/templates/README.md",
"skills/lark-slides/scripts/artboard_renderer/themes/dark-clarity.json",
"skills/lark-slides/scripts/svglide_project_runner.py",
"skills/lark-slides/scripts/svglide_strategy_review.py",
"skills/lark-slides/scripts/svglide_prepare.py",
@@ -80,6 +91,8 @@
"skills/lark-slides/references/svg-protocol.md",
"skills/lark-slides/scripts/svglide_source.py",
"skills/lark-slides/scripts/svglide_assets.py",
"skills/lark-slides/scripts/svglide_artboard_renderer.py",
"skills/lark-slides/scripts/svglide_template_fit_check.py",
"skills/lark-slides/scripts/svglide_project_runner.py",
"skills/lark-slides/scripts/svglide_strategy_review.py",
"skills/lark-slides/scripts/svglide_prepare.py",
@@ -111,6 +124,7 @@
"skills/lark-slides/references/svglide-lock.contract.md",
"skills/lark-slides/references/svglide-assets.contract.md",
"skills/lark-slides/references/svglide-generate-svg.contract.md",
"skills/lark-slides/references/svglide-artboard-satori.contract.md",
"skills/lark-slides/references/svglide-preview.spec.md",
"skills/lark-slides/references/svglide-checks.checklist.md",
"skills/lark-slides/references/svglide-readback.contract.md",
@@ -120,6 +134,10 @@
"skills/lark-slides/references/svglide-strategy-review.schema.json",
"skills/lark-slides/references/svglide-ppt-master-asset-map.schema.json",
"skills/lark-slides/references/svglide-template-admission.schema.json",
"skills/lark-slides/references/svglide-canvas-spec.schema.json",
"skills/lark-slides/references/svglide-semantic-map.schema.json",
"skills/lark-slides/references/svglide-node-layout-map.schema.json",
"skills/lark-slides/references/svglide-artboard-receipt.schema.json",
"skills/lark-slides/references/svglide-evidence.schema.json",
"skills/lark-slides/references/svglide-source-receipt.schema.json",
"skills/lark-slides/references/svglide-generator-receipt.schema.json",
@@ -137,6 +155,12 @@
"skills/lark-slides/references/svglide-quality-gate.schema.json",
"skills/lark-slides/scripts/svglide_source.py",
"skills/lark-slides/scripts/svglide_assets.py",
"skills/lark-slides/scripts/svglide_artboard_renderer.py",
"skills/lark-slides/scripts/svglide_template_fit_check.py",
"skills/lark-slides/scripts/artboard_renderer/package.json",
"skills/lark-slides/scripts/artboard_renderer/render.mjs",
"skills/lark-slides/scripts/artboard_renderer/templates/README.md",
"skills/lark-slides/scripts/artboard_renderer/themes/dark-clarity.json",
"skills/lark-slides/scripts/svglide_project_runner.py",
"skills/lark-slides/scripts/svglide_strategy_review.py",
"skills/lark-slides/scripts/svglide_prepare.py",

View File

@@ -0,0 +1,170 @@
{
"schema_version": "svglide-template-registry/v1",
"templates": [
{
"id": "cover-hero",
"status": "active",
"renderer_id": "artboard_satori.cover-hero",
"layout_family": "cover",
"required_content": ["title"],
"optional_content": ["eyebrow", "subtitle", "chips"],
"max_items": {"chips": 4},
"text_budget": {"eyebrow": 28, "title": 32, "subtitle": 80, "chips": 16},
"supported_theme_ids": ["dark-clarity", "forest-signal", "warm-editorial", "blueprint-technical", "editorial-tritone", "cobalt-grid", "finance-dark", "swiss-red", "glass-neon", "paper-research"]
},
{
"id": "comparison-cards",
"status": "active",
"renderer_id": "artboard_satori.comparison-cards",
"layout_family": "comparison",
"required_content": ["title", "left_title", "right_title", "left_points", "right_points"],
"optional_content": ["conclusion"],
"max_items": {"left_points": 3, "right_points": 3},
"text_budget": {"title": 36, "left_title": 16, "right_title": 16, "left_points": 22, "right_points": 22, "conclusion": 52},
"supported_theme_ids": ["dark-clarity", "forest-signal", "warm-editorial", "blueprint-technical", "editorial-tritone", "cobalt-grid", "finance-dark", "swiss-red", "glass-neon", "paper-research"]
},
{
"id": "summary-final",
"status": "active",
"renderer_id": "artboard_satori.summary-final",
"layout_family": "closing",
"required_content": ["title"],
"optional_content": ["eyebrow", "subtitle", "takeaways"],
"max_items": {"takeaways": 3},
"text_budget": {"eyebrow": 20, "title": 34, "subtitle": 76, "takeaways": 24},
"supported_theme_ids": ["dark-clarity", "forest-signal", "warm-editorial", "blueprint-technical", "editorial-tritone", "cobalt-grid", "finance-dark", "swiss-red", "glass-neon", "paper-research"]
},
{
"id": "section-title",
"status": "active",
"renderer_id": "artboard_satori.section-title",
"layout_family": "section",
"required_content": ["title"],
"optional_content": ["eyebrow", "subtitle"],
"max_items": {},
"text_budget": {"eyebrow": 24, "title": 34, "subtitle": 72},
"supported_theme_ids": ["dark-clarity", "forest-signal", "warm-editorial", "blueprint-technical", "editorial-tritone", "cobalt-grid", "finance-dark", "swiss-red", "glass-neon", "paper-research"]
},
{
"id": "agenda-list",
"status": "active",
"renderer_id": "artboard_satori.agenda-list",
"layout_family": "agenda",
"required_content": ["title", "items"],
"optional_content": ["eyebrow", "subtitle"],
"max_items": {"items": 6},
"text_budget": {"eyebrow": 24, "title": 36, "subtitle": 72, "items": 28},
"supported_theme_ids": ["dark-clarity", "forest-signal", "warm-editorial", "blueprint-technical", "editorial-tritone", "cobalt-grid", "finance-dark", "swiss-red", "glass-neon", "paper-research"]
},
{
"id": "timeline-steps",
"status": "active",
"renderer_id": "artboard_satori.timeline-steps",
"layout_family": "timeline",
"required_content": ["title", "events"],
"optional_content": ["eyebrow", "subtitle"],
"max_items": {"events": 5},
"text_budget": {"eyebrow": 24, "title": 36, "subtitle": 72, "events": 22},
"supported_theme_ids": ["dark-clarity", "forest-signal", "warm-editorial", "blueprint-technical", "editorial-tritone", "cobalt-grid", "finance-dark", "swiss-red", "glass-neon", "paper-research"]
},
{
"id": "process-flow",
"status": "active",
"renderer_id": "artboard_satori.process-flow",
"layout_family": "process",
"required_content": ["title", "steps"],
"optional_content": ["eyebrow", "subtitle", "conclusion"],
"max_items": {"steps": 5},
"text_budget": {"eyebrow": 24, "title": 36, "subtitle": 72, "steps": 24, "conclusion": 56},
"supported_theme_ids": ["dark-clarity", "forest-signal", "warm-editorial", "blueprint-technical", "editorial-tritone", "cobalt-grid", "finance-dark", "swiss-red", "glass-neon", "paper-research"]
},
{
"id": "metric-dashboard",
"status": "active",
"renderer_id": "artboard_satori.metric-dashboard",
"layout_family": "dashboard",
"required_content": ["title", "metrics"],
"optional_content": ["eyebrow", "subtitle"],
"max_items": {"metrics": 6},
"text_budget": {"eyebrow": 24, "title": 34, "subtitle": 68, "metrics": 20},
"supported_theme_ids": ["dark-clarity", "forest-signal", "warm-editorial", "blueprint-technical", "editorial-tritone", "cobalt-grid", "finance-dark", "swiss-red", "glass-neon", "paper-research"]
},
{
"id": "quote-focus",
"status": "active",
"renderer_id": "artboard_satori.quote-focus",
"layout_family": "quote",
"required_content": ["title", "quote"],
"optional_content": ["attribution"],
"max_items": {},
"text_budget": {"title": 36, "quote": 88, "attribution": 36},
"supported_theme_ids": ["dark-clarity", "forest-signal", "warm-editorial", "blueprint-technical", "editorial-tritone", "cobalt-grid", "finance-dark", "swiss-red", "glass-neon", "paper-research"]
},
{
"id": "image-feature",
"status": "active",
"renderer_id": "artboard_satori.image-feature",
"layout_family": "image_text",
"required_content": ["title", "points"],
"optional_content": ["eyebrow", "subtitle", "image_label", "caption"],
"max_items": {"points": 3},
"text_budget": {"eyebrow": 24, "title": 34, "subtitle": 64, "points": 24, "caption": 52, "image_label": 18},
"supported_theme_ids": ["dark-clarity", "forest-signal", "warm-editorial", "blueprint-technical", "editorial-tritone", "cobalt-grid", "finance-dark", "swiss-red", "glass-neon", "paper-research"]
},
{
"id": "research-poster",
"status": "active",
"renderer_id": "artboard_satori.research-poster",
"layout_family": "poster",
"required_content": ["title", "sections"],
"optional_content": ["eyebrow", "authors", "key_visual"],
"max_items": {"sections": 6},
"text_budget": {"eyebrow": 24, "title": 42, "authors": 48, "sections": 22, "key_visual": 28},
"supported_theme_ids": ["dark-clarity", "forest-signal", "warm-editorial", "blueprint-technical", "editorial-tritone", "cobalt-grid", "finance-dark", "swiss-red", "glass-neon", "paper-research"]
},
{
"id": "data-story",
"status": "active",
"renderer_id": "artboard_satori.data-story",
"layout_family": "data_story",
"required_content": ["title", "metrics"],
"optional_content": ["eyebrow", "subtitle", "callout"],
"max_items": {"metrics": 4},
"text_budget": {"eyebrow": 24, "title": 34, "subtitle": 64, "metrics": 18, "callout": 48},
"supported_theme_ids": ["dark-clarity", "forest-signal", "warm-editorial", "blueprint-technical", "editorial-tritone", "cobalt-grid", "finance-dark", "swiss-red", "glass-neon", "paper-research"]
},
{
"id": "risk-alert",
"status": "active",
"renderer_id": "artboard_satori.risk-alert",
"layout_family": "risk",
"required_content": ["title", "risks"],
"optional_content": ["eyebrow", "subtitle", "summary", "severity"],
"max_items": {"risks": 4},
"text_budget": {"eyebrow": 24, "title": 36, "subtitle": 64, "risks": 28, "summary": 52, "severity": 8},
"supported_theme_ids": ["dark-clarity", "forest-signal", "warm-editorial", "blueprint-technical", "editorial-tritone", "cobalt-grid", "finance-dark", "swiss-red", "glass-neon", "paper-research"]
},
{
"id": "roadmap-lanes",
"status": "active",
"renderer_id": "artboard_satori.roadmap-lanes",
"layout_family": "roadmap",
"required_content": ["title", "lanes"],
"optional_content": ["eyebrow", "subtitle"],
"max_items": {"lanes": 4},
"text_budget": {"eyebrow": 24, "title": 36, "subtitle": 64, "lanes": 18},
"supported_theme_ids": ["dark-clarity", "forest-signal", "warm-editorial", "blueprint-technical", "editorial-tritone", "cobalt-grid", "finance-dark", "swiss-red", "glass-neon", "paper-research"]
},
{
"id": "architecture-blueprint",
"status": "active",
"renderer_id": "artboard_satori.architecture-blueprint",
"layout_family": "architecture",
"required_content": ["title", "nodes"],
"optional_content": ["eyebrow", "subtitle"],
"max_items": {"nodes": 6},
"text_budget": {"eyebrow": 24, "title": 38, "subtitle": 64, "nodes": 22},
"supported_theme_ids": ["dark-clarity", "forest-signal", "warm-editorial", "blueprint-technical", "editorial-tritone", "cobalt-grid", "finance-dark", "swiss-red", "glass-neon", "paper-research"]
}
]
}

View File

@@ -0,0 +1,113 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://larksuite.local/svglide-theme-spec.schema.json",
"title": "SVGlide ThemeSpec",
"type": "object",
"required": [
"schema_version",
"theme_id",
"mode",
"colors",
"semantic_colors",
"tokens",
"contrast",
"allowed_color_roles"
],
"additionalProperties": true,
"properties": {
"schema_version": {"const": "svglide-theme/v1"},
"theme_id": {"type": "string", "minLength": 1},
"mode": {"enum": ["light", "dark"]},
"colors": {
"type": "object",
"required": [
"background",
"surface",
"text",
"muted",
"primary",
"accent",
"success",
"warning",
"danger"
],
"additionalProperties": {"$ref": "#/$defs/hexColor"},
"properties": {
"background": {"$ref": "#/$defs/hexColor"},
"surface": {"$ref": "#/$defs/hexColor"},
"text": {"$ref": "#/$defs/hexColor"},
"muted": {"$ref": "#/$defs/hexColor"},
"primary": {"$ref": "#/$defs/hexColor"},
"accent": {"$ref": "#/$defs/hexColor"},
"success": {"$ref": "#/$defs/hexColor"},
"warning": {"$ref": "#/$defs/hexColor"},
"danger": {"$ref": "#/$defs/hexColor"}
}
},
"semantic_colors": {
"type": "object",
"minProperties": 1,
"additionalProperties": {
"anyOf": [
{"$ref": "#/$defs/hexColor"},
{"type": "string", "minLength": 1}
]
}
},
"tokens": {
"type": "object",
"required": [
"color.background",
"color.surface",
"color.text",
"color.muted",
"color.primary",
"color.accent",
"color.success",
"color.warning",
"color.danger"
],
"additionalProperties": {
"anyOf": [
{"$ref": "#/$defs/hexColor"},
{"type": "string", "minLength": 1}
]
},
"properties": {
"color.background": {"$ref": "#/$defs/hexColor"},
"color.surface": {"$ref": "#/$defs/hexColor"},
"color.text": {"$ref": "#/$defs/hexColor"},
"color.muted": {"$ref": "#/$defs/hexColor"},
"color.primary": {"$ref": "#/$defs/hexColor"},
"color.accent": {"$ref": "#/$defs/hexColor"},
"color.success": {"$ref": "#/$defs/hexColor"},
"color.warning": {"$ref": "#/$defs/hexColor"},
"color.danger": {"$ref": "#/$defs/hexColor"}
}
},
"contrast": {
"type": "object",
"required": ["min_text_contrast"],
"additionalProperties": true,
"properties": {
"min_text_contrast": {"type": "number"}
}
},
"allowed_color_roles": {
"type": "array",
"minItems": 1,
"uniqueItems": true,
"items": {"type": "string", "minLength": 1}
},
"data_series": {
"type": "array",
"items": {"$ref": "#/$defs/hexColor"}
}
},
"$defs": {
"hexColor": {
"type": "string",
"pattern": "^#(?:[0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})$"
}
}
}

View File

@@ -0,0 +1,96 @@
export function node(type, style, children) {
return { type, props: { style, children } }
}
export function box(style, children = []) {
return node('div', { display: 'flex', boxSizing: 'border-box', ...style }, children)
}
export function TextBlock(value, style = {}) {
return node(
'div',
{
display: 'flex',
boxSizing: 'border-box',
whiteSpace: 'normal',
...style
},
value
)
}
export function Title(value, style = {}) {
return TextBlock(value, {
fontSize: 58,
fontWeight: 800,
lineHeight: 1.05,
...style
})
}
export function Subtitle(value, style = {}) {
return TextBlock(value, {
fontSize: 24,
fontWeight: 500,
lineHeight: 1.25,
...style
})
}
export function Badge(value, style = {}) {
return TextBlock(value, {
fontSize: 18,
fontWeight: 700,
...style
})
}
export function Chip(value, style = {}) {
return TextBlock(value, {
minWidth: 92,
height: 40,
padding: '8px 15px',
fontSize: 17,
fontWeight: 600,
...style
})
}
export function StatCard({ index, label, color, textColor, panelColor, style = {} }) {
return box(
{
width: 250,
minHeight: 126,
flexDirection: 'column',
backgroundColor: panelColor,
padding: 22,
...style
},
[
TextBlock(String(index).padStart(2, '0'), {
color,
fontSize: 18,
fontWeight: 800,
marginBottom: 12
}),
TextBlock(label, {
color: textColor,
fontSize: 21,
fontWeight: 700,
lineHeight: 1.18
})
]
)
}
export function ImageFrame({ children = [], style = {} }) {
return box(
{
position: 'relative',
overflow: 'hidden',
backgroundColor: 'rgba(255,255,255,0.08)',
...style
},
children
)
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,21 @@
{
"name": "@larksuite/svglide-artboard-renderer",
"version": "0.0.0-p0",
"private": true,
"type": "module",
"scripts": {
"render": "node render.mjs",
"build": "esbuild render.mjs --bundle --platform=node --format=esm --target=node18 --external:@resvg/resvg-js --outfile=dist/render.mjs",
"check": "node render.mjs --check-runtime"
},
"engines": {
"node": ">=18"
},
"dependencies": {
"@resvg/resvg-js": "2.6.2",
"satori": "0.26.0"
},
"devDependencies": {
"esbuild": "0.28.1"
}
}

View File

@@ -0,0 +1,569 @@
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
dependencies:
'@resvg/resvg-js':
specifier: 2.6.2
version: 2.6.2
satori:
specifier: 0.26.0
version: 0.26.0
devDependencies:
esbuild:
specifier: 0.28.1
version: 0.28.1
packages:
'@esbuild/aix-ppc64@0.28.1':
resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==}
engines: {node: '>=18'}
cpu: [ppc64]
os: [aix]
'@esbuild/android-arm64@0.28.1':
resolution: {integrity: sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [android]
'@esbuild/android-arm@0.28.1':
resolution: {integrity: sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==}
engines: {node: '>=18'}
cpu: [arm]
os: [android]
'@esbuild/android-x64@0.28.1':
resolution: {integrity: sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==}
engines: {node: '>=18'}
cpu: [x64]
os: [android]
'@esbuild/darwin-arm64@0.28.1':
resolution: {integrity: sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==}
engines: {node: '>=18'}
cpu: [arm64]
os: [darwin]
'@esbuild/darwin-x64@0.28.1':
resolution: {integrity: sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==}
engines: {node: '>=18'}
cpu: [x64]
os: [darwin]
'@esbuild/freebsd-arm64@0.28.1':
resolution: {integrity: sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==}
engines: {node: '>=18'}
cpu: [arm64]
os: [freebsd]
'@esbuild/freebsd-x64@0.28.1':
resolution: {integrity: sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==}
engines: {node: '>=18'}
cpu: [x64]
os: [freebsd]
'@esbuild/linux-arm64@0.28.1':
resolution: {integrity: sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==}
engines: {node: '>=18'}
cpu: [arm64]
os: [linux]
'@esbuild/linux-arm@0.28.1':
resolution: {integrity: sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==}
engines: {node: '>=18'}
cpu: [arm]
os: [linux]
'@esbuild/linux-ia32@0.28.1':
resolution: {integrity: sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==}
engines: {node: '>=18'}
cpu: [ia32]
os: [linux]
'@esbuild/linux-loong64@0.28.1':
resolution: {integrity: sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==}
engines: {node: '>=18'}
cpu: [loong64]
os: [linux]
'@esbuild/linux-mips64el@0.28.1':
resolution: {integrity: sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==}
engines: {node: '>=18'}
cpu: [mips64el]
os: [linux]
'@esbuild/linux-ppc64@0.28.1':
resolution: {integrity: sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==}
engines: {node: '>=18'}
cpu: [ppc64]
os: [linux]
'@esbuild/linux-riscv64@0.28.1':
resolution: {integrity: sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==}
engines: {node: '>=18'}
cpu: [riscv64]
os: [linux]
'@esbuild/linux-s390x@0.28.1':
resolution: {integrity: sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==}
engines: {node: '>=18'}
cpu: [s390x]
os: [linux]
'@esbuild/linux-x64@0.28.1':
resolution: {integrity: sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==}
engines: {node: '>=18'}
cpu: [x64]
os: [linux]
'@esbuild/netbsd-arm64@0.28.1':
resolution: {integrity: sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==}
engines: {node: '>=18'}
cpu: [arm64]
os: [netbsd]
'@esbuild/netbsd-x64@0.28.1':
resolution: {integrity: sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==}
engines: {node: '>=18'}
cpu: [x64]
os: [netbsd]
'@esbuild/openbsd-arm64@0.28.1':
resolution: {integrity: sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==}
engines: {node: '>=18'}
cpu: [arm64]
os: [openbsd]
'@esbuild/openbsd-x64@0.28.1':
resolution: {integrity: sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==}
engines: {node: '>=18'}
cpu: [x64]
os: [openbsd]
'@esbuild/openharmony-arm64@0.28.1':
resolution: {integrity: sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [openharmony]
'@esbuild/sunos-x64@0.28.1':
resolution: {integrity: sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==}
engines: {node: '>=18'}
cpu: [x64]
os: [sunos]
'@esbuild/win32-arm64@0.28.1':
resolution: {integrity: sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==}
engines: {node: '>=18'}
cpu: [arm64]
os: [win32]
'@esbuild/win32-ia32@0.28.1':
resolution: {integrity: sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==}
engines: {node: '>=18'}
cpu: [ia32]
os: [win32]
'@esbuild/win32-x64@0.28.1':
resolution: {integrity: sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==}
engines: {node: '>=18'}
cpu: [x64]
os: [win32]
'@resvg/resvg-js-android-arm-eabi@2.6.2':
resolution: {integrity: sha512-FrJibrAk6v29eabIPgcTUMPXiEz8ssrAk7TXxsiZzww9UTQ1Z5KAbFJs+Z0Ez+VZTYgnE5IQJqBcoSiMebtPHA==}
engines: {node: '>= 10'}
cpu: [arm]
os: [android]
'@resvg/resvg-js-android-arm64@2.6.2':
resolution: {integrity: sha512-VcOKezEhm2VqzXpcIJoITuvUS/fcjIw5NA/w3tjzWyzmvoCdd+QXIqy3FBGulWdClvp4g+IfUemigrkLThSjAQ==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [android]
'@resvg/resvg-js-darwin-arm64@2.6.2':
resolution: {integrity: sha512-nmok2LnAd6nLUKI16aEB9ydMC6Lidiiq2m1nEBDR1LaaP7FGs4AJ90qDraxX+CWlVuRlvNjyYJTNv8qFjtL9+A==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
'@resvg/resvg-js-darwin-x64@2.6.2':
resolution: {integrity: sha512-GInyZLjgWDfsVT6+SHxQVRwNzV0AuA1uqGsOAW+0th56J7Nh6bHHKXHBWzUrihxMetcFDmQMAX1tZ1fZDYSRsw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
'@resvg/resvg-js-linux-arm-gnueabihf@2.6.2':
resolution: {integrity: sha512-YIV3u/R9zJbpqTTNwTZM5/ocWetDKGsro0SWp70eGEM9eV2MerWyBRZnQIgzU3YBnSBQ1RcxRZvY/UxwESfZIw==}
engines: {node: '>= 10'}
cpu: [arm]
os: [linux]
'@resvg/resvg-js-linux-arm64-gnu@2.6.2':
resolution: {integrity: sha512-zc2BlJSim7YR4FZDQ8OUoJg5holYzdiYMeobb9pJuGDidGL9KZUv7SbiD4E8oZogtYY42UZEap7dqkkYuA91pg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@resvg/resvg-js-linux-arm64-musl@2.6.2':
resolution: {integrity: sha512-3h3dLPWNgSsD4lQBJPb4f+kvdOSJHa5PjTYVsWHxLUzH4IFTJUAnmuWpw4KqyQ3NA5QCyhw4TWgxk3jRkQxEKg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@resvg/resvg-js-linux-x64-gnu@2.6.2':
resolution: {integrity: sha512-IVUe+ckIerA7xMZ50duAZzwf1U7khQe2E0QpUxu5MBJNao5RqC0zwV/Zm965vw6D3gGFUl7j4m+oJjubBVoftw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@resvg/resvg-js-linux-x64-musl@2.6.2':
resolution: {integrity: sha512-UOf83vqTzoYQO9SZ0fPl2ZIFtNIz/Rr/y+7X8XRX1ZnBYsQ/tTb+cj9TE+KHOdmlTFBxhYzVkP2lRByCzqi4jQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [musl]
'@resvg/resvg-js-win32-arm64-msvc@2.6.2':
resolution: {integrity: sha512-7C/RSgCa+7vqZ7qAbItfiaAWhyRSoD4l4BQAbVDqRRsRgY+S+hgS3in0Rxr7IorKUpGE69X48q6/nOAuTJQxeQ==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
'@resvg/resvg-js-win32-ia32-msvc@2.6.2':
resolution: {integrity: sha512-har4aPAlvjnLcil40AC77YDIk6loMawuJwFINEM7n0pZviwMkMvjb2W5ZirsNOZY4aDbo5tLx0wNMREp5Brk+w==}
engines: {node: '>= 10'}
cpu: [ia32]
os: [win32]
'@resvg/resvg-js-win32-x64-msvc@2.6.2':
resolution: {integrity: sha512-ZXtYhtUr5SSaBrUDq7DiyjOFJqBVL/dOBN7N/qmi/pO0IgiWW/f/ue3nbvu9joWE5aAKDoIzy/CxsY0suwGosQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
'@resvg/resvg-js@2.6.2':
resolution: {integrity: sha512-xBaJish5OeGmniDj9cW5PRa/PtmuVU3ziqrbr5xJj901ZDN4TosrVaNZpEiLZAxdfnhAe7uQ7QFWfjPe9d9K2Q==}
engines: {node: '>= 10'}
'@shuding/opentype.js@1.4.0-beta.0':
resolution: {integrity: sha512-3NgmNyH3l/Hv6EvsWJbsvpcpUba6R8IREQ83nH83cyakCw7uM1arZKNfHwv1Wz6jgqrF/j4x5ELvR6PnK9nTcA==}
engines: {node: '>= 8.0.0'}
hasBin: true
base64-js@0.0.8:
resolution: {integrity: sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==}
engines: {node: '>= 0.4'}
camelize@1.0.1:
resolution: {integrity: sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==}
color-name@1.1.4:
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
css-background-parser@0.1.0:
resolution: {integrity: sha512-2EZLisiZQ+7m4wwur/qiYJRniHX4K5Tc9w93MT3AS0WS1u5kaZ4FKXlOTBhOjc+CgEgPiGY+fX1yWD8UwpEqUA==}
css-box-shadow@1.0.0-3:
resolution: {integrity: sha512-9jaqR6e7Ohds+aWwmhe6wILJ99xYQbfmK9QQB9CcMjDbTxPZjwEmUQpU91OG05Xgm8BahT5fW+svbsQGjS/zPg==}
css-color-keywords@1.0.0:
resolution: {integrity: sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==}
engines: {node: '>=4'}
css-gradient-parser@0.0.17:
resolution: {integrity: sha512-w2Xy9UMMwlKtou0vlRnXvWglPAceXCTtcmVSo8ZBUvqCV5aXEFP/PC6d+I464810I9FT++UACwTD5511bmGPUg==}
engines: {node: '>=16'}
css-to-react-native@3.2.0:
resolution: {integrity: sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==}
emoji-regex-xs@2.0.1:
resolution: {integrity: sha512-1QFuh8l7LqUcKe24LsPUNzjrzJQ7pgRwp1QMcZ5MX6mFplk2zQ08NVCM84++1cveaUUYtcCYHmeFEuNg16sU4g==}
engines: {node: '>=10.0.0'}
esbuild@0.28.1:
resolution: {integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==}
engines: {node: '>=18'}
hasBin: true
escape-html@1.0.3:
resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==}
fflate@0.7.4:
resolution: {integrity: sha512-5u2V/CDW15QM1XbbgS+0DfPxVB+jUKhWEKuuFuHncbk3tEEqzmoXL+2KyOFuKGqOnmdIy0/davWF1CkuwtibCw==}
hex-rgb@4.3.0:
resolution: {integrity: sha512-Ox1pJVrDCyGHMG9CFg1tmrRUMRPRsAWYc/PinY0XzJU4K7y7vjNoLKIQ7BR5UJMCxNN8EM1MNDmHWA/B3aZUuw==}
engines: {node: '>=6'}
linebreak@1.1.0:
resolution: {integrity: sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==}
pako@0.2.9:
resolution: {integrity: sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==}
parse-css-color@0.2.1:
resolution: {integrity: sha512-bwS/GGIFV3b6KS4uwpzCFj4w297Yl3uqnSgIPsoQkx7GMLROXfMnWvxfNkL0oh8HVhZA4hvJoEoEIqonfJ3BWg==}
postcss-value-parser@4.2.0:
resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==}
satori@0.26.0:
resolution: {integrity: sha512-tkMFrfIs3l2mQ2JEcyW0ADTy3zGggFRFzi6Ef8YozQSFsFKEqaSO1Y8F9wJg4//PJGQauMalHGTUEkPrFwhVPA==}
engines: {node: '>=16'}
string.prototype.codepointat@0.2.1:
resolution: {integrity: sha512-2cBVCj6I4IOvEnjgO/hWqXjqBGsY+zwPmHl12Srk9IXSZ56Jwwmy+66XO5Iut/oQVR7t5ihYdLB0GMa4alEUcg==}
tiny-inflate@1.0.3:
resolution: {integrity: sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==}
unicode-trie@2.0.0:
resolution: {integrity: sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==}
yoga-layout@3.2.1:
resolution: {integrity: sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==}
snapshots:
'@esbuild/aix-ppc64@0.28.1':
optional: true
'@esbuild/android-arm64@0.28.1':
optional: true
'@esbuild/android-arm@0.28.1':
optional: true
'@esbuild/android-x64@0.28.1':
optional: true
'@esbuild/darwin-arm64@0.28.1':
optional: true
'@esbuild/darwin-x64@0.28.1':
optional: true
'@esbuild/freebsd-arm64@0.28.1':
optional: true
'@esbuild/freebsd-x64@0.28.1':
optional: true
'@esbuild/linux-arm64@0.28.1':
optional: true
'@esbuild/linux-arm@0.28.1':
optional: true
'@esbuild/linux-ia32@0.28.1':
optional: true
'@esbuild/linux-loong64@0.28.1':
optional: true
'@esbuild/linux-mips64el@0.28.1':
optional: true
'@esbuild/linux-ppc64@0.28.1':
optional: true
'@esbuild/linux-riscv64@0.28.1':
optional: true
'@esbuild/linux-s390x@0.28.1':
optional: true
'@esbuild/linux-x64@0.28.1':
optional: true
'@esbuild/netbsd-arm64@0.28.1':
optional: true
'@esbuild/netbsd-x64@0.28.1':
optional: true
'@esbuild/openbsd-arm64@0.28.1':
optional: true
'@esbuild/openbsd-x64@0.28.1':
optional: true
'@esbuild/openharmony-arm64@0.28.1':
optional: true
'@esbuild/sunos-x64@0.28.1':
optional: true
'@esbuild/win32-arm64@0.28.1':
optional: true
'@esbuild/win32-ia32@0.28.1':
optional: true
'@esbuild/win32-x64@0.28.1':
optional: true
'@resvg/resvg-js-android-arm-eabi@2.6.2':
optional: true
'@resvg/resvg-js-android-arm64@2.6.2':
optional: true
'@resvg/resvg-js-darwin-arm64@2.6.2':
optional: true
'@resvg/resvg-js-darwin-x64@2.6.2':
optional: true
'@resvg/resvg-js-linux-arm-gnueabihf@2.6.2':
optional: true
'@resvg/resvg-js-linux-arm64-gnu@2.6.2':
optional: true
'@resvg/resvg-js-linux-arm64-musl@2.6.2':
optional: true
'@resvg/resvg-js-linux-x64-gnu@2.6.2':
optional: true
'@resvg/resvg-js-linux-x64-musl@2.6.2':
optional: true
'@resvg/resvg-js-win32-arm64-msvc@2.6.2':
optional: true
'@resvg/resvg-js-win32-ia32-msvc@2.6.2':
optional: true
'@resvg/resvg-js-win32-x64-msvc@2.6.2':
optional: true
'@resvg/resvg-js@2.6.2':
optionalDependencies:
'@resvg/resvg-js-android-arm-eabi': 2.6.2
'@resvg/resvg-js-android-arm64': 2.6.2
'@resvg/resvg-js-darwin-arm64': 2.6.2
'@resvg/resvg-js-darwin-x64': 2.6.2
'@resvg/resvg-js-linux-arm-gnueabihf': 2.6.2
'@resvg/resvg-js-linux-arm64-gnu': 2.6.2
'@resvg/resvg-js-linux-arm64-musl': 2.6.2
'@resvg/resvg-js-linux-x64-gnu': 2.6.2
'@resvg/resvg-js-linux-x64-musl': 2.6.2
'@resvg/resvg-js-win32-arm64-msvc': 2.6.2
'@resvg/resvg-js-win32-ia32-msvc': 2.6.2
'@resvg/resvg-js-win32-x64-msvc': 2.6.2
'@shuding/opentype.js@1.4.0-beta.0':
dependencies:
fflate: 0.7.4
string.prototype.codepointat: 0.2.1
base64-js@0.0.8: {}
camelize@1.0.1: {}
color-name@1.1.4: {}
css-background-parser@0.1.0: {}
css-box-shadow@1.0.0-3: {}
css-color-keywords@1.0.0: {}
css-gradient-parser@0.0.17: {}
css-to-react-native@3.2.0:
dependencies:
camelize: 1.0.1
css-color-keywords: 1.0.0
postcss-value-parser: 4.2.0
emoji-regex-xs@2.0.1: {}
esbuild@0.28.1:
optionalDependencies:
'@esbuild/aix-ppc64': 0.28.1
'@esbuild/android-arm': 0.28.1
'@esbuild/android-arm64': 0.28.1
'@esbuild/android-x64': 0.28.1
'@esbuild/darwin-arm64': 0.28.1
'@esbuild/darwin-x64': 0.28.1
'@esbuild/freebsd-arm64': 0.28.1
'@esbuild/freebsd-x64': 0.28.1
'@esbuild/linux-arm': 0.28.1
'@esbuild/linux-arm64': 0.28.1
'@esbuild/linux-ia32': 0.28.1
'@esbuild/linux-loong64': 0.28.1
'@esbuild/linux-mips64el': 0.28.1
'@esbuild/linux-ppc64': 0.28.1
'@esbuild/linux-riscv64': 0.28.1
'@esbuild/linux-s390x': 0.28.1
'@esbuild/linux-x64': 0.28.1
'@esbuild/netbsd-arm64': 0.28.1
'@esbuild/netbsd-x64': 0.28.1
'@esbuild/openbsd-arm64': 0.28.1
'@esbuild/openbsd-x64': 0.28.1
'@esbuild/openharmony-arm64': 0.28.1
'@esbuild/sunos-x64': 0.28.1
'@esbuild/win32-arm64': 0.28.1
'@esbuild/win32-ia32': 0.28.1
'@esbuild/win32-x64': 0.28.1
escape-html@1.0.3: {}
fflate@0.7.4: {}
hex-rgb@4.3.0: {}
linebreak@1.1.0:
dependencies:
base64-js: 0.0.8
unicode-trie: 2.0.0
pako@0.2.9: {}
parse-css-color@0.2.1:
dependencies:
color-name: 1.1.4
hex-rgb: 4.3.0
postcss-value-parser@4.2.0: {}
satori@0.26.0:
dependencies:
'@shuding/opentype.js': 1.4.0-beta.0
css-background-parser: 0.1.0
css-box-shadow: 1.0.0-3
css-gradient-parser: 0.0.17
css-to-react-native: 3.2.0
emoji-regex-xs: 2.0.1
escape-html: 1.0.3
linebreak: 1.1.0
parse-css-color: 0.2.1
postcss-value-parser: 4.2.0
yoga-layout: 3.2.1
string.prototype.codepointat@0.2.1: {}
tiny-inflate@1.0.3: {}
unicode-trie@2.0.0:
dependencies:
pako: 0.2.9
tiny-inflate: 1.0.3
yoga-layout@3.2.1: {}

View File

@@ -0,0 +1,132 @@
import fs from 'node:fs/promises'
import path from 'node:path'
import process from 'node:process'
import { renderTree } from './templates/p0-templates.mjs'
const SATORI_VERSION = '0.26.0'
const RESVG_VERSION = '2.6.2'
const DEFAULT_FONT_FAMILY = 'SVGlideDefault'
const DEFAULT_FONT_CANDIDATES = [
'/System/Library/Fonts/Supplemental/Arial Unicode.ttf',
'/System/Library/Fonts/Supplemental/Arial.ttf',
'/System/Library/Fonts/Supplemental/Verdana.ttf',
'/System/Library/Fonts/Supplemental/Trebuchet MS.ttf',
'/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf',
'/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf',
'/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc',
'C:\\Windows\\Fonts\\arial.ttf'
]
async function pathExists(candidate) {
try {
await fs.access(candidate)
return true
} catch {
return false
}
}
async function resolveFontPath() {
if (process.env.SVGLIDE_SATORI_FONT_PATH) {
return process.env.SVGLIDE_SATORI_FONT_PATH
}
for (const candidate of DEFAULT_FONT_CANDIDATES) {
if (await pathExists(candidate)) {
return candidate
}
}
throw new Error(
'no usable Satori font found; set SVGLIDE_SATORI_FONT_PATH to a .ttf/.otf font available on this machine'
)
}
async function loadFont() {
const fontPath = await resolveFontPath()
const data = await fs.readFile(fontPath)
return { name: DEFAULT_FONT_FAMILY, data, weight: 400, style: 'normal', path: fontPath }
}
async function loadSatori() {
try {
return (await import('satori')).default
} catch (error) {
console.error('satori dependency is not available in this adapter runtime')
console.error('development fix: run pnpm install --frozen-lockfile in skills/lark-slides/scripts/artboard_renderer')
console.error('release fix: publish dist/render.mjs built by pnpm run build, not the unbundled source entry')
console.error(String(error?.message || error))
process.exit(3)
}
}
async function loadResvg() {
try {
return (await import('@resvg/resvg-js')).Resvg
} catch (error) {
console.error('@resvg/resvg-js native dependency is not available in this adapter runtime')
console.error('fix: run pnpm --dir skills/lark-slides/scripts/artboard_renderer install --frozen-lockfile')
console.error('release fix: install the platform-native @resvg/resvg-js package before running dist/render.mjs --check-runtime')
console.error(String(error?.message || error))
process.exit(4)
}
}
async function checkRuntime() {
await loadSatori()
const Resvg = await loadResvg()
const font = await loadFont()
const probe = '<svg xmlns="http://www.w3.org/2000/svg" width="4" height="4"><rect width="4" height="4" fill="#000"/></svg>'
new Resvg(probe).render().asPng()
console.log(JSON.stringify({ ok: true, renderer: 'satori-resvg', satori_version: SATORI_VERSION, resvg_version: RESVG_VERSION, font_path: font.path }))
}
async function main() {
const [, , inputPath, outputPath, pngPath, metadataPath] = process.argv
if (inputPath === '--check-runtime') {
await checkRuntime()
return
}
if (!inputPath || !outputPath) {
console.error('usage: node render.mjs <canvas-spec.json> <output.svg> [output.png] [metadata.json]')
process.exit(2)
}
const satori = await loadSatori()
const Resvg = await loadResvg()
const spec = JSON.parse(await fs.readFile(inputPath, 'utf8'))
const font = await loadFont()
const svg = await satori(renderTree(spec), {
width: 960,
height: 540,
embedFont: false,
fonts: [font]
})
await fs.mkdir(path.dirname(outputPath), { recursive: true })
await fs.writeFile(outputPath, svg)
let pngBytes = null
if (pngPath) {
pngBytes = new Resvg(svg, {
fitTo: { mode: 'width', value: 960 },
font: { loadSystemFonts: true }
}).render().asPng()
await fs.mkdir(path.dirname(pngPath), { recursive: true })
await fs.writeFile(pngPath, pngBytes)
}
if (metadataPath) {
await fs.mkdir(path.dirname(metadataPath), { recursive: true })
await fs.writeFile(
metadataPath,
JSON.stringify(
{
node_version: process.version,
satori_version: SATORI_VERSION,
resvg_version: RESVG_VERSION,
font_path: font.path,
png_bytes: pngBytes ? pngBytes.length : null
},
null,
2
) + '\n'
)
}
}
main()

View File

@@ -0,0 +1,39 @@
# Artboard Templates
P0 keeps live SVGlide output in `svglide_artboard_renderer.py`.
The Node renderer in this folder is the isolated Satori adapter. It stays in a skill-local subpackage so the root Go CLI does not absorb Node dependencies directly.
Supported P0 templates:
- `cover_hero`
- `section_title`
- `comparison`
The final SVGlide SVG is compiled from the Python CanvasSpec template artifact. The Node adapter is still required for the `artboard_satori` preview path because it renders Satori SVG and the resvg PNG used by preview and quality evidence.
To enable true Satori source rendering:
1. Install the adapter dependencies from `skills/lark-slides/scripts/artboard_renderer`.
2. Build and publish `dist/render.mjs` with the `lark-slides` skill machine resources.
3. Install the platform-native `@resvg/resvg-js` package for the target host; do not commit or embed `node_modules`.
4. Optionally set `SVGLIDE_SATORI_FONT_PATH` to a readable TTF/OTF font when the host has no usable system font.
5. Run the project with `SVGLIDE_ARTBOARD_USE_NODE_SATORI=1`.
Build command:
```bash
pnpm install --frozen-lockfile
pnpm run build
node dist/render.mjs --check-runtime
```
Gate 11 package validation command:
```bash
python3 skills/lark-slides/scripts/svglide_artboard_package_check.py --pretty
```
The published runtime must not require a sibling Satori source checkout. It does require the package-managed native `@resvg/resvg-js` runtime dependency; missing Node/resvg dependencies must fail fast before live create with the install command above.
The final SVG passed downstream is generated by parsing controlled Satori SVG and adding private SVGlide markers. Arbitrary HTML/CSS is still not accepted.

View File

@@ -0,0 +1,526 @@
import { Badge, Chip, StatCard, Subtitle, TextBlock, Title, box } from '../components/primitives.mjs'
const CANVAS = { width: 960, height: 540 }
const DEFAULT_FONT_FAMILY = 'SVGlideDefault'
function colors(spec) {
const source = spec.theme?.colors || {}
return {
background: source.background || '#0F172A',
panel: source.panel || '#111827',
primary: source.primary || '#38BDF8',
accent: source.accent || '#A78BFA',
text: source.text || '#F8FAFC',
muted: source.muted || '#CBD5E1'
}
}
function text(spec, key, fallback = '') {
const value = spec.content?.[key]
return typeof value === 'string' && value.trim() ? value.trim() : fallback
}
function list(spec, key) {
const value = spec.content?.[key]
return Array.isArray(value) ? value.filter((item) => typeof item === 'string' && item.trim()).map((item) => item.trim()) : []
}
function firstList(spec, keys, fallback = []) {
for (const key of keys) {
const values = list(spec, key)
if (values.length) return values
}
return fallback
}
function themeSize(spec, key, fallback) {
const value = spec.theme?.typography?.[key]
return typeof value === 'number' ? value : fallback
}
function pageShell(spec, children) {
const theme = colors(spec)
return box(
{
width: CANVAS.width,
height: CANVAS.height,
position: 'relative',
flexDirection: 'column',
backgroundColor: theme.background,
color: theme.text,
fontFamily: DEFAULT_FONT_FAMILY,
padding: 56
},
children
)
}
function pageHeader(spec, { titleWidth = 720, titleSize = null, subtitleKey = 'subtitle' } = {}) {
const theme = colors(spec)
return box({ flexDirection: 'column', marginBottom: 28 }, [
Badge(text(spec, 'eyebrow', '').toUpperCase(), {
color: theme.primary,
fontSize: 16,
fontWeight: 800,
marginBottom: 12
}),
Title(text(spec, 'title', 'Untitled'), {
width: titleWidth,
color: theme.text,
fontSize: titleSize || themeSize(spec, 'title', 42),
fontWeight: 850,
lineHeight: 1.08,
marginBottom: 14
}),
Subtitle(text(spec, subtitleKey, ''), {
width: Math.min(titleWidth, 700),
color: theme.muted,
fontSize: themeSize(spec, 'subtitle', 21),
lineHeight: 1.22
})
])
}
function numberedRows(items, theme, { start = 1, max = 6 } = {}) {
return items.slice(0, max).map((item, index) =>
box(
{
width: '100%',
minHeight: 46,
flexDirection: 'row',
alignItems: 'center',
marginBottom: 12,
backgroundColor: theme.panel,
padding: '11px 14px'
},
[
TextBlock(String(index + start).padStart(2, '0'), {
width: 48,
color: theme.primary,
fontSize: 18,
fontWeight: 850
}),
TextBlock(item, {
flex: 1,
color: theme.text,
fontSize: 20,
fontWeight: 650,
lineHeight: 1.15
})
]
)
)
}
function smallCard(label, value, theme, style = {}) {
return box(
{
width: 184,
minHeight: 112,
flexDirection: 'column',
backgroundColor: theme.panel,
padding: 18,
...style
},
[
TextBlock(label, { color: theme.muted, fontSize: 15, fontWeight: 700, marginBottom: 14 }),
TextBlock(value, { color: theme.text, fontSize: 25, fontWeight: 850, lineHeight: 1.05 })
]
)
}
function coverHero(spec) {
const theme = colors(spec)
const chips = list(spec, 'chips').slice(0, 4)
return box(
{
width: CANVAS.width,
height: CANVAS.height,
position: 'relative',
flexDirection: 'column',
backgroundColor: theme.background,
color: theme.text,
fontFamily: DEFAULT_FONT_FAMILY,
padding: 72
},
[
box({
position: 'absolute',
left: 724,
top: 36,
width: 192,
height: 192,
borderRadius: 96,
backgroundColor: theme.accent,
opacity: 0.28
}),
box({
width: 704,
minHeight: 356,
flexDirection: 'column',
backgroundColor: theme.panel,
opacity: 0.96,
padding: 28
}, [
Badge(text(spec, 'eyebrow', 'SVGLIDE ARTBOARD'), {
color: theme.primary,
marginBottom: 18
}),
Title(text(spec, 'title', 'Untitled'), {
color: theme.text,
fontSize: 58,
fontWeight: 800,
lineHeight: 1.05,
marginBottom: 20
}),
Subtitle(text(spec, 'subtitle', ''), {
color: theme.muted,
fontSize: 24,
fontWeight: 500,
lineHeight: 1.25
})
]),
box(
{
position: 'absolute',
left: 84,
top: 444,
flexDirection: 'row',
gap: 14
},
chips.map((chip) =>
Chip(chip, {
backgroundColor: theme.primary,
color: theme.text,
opacity: 0.86
})
)
)
]
)
}
function comparisonCards(spec) {
const theme = colors(spec)
const leftPoints = list(spec, 'left_points').slice(0, 3)
const rightPoints = list(spec, 'right_points').slice(0, 3)
const point = (value, color) =>
box({ flexDirection: 'row', alignItems: 'center', marginBottom: 18 }, [
box({ width: 10, height: 10, borderRadius: 5, backgroundColor: color, marginRight: 14 }),
TextBlock(value, { color: theme.muted, fontSize: 20, fontWeight: 500, lineHeight: 1.2 })
])
return box(
{
width: CANVAS.width,
height: CANVAS.height,
position: 'relative',
flexDirection: 'column',
backgroundColor: theme.background,
color: theme.text,
fontFamily: DEFAULT_FONT_FAMILY,
padding: '52px 64px'
},
[
Title(text(spec, 'title', 'Comparison'), { color: theme.text, fontSize: 40, lineHeight: 1.1, marginBottom: 44 }),
box({ flexDirection: 'row', gap: 52 }, [
box({ width: 390, height: 250, flexDirection: 'column', backgroundColor: theme.panel, padding: 28 }, [
Title(text(spec, 'left_title', 'Before'), { color: theme.primary, fontSize: 24, lineHeight: 1.1, marginBottom: 28 }),
...leftPoints.map((item) => point(item, theme.primary))
]),
box({ width: 390, height: 250, flexDirection: 'column', backgroundColor: theme.panel, padding: 28 }, [
Title(text(spec, 'right_title', 'After'), { color: theme.accent, fontSize: 24, lineHeight: 1.1, marginBottom: 28 }),
...rightPoints.map((item) => point(item, theme.accent))
])
]),
TextBlock(text(spec, 'conclusion', ''), {
position: 'absolute',
left: 64,
top: 414,
width: 832,
height: 66,
padding: '20px 22px',
backgroundColor: theme.primary,
color: theme.text,
opacity: 0.88,
fontSize: 22,
fontWeight: 700
})
]
)
}
function summaryFinal(spec) {
const theme = colors(spec)
const takeaways = list(spec, 'takeaways').slice(0, 3)
return box(
{
width: CANVAS.width,
height: CANVAS.height,
position: 'relative',
flexDirection: 'column',
backgroundColor: theme.background,
color: theme.text,
fontFamily: DEFAULT_FONT_FAMILY,
padding: '64px 72px'
},
[
box({ position: 'absolute', left: 704, top: 54, width: 164, height: 164, borderRadius: 82, backgroundColor: theme.accent, opacity: 0.22 }),
box({ position: 'absolute', left: 712, top: 286, flexDirection: 'row', alignItems: 'flex-end', gap: 12 }, [
box({ width: 18, height: 30, backgroundColor: theme.primary, opacity: 0.72 }),
box({ width: 18, height: 48, backgroundColor: theme.primary, opacity: 0.86 }),
box({ width: 18, height: 66, backgroundColor: theme.accent, opacity: 0.92 })
]),
Badge(text(spec, 'eyebrow', 'SUMMARY'), { color: theme.primary, fontSize: 18, fontWeight: 800, marginBottom: 24 }),
Title(text(spec, 'title', 'Summary'), { width: 700, color: theme.text, fontSize: 50, fontWeight: 850, lineHeight: 1.08, marginBottom: 24 }),
Subtitle(text(spec, 'subtitle', ''), { width: 640, color: theme.muted, fontSize: 23, marginBottom: 34 }),
box(
{ flexDirection: 'row', gap: 18 },
takeaways.map((item, index) =>
StatCard({
index: index + 1,
label: item,
color: theme.primary,
textColor: theme.text,
panelColor: theme.panel
})
)
)
]
)
}
function sectionTitle(spec) {
const theme = colors(spec)
return pageShell(spec, [
box({ position: 'absolute', left: 72, top: 116, width: 8, height: 258, backgroundColor: theme.primary }),
box({ position: 'absolute', left: 734, top: 74, width: 148, height: 148, backgroundColor: theme.accent, opacity: 0.2 }),
box({ position: 'absolute', left: 734, top: 242, width: 148, height: 12, backgroundColor: theme.primary }),
box({ marginLeft: 52, marginTop: 64 }, [pageHeader(spec, { titleWidth: 690, titleSize: 56 })])
])
}
function agendaList(spec) {
const theme = colors(spec)
const items = firstList(spec, ['items', 'takeaways'], ['Context', 'Evidence', 'Decision']).slice(0, 6)
return pageShell(spec, [
pageHeader(spec, { titleWidth: 760, titleSize: 42 }),
box({ width: 724, flexDirection: 'column' }, numberedRows(items, theme, { max: 6 })),
box({ position: 'absolute', right: 56, top: 126, width: 112, height: 310, backgroundColor: theme.primary, opacity: 0.12 })
])
}
function timelineSteps(spec) {
const theme = colors(spec)
const events = firstList(spec, ['events', 'steps', 'items'], ['Discover', 'Design', 'Deliver', 'Measure']).slice(0, 5)
return pageShell(spec, [
pageHeader(spec, { titleWidth: 760, titleSize: 40 }),
box({ position: 'absolute', left: 110, top: 330, width: 740, height: 4, backgroundColor: theme.primary, opacity: 0.55 }),
box(
{ position: 'absolute', left: 96, top: 254, flexDirection: 'row', gap: 22 },
events.map((event, index) =>
box({ width: 130, flexDirection: 'column', alignItems: 'center' }, [
TextBlock(String(index + 1).padStart(2, '0'), {
width: 52,
height: 52,
color: theme.text,
backgroundColor: index % 2 ? theme.accent : theme.primary,
fontSize: 20,
fontWeight: 850,
padding: '14px 0',
textAlign: 'center',
marginBottom: 18
}),
TextBlock(event, { color: theme.text, fontSize: 18, fontWeight: 700, textAlign: 'center', lineHeight: 1.18 })
])
)
)
])
}
function processFlow(spec) {
const theme = colors(spec)
const steps = firstList(spec, ['steps', 'items'], ['Input', 'Normalize', 'Render', 'Verify']).slice(0, 5)
return pageShell(spec, [
pageHeader(spec, { titleWidth: 730, titleSize: 40 }),
box(
{ flexDirection: 'row', gap: 18, marginTop: 26 },
steps.map((step, index) =>
box({ width: 154, height: 172, flexDirection: 'column', backgroundColor: theme.panel, padding: 18 }, [
TextBlock(String(index + 1), { color: theme.primary, fontSize: 28, fontWeight: 900, marginBottom: 20 }),
TextBlock(step, { color: theme.text, fontSize: 21, fontWeight: 750, lineHeight: 1.15 }),
box({ width: 48, height: 5, backgroundColor: index % 2 ? theme.accent : theme.primary, marginTop: 'auto' })
])
)
),
TextBlock(text(spec, 'conclusion', ''), {
position: 'absolute',
left: 74,
bottom: 50,
width: 812,
minHeight: 48,
color: theme.text,
backgroundColor: theme.primary,
opacity: 0.18,
fontSize: 20,
fontWeight: 750,
padding: 14
})
])
}
function metricDashboard(spec) {
const theme = colors(spec)
const metrics = firstList(spec, ['metrics', 'items'], ['Velocity +32%', 'Cost -18%', 'Quality 96%', 'Reach 4.2x']).slice(0, 6)
return pageShell(spec, [
pageHeader(spec, { titleWidth: 710, titleSize: 38 }),
box(
{ flexDirection: 'row', flexWrap: 'wrap', gap: 18, marginTop: 6 },
metrics.map((metric, index) => smallCard(`METRIC ${index + 1}`, metric, theme))
)
])
}
function quoteFocus(spec) {
const theme = colors(spec)
return pageShell(spec, [
TextBlock('“', { position: 'absolute', left: 60, top: 36, color: theme.primary, fontSize: 132, fontWeight: 900, opacity: 0.7 }),
TextBlock(text(spec, 'quote', text(spec, 'title', 'A strong point belongs on a quiet page.')), {
width: 720,
marginTop: 116,
marginLeft: 72,
color: theme.text,
fontSize: 42,
fontWeight: 850,
lineHeight: 1.13
}),
TextBlock(text(spec, 'attribution', ''), {
marginLeft: 76,
marginTop: 34,
color: theme.muted,
fontSize: 22,
fontWeight: 700
}),
box({ position: 'absolute', right: 80, bottom: 72, width: 150, height: 10, backgroundColor: theme.accent })
])
}
function imageFeature(spec) {
const theme = colors(spec)
const points = firstList(spec, ['points', 'items'], ['Primary visual anchor', 'Caption explains evidence', 'Text stays out of the image']).slice(0, 3)
return pageShell(spec, [
box({ position: 'absolute', left: 56, top: 56, width: 452, height: 428, backgroundColor: theme.panel }),
box({ position: 'absolute', left: 86, top: 86, width: 392, height: 268, backgroundColor: theme.primary, opacity: 0.18 }),
TextBlock(text(spec, 'image_label', 'IMAGE'), { position: 'absolute', left: 226, top: 204, color: theme.primary, fontSize: 28, fontWeight: 900 }),
TextBlock(text(spec, 'caption', ''), { position: 'absolute', left: 86, top: 386, width: 388, color: theme.muted, fontSize: 19, fontWeight: 650 }),
box({ position: 'absolute', left: 548, top: 72, width: 330 }, [pageHeader(spec, { titleWidth: 330, titleSize: 38 })]),
box({ position: 'absolute', left: 552, top: 280, width: 324, flexDirection: 'column' }, numberedRows(points, theme, { max: 3 }))
])
}
function researchPoster(spec) {
const theme = colors(spec)
const sections = firstList(spec, ['sections', 'items'], ['Context', 'Method', 'Result', 'Implication']).slice(0, 6)
return pageShell(spec, [
box({ position: 'absolute', left: 56, top: 42, width: 588 }, [pageHeader(spec, { titleWidth: 588, titleSize: 34, subtitleKey: 'authors' })]),
box({ position: 'absolute', right: 70, top: 54, width: 140, height: 96, backgroundColor: theme.primary, opacity: 0.18 }),
box(
{ position: 'absolute', left: 58, top: 194, flexDirection: 'row', gap: 20 },
[0, 1, 2].map((column) =>
box(
{ width: 268, flexDirection: 'column', gap: 14 },
sections.slice(column * 2, column * 2 + 2).map((section, index) =>
box({ height: 120, flexDirection: 'column', backgroundColor: theme.panel, padding: 16 }, [
TextBlock(section, { color: theme.primary, fontSize: 20, fontWeight: 850, marginBottom: 12 }),
TextBlock(column === 1 && index === 0 ? text(spec, 'key_visual', 'key visual') : 'Evidence block', {
color: theme.muted,
fontSize: 17,
fontWeight: 600
})
])
)
)
)
)
])
}
function dataStory(spec) {
const theme = colors(spec)
const metrics = firstList(spec, ['metrics', 'items'], ['North 42', 'South 35', 'West 28', 'East 19']).slice(0, 4)
return pageShell(spec, [
pageHeader(spec, { titleWidth: 600, titleSize: 38 }),
box({ position: 'absolute', left: 86, top: 260, flexDirection: 'row', alignItems: 'flex-end', gap: 34 }, metrics.map((metric, index) =>
box({ width: 112, flexDirection: 'column', alignItems: 'center' }, [
box({ width: 64, height: 82 + index * 28, backgroundColor: index % 2 ? theme.accent : theme.primary, marginBottom: 18 }),
TextBlock(metric, { color: theme.text, fontSize: 18, fontWeight: 750, textAlign: 'center' })
])
)),
TextBlock(text(spec, 'callout', ''), { position: 'absolute', right: 72, top: 184, width: 260, color: theme.text, backgroundColor: theme.panel, fontSize: 24, fontWeight: 850, lineHeight: 1.14, padding: 22 })
])
}
function riskAlert(spec) {
const theme = colors(spec)
const risks = firstList(spec, ['risks', 'items'], ['Scope drift', 'Dependency delay', 'Insufficient evidence']).slice(0, 4)
return pageShell(spec, [
TextBlock(text(spec, 'severity', 'L2'), { position: 'absolute', right: 70, top: 54, color: theme.text, backgroundColor: theme.primary, fontSize: 28, fontWeight: 900, padding: '14px 22px' }),
pageHeader(spec, { titleWidth: 690, titleSize: 40 }),
box({ width: 800, flexDirection: 'column', marginTop: 16 }, risks.map((risk, index) =>
box({ height: 58, flexDirection: 'row', alignItems: 'center', backgroundColor: theme.panel, marginBottom: 14, padding: 16 }, [
box({ width: 12, height: 34, backgroundColor: index === 0 ? theme.accent : theme.primary, marginRight: 16 }),
TextBlock(risk, { color: theme.text, fontSize: 22, fontWeight: 760 })
])
)),
TextBlock(text(spec, 'summary', ''), { color: theme.muted, fontSize: 18, fontWeight: 650, marginTop: 6 })
])
}
function roadmapLanes(spec) {
const theme = colors(spec)
const lanes = firstList(spec, ['lanes', 'items'], ['Now', 'Next', 'Later']).slice(0, 4)
return pageShell(spec, [
pageHeader(spec, { titleWidth: 700, titleSize: 38 }),
box({ flexDirection: 'column', gap: 16, marginTop: 16 }, lanes.map((lane, index) =>
box({ width: 820, height: 62, flexDirection: 'row', alignItems: 'center', backgroundColor: theme.panel, padding: '0 18px' }, [
TextBlock(lane, { width: 132, color: theme.primary, fontSize: 21, fontWeight: 850 }),
box({ flex: 1, height: 12, backgroundColor: index % 2 ? theme.accent : theme.primary, opacity: 0.38 }),
TextBlock(`Q${index + 1}`, { width: 54, color: theme.text, fontSize: 18, fontWeight: 800, textAlign: 'right' })
])
))
])
}
function architectureBlueprint(spec) {
const theme = colors(spec)
const nodes = firstList(spec, ['nodes', 'items'], ['Planner', 'CanvasSpec', 'Renderer', 'SVGlide']).slice(0, 6)
return pageShell(spec, [
pageHeader(spec, { titleWidth: 630, titleSize: 36 }),
box(
{ position: 'absolute', left: 86, top: 240, flexDirection: 'row', flexWrap: 'wrap', gap: 24, width: 780 },
nodes.map((item, index) =>
box({ width: 236, height: 72, backgroundColor: theme.panel, borderWidth: 2, borderColor: index % 2 ? theme.accent : theme.primary, padding: 16 }, [
TextBlock(item, { color: theme.text, fontSize: 20, fontWeight: 800 })
])
)
)
])
}
export function renderTree(spec) {
if (spec.template_id === 'cover-hero') return coverHero(spec)
if (spec.template_id === 'comparison-cards') return comparisonCards(spec)
if (spec.template_id === 'summary-final') return summaryFinal(spec)
if (spec.template_id === 'section-title') return sectionTitle(spec)
if (spec.template_id === 'agenda-list') return agendaList(spec)
if (spec.template_id === 'timeline-steps') return timelineSteps(spec)
if (spec.template_id === 'process-flow') return processFlow(spec)
if (spec.template_id === 'metric-dashboard') return metricDashboard(spec)
if (spec.template_id === 'quote-focus') return quoteFocus(spec)
if (spec.template_id === 'image-feature') return imageFeature(spec)
if (spec.template_id === 'research-poster') return researchPoster(spec)
if (spec.template_id === 'data-story') return dataStory(spec)
if (spec.template_id === 'risk-alert') return riskAlert(spec)
if (spec.template_id === 'roadmap-lanes') return roadmapLanes(spec)
if (spec.template_id === 'architecture-blueprint') return architectureBlueprint(spec)
throw new Error(`unsupported template_id for Satori adapter: ${spec.template_id}`)
}

View File

@@ -0,0 +1,22 @@
{
"theme_id": "blueprint-technical",
"colors": {
"background": "#071827",
"panel": "#0E2A3F",
"primary": "#5EEAD4",
"accent": "#F97316",
"text": "#EAF6FF",
"muted": "#93B4C8"
},
"typography": {
"title": 54,
"subtitle": 23,
"body": 20,
"label": 18
},
"spacing": {
"page": 64,
"card": 28,
"gap": 20
}
}

View File

@@ -0,0 +1,22 @@
{
"theme_id": "cobalt-grid",
"colors": {
"background": "#081C4A",
"panel": "#102C6B",
"primary": "#60A5FA",
"accent": "#FDE047",
"text": "#EEF6FF",
"muted": "#B7C8E8"
},
"typography": {
"title": 56,
"subtitle": 24,
"body": 20,
"label": 18
},
"spacing": {
"page": 64,
"card": 28,
"gap": 20
}
}

View File

@@ -0,0 +1,22 @@
{
"theme_id": "dark-clarity",
"colors": {
"background": "#0F172A",
"panel": "#111827",
"primary": "#38BDF8",
"accent": "#A78BFA",
"text": "#F8FAFC",
"muted": "#CBD5E1"
},
"typography": {
"title": 58,
"subtitle": 24,
"body": 20,
"label": 18
},
"spacing": {
"page": 64,
"card": 28,
"gap": 20
}
}

View File

@@ -0,0 +1,22 @@
{
"theme_id": "editorial-tritone",
"colors": {
"background": "#F6E7DF",
"panel": "#FFF7F0",
"primary": "#9F1239",
"accent": "#D97706",
"text": "#351A24",
"muted": "#7C5B62"
},
"typography": {
"title": 52,
"subtitle": 23,
"body": 20,
"label": 18
},
"spacing": {
"page": 64,
"card": 28,
"gap": 20
}
}

View File

@@ -0,0 +1,22 @@
{
"theme_id": "finance-dark",
"colors": {
"background": "#07110E",
"panel": "#10201A",
"primary": "#22C55E",
"accent": "#F59E0B",
"text": "#ECFDF5",
"muted": "#A7C4B7"
},
"typography": {
"title": 54,
"subtitle": 23,
"body": 20,
"label": 18
},
"spacing": {
"page": 64,
"card": 28,
"gap": 20
}
}

View File

@@ -0,0 +1,22 @@
{
"theme_id": "forest-signal",
"colors": {
"background": "#0B1F1A",
"panel": "#123329",
"primary": "#34D399",
"accent": "#FBBF24",
"text": "#F7FCEB",
"muted": "#B7D3C6"
},
"typography": {
"title": 56,
"subtitle": 24,
"body": 20,
"label": 18
},
"spacing": {
"page": 64,
"card": 28,
"gap": 20
}
}

View File

@@ -0,0 +1,22 @@
{
"theme_id": "glass-neon",
"colors": {
"background": "#090B1A",
"panel": "#14172E",
"primary": "#22D3EE",
"accent": "#C084FC",
"text": "#F8FAFC",
"muted": "#BAC3D9"
},
"typography": {
"title": 56,
"subtitle": 24,
"body": 20,
"label": 18
},
"spacing": {
"page": 64,
"card": 28,
"gap": 20
}
}

View File

@@ -0,0 +1,22 @@
{
"theme_id": "paper-research",
"colors": {
"background": "#F7F3E8",
"panel": "#FFFDF6",
"primary": "#1E3A8A",
"accent": "#B45309",
"text": "#1F2937",
"muted": "#6B7280"
},
"typography": {
"title": 52,
"subtitle": 22,
"body": 20,
"label": 18
},
"spacing": {
"page": 64,
"card": 28,
"gap": 20
}
}

View File

@@ -0,0 +1,55 @@
{
"schema_version": "svglide-theme-registry/v1",
"themes": [
{
"id": "dark-clarity",
"status": "active",
"path": "skills/lark-slides/scripts/artboard_renderer/themes/dark-clarity.json"
},
{
"id": "forest-signal",
"status": "active",
"path": "skills/lark-slides/scripts/artboard_renderer/themes/forest-signal.json"
},
{
"id": "warm-editorial",
"status": "active",
"path": "skills/lark-slides/scripts/artboard_renderer/themes/warm-editorial.json"
},
{
"id": "blueprint-technical",
"status": "active",
"path": "skills/lark-slides/scripts/artboard_renderer/themes/blueprint-technical.json"
},
{
"id": "editorial-tritone",
"status": "active",
"path": "skills/lark-slides/scripts/artboard_renderer/themes/editorial-tritone.json"
},
{
"id": "cobalt-grid",
"status": "active",
"path": "skills/lark-slides/scripts/artboard_renderer/themes/cobalt-grid.json"
},
{
"id": "finance-dark",
"status": "active",
"path": "skills/lark-slides/scripts/artboard_renderer/themes/finance-dark.json"
},
{
"id": "swiss-red",
"status": "active",
"path": "skills/lark-slides/scripts/artboard_renderer/themes/swiss-red.json"
},
{
"id": "glass-neon",
"status": "active",
"path": "skills/lark-slides/scripts/artboard_renderer/themes/glass-neon.json"
},
{
"id": "paper-research",
"status": "active",
"path": "skills/lark-slides/scripts/artboard_renderer/themes/paper-research.json"
}
]
}

View File

@@ -0,0 +1,22 @@
{
"theme_id": "swiss-red",
"colors": {
"background": "#F8F8F4",
"panel": "#FFFFFF",
"primary": "#E11D48",
"accent": "#111111",
"text": "#111111",
"muted": "#666666"
},
"typography": {
"title": 52,
"subtitle": 22,
"body": 20,
"label": 18
},
"spacing": {
"page": 64,
"card": 28,
"gap": 20
}
}

View File

@@ -0,0 +1,22 @@
{
"theme_id": "warm-editorial",
"colors": {
"background": "#27130F",
"panel": "#3A211B",
"primary": "#F97316",
"accent": "#22D3EE",
"text": "#FFF7ED",
"muted": "#F4C9A8"
},
"typography": {
"title": 54,
"subtitle": 23,
"body": 20,
"label": 18
},
"spacing": {
"page": 64,
"card": 28,
"gap": 20
}
}

View File

@@ -0,0 +1,47 @@
{
"schema_version": "svglide-deck-plan/v1",
"topic": "冰岛火山研究",
"audience": "地学研究汇报听众",
"objective": "用三页说明研究背景、证据对比和下一步观测重点。",
"target_slide_count": 3,
"narrative_arc": ["建立研究对象", "比较观测证据", "收束行动重点"],
"theme_direction": {
"preferred_theme_ids": ["blueprint-technical", "paper-research", "warm-editorial"],
"visual_identity": "冷色科研蓝图结合火山暖色强调。",
"tone": "克制、研究型、可审查"
},
"constraints": {
"generation_mode": "artboard_satori",
"source_policy": "只使用用户给定主题,不编造具体数值。",
"forbidden_outputs": ["free_html", "free_css", "free_svg", "markdown_fence", "base64_image_data"]
},
"slides": [
{
"page": 1,
"title": "冰岛火山研究",
"role": "cover",
"key_message": "研究从地貌、活动迹象和观测路线展开。",
"content_goal": "建立研究对象和语境。",
"visual_goal": "使用封面模板给出冷色科研气质。",
"allowed_template_ids": ["cover-hero", "section-title"]
},
{
"page": 2,
"title": "观测证据对比",
"role": "evidence",
"key_message": "不同观测来源共同约束火山活动判断。",
"content_goal": "把地表和地下信号分开比较。",
"visual_goal": "使用左右对比保持信息可扫读。",
"allowed_template_ids": ["comparison-cards"]
},
{
"page": 3,
"title": "下一步观测",
"role": "closing",
"key_message": "后续重点是连续监测和风险沟通。",
"content_goal": "收束为三个行动方向。",
"visual_goal": "使用总结页形成清晰结尾。",
"allowed_template_ids": ["summary-final"]
}
]
}

View File

@@ -0,0 +1,13 @@
{
"schema_version": "svglide-repair-plan/v1",
"target_plan_path": "02-plan/slide_plan.json",
"change_reason": "示例修复标题预算风险,只修改单个 CanvasSpec 字段。",
"patches": [
{
"op": "replace",
"path": "/slides/1/canvas_spec/content/conclusion",
"value": "多源观测共同降低单一证据偏差。",
"reason": "收短结论文案以满足 comparison-cards text_budget。"
}
]
}

View File

@@ -0,0 +1,37 @@
{
"schema_version": "svglide-slide-plan/v1",
"deck_plan_ref": {"path": "02-plan/deck-plan.json"},
"generation_mode": "artboard_satori",
"slides": [
{
"page": 1,
"title": "冰岛火山研究",
"key_message": "研究从地貌、活动迹象和观测路线展开。",
"template_id": "cover-hero",
"theme_id": "blueprint-technical",
"content_requirements": {"eyebrow": "ICELAND VOLCANO", "subtitle": "地貌证据、活动迹象与连续观测。", "chips": ["地貌", "监测", "风险"]},
"visual_role": "research cover",
"source_policy": "topic-provided; no invented metrics"
},
{
"page": 2,
"title": "观测证据对比",
"key_message": "不同观测来源共同约束火山活动判断。",
"template_id": "comparison-cards",
"theme_id": "paper-research",
"content_requirements": {"left_title": "地表信号", "right_title": "地下信号", "left_points": ["熔岩地貌", "热异常", "气体羽流"], "right_points": ["地震活动", "形变趋势", "岩浆补给"]},
"visual_role": "evidence comparison",
"source_policy": "topic-derived categories; no numeric claims"
},
{
"page": 3,
"title": "下一步观测",
"key_message": "后续重点是连续监测和风险沟通。",
"template_id": "summary-final",
"theme_id": "warm-editorial",
"content_requirements": {"eyebrow": "NEXT", "subtitle": "把研究结果转成可执行的观测闭环。", "takeaways": ["连续监测", "现场复核", "风险沟通"]},
"visual_role": "closing summary",
"source_policy": "topic-derived actions"
}
]
}

View File

@@ -0,0 +1,131 @@
{
"schema_version": "svglide-canvas-plan/v1",
"route": "svglide-svg",
"generation_mode": "artboard_satori",
"page_count": 3,
"target_slide_count": 3,
"plan_path": "02-plan/slide_plan.json",
"style_preset": "cobalt_bloom",
"style_selection_reason": "冷色研究基调适合冰岛火山主题,暖色 accent 对应火山活动。",
"style_system": {
"palette": {"background": "#071827", "text": "#EAF6FF", "accent": "#F97316"},
"typography": "Satori-compatible static text hierarchy",
"background_strategy": "研究页使用深色蓝图背景,结尾页允许暖色强调。",
"motif": "科研蓝图线索和火山暖色标记"
},
"loaded_rule_set": [
"skills/lark-slides/references/svglide-planning-layer.md",
"skills/lark-slides/references/svglide-canvas-spec.schema.json",
"skills/lark-slides/references/svglide-template-registry.json"
],
"quality_gates": {"no_text_overflow": true, "no_debug_guides": true, "no_xml_like_pages": true},
"art_direction": {
"cover_treatment": "深色研究封面,用简洁标题和 chips 建立主题。",
"section_divider_treatment": "不设独立章节页,使用第二页证据对比作为节奏转换。",
"closing_treatment": "暖色总结页收束为三个行动方向。",
"deck_motif": "冰岛火山研究蓝图",
"svg_native_moments": ["封面 chips", "左右证据卡片", "总结 stat cards"]
},
"business_claims": [
{"claim": "不展示具体数值,避免编造研究数据。", "source_type": "derived", "derivation": "derived from the no-invented-metrics source policy"}
],
"slides": [
{
"page": 1,
"title": "冰岛火山研究",
"key_message": "研究从地貌、活动迹象和观测路线展开。",
"renderer_id": "artboard_satori.cover-hero",
"layout_family": "cover",
"visual_recipe": "hero_typography",
"visual_intent": "建立研究主题和冷色科研气质。",
"visual_focal_point": "大标题和研究标签。",
"visual_signature": "blueprint cover panel",
"svg_effects": ["typography"],
"required_primitives": ["typography", "geometric_shape"],
"svg_primitives": ["typography", "rect", "circle"],
"xml_like_risk": "普通标题页会丢失科研蓝图气质。",
"content_density_contract": "cover title plus 3 chips",
"risk_flags": [],
"source_policy": "topic-provided; no invented metrics",
"asset_contract": "none_required",
"canvas_spec": {
"version": "svglide-canvas-spec/v1",
"canvas": {"width": 960, "height": 540, "viewBox": "0 0 960 540"},
"safe_area": {"x": 48, "y": 40, "width": 864, "height": 460},
"template_id": "cover-hero",
"theme_id": "blueprint-technical",
"theme": {"colors": {"background": "#071827", "panel": "#0E2A3F", "primary": "#5EEAD4", "accent": "#F97316", "text": "#EAF6FF", "muted": "#93B4C8"}},
"content": {"eyebrow": "ICELAND VOLCANO", "title": "冰岛火山研究", "subtitle": "地貌证据、活动迹象与连续观测。", "chips": ["地貌", "监测", "风险"]},
"semantic_elements": [{"element_id": "title", "kind": "text", "role": "title", "source_ref": "canvas_spec.content.title", "bbox": {"x": 84, "y": 142, "width": 628, "height": 142}}],
"quality_constraints": {"max_title_lines": 2, "min_font_size": 18, "safe_area": {"x": 48, "y": 40, "width": 864, "height": 460}}
}
},
{
"page": 2,
"title": "观测证据对比",
"key_message": "不同观测来源共同约束火山活动判断。",
"renderer_id": "artboard_satori.comparison-cards",
"layout_family": "comparison",
"visual_recipe": "two_column_comparison",
"visual_intent": "区分地表和地下观测证据。",
"visual_focal_point": "左右证据卡片。",
"visual_signature": "research evidence cards",
"svg_effects": ["typography"],
"required_primitives": ["typography", "geometric_shape"],
"svg_primitives": ["typography", "rect", "circle"],
"xml_like_risk": "普通项目符号会弱化证据对照。",
"content_density_contract": "two cards with 3 points each",
"risk_flags": [],
"source_policy": "topic-derived categories; no numeric claims",
"asset_contract": "none_required",
"canvas_spec": {
"version": "svglide-canvas-spec/v1",
"canvas": {"width": 960, "height": 540, "viewBox": "0 0 960 540"},
"safe_area": {"x": 48, "y": 40, "width": 864, "height": 460},
"template_id": "comparison-cards",
"theme_id": "paper-research",
"theme": {"colors": {"background": "#F7F3E8", "panel": "#FFFDF6", "primary": "#1E3A8A", "accent": "#B45309", "text": "#1F2937", "muted": "#6B7280"}},
"content": {
"title": "观测证据对比",
"left_title": "地表信号",
"right_title": "地下信号",
"left_points": ["熔岩地貌", "热异常", "气体羽流"],
"right_points": ["地震活动", "形变趋势", "岩浆补给"],
"conclusion": "多源观测共同降低单一证据偏差。"
},
"semantic_elements": [{"element_id": "title", "kind": "text", "role": "title", "source_ref": "canvas_spec.content.title", "bbox": {"x": 64, "y": 52, "width": 760, "height": 64}}],
"quality_constraints": {"max_title_lines": 2, "min_font_size": 18, "safe_area": {"x": 48, "y": 40, "width": 864, "height": 460}}
}
},
{
"page": 3,
"title": "下一步观测",
"key_message": "后续重点是连续监测和风险沟通。",
"renderer_id": "artboard_satori.summary-final",
"layout_family": "closing",
"visual_recipe": "summary_cards",
"visual_intent": "把研究结果收束成行动闭环。",
"visual_focal_point": "三个行动卡片。",
"visual_signature": "warm volcanic closing",
"svg_effects": ["typography"],
"required_primitives": ["typography", "geometric_shape"],
"svg_primitives": ["typography", "rect", "circle"],
"xml_like_risk": "普通总结页会缺少行动优先级。",
"content_density_contract": "3 takeaway cards",
"risk_flags": [],
"source_policy": "topic-derived actions",
"asset_contract": "none_required",
"canvas_spec": {
"version": "svglide-canvas-spec/v1",
"canvas": {"width": 960, "height": 540, "viewBox": "0 0 960 540"},
"safe_area": {"x": 48, "y": 40, "width": 864, "height": 460},
"template_id": "summary-final",
"theme_id": "warm-editorial",
"theme": {"colors": {"background": "#27130F", "panel": "#3A211B", "primary": "#F97316", "accent": "#22D3EE", "text": "#FFF7ED", "muted": "#F4C9A8"}},
"content": {"eyebrow": "NEXT", "title": "下一步观测", "subtitle": "把研究结果转成可执行的观测闭环。", "takeaways": ["连续监测", "现场复核", "风险沟通"]},
"semantic_elements": [{"element_id": "title", "kind": "text", "role": "title", "source_ref": "canvas_spec.content.title", "bbox": {"x": 72, "y": 110, "width": 700, "height": 110}}],
"quality_constraints": {"max_title_lines": 2, "min_font_size": 18, "safe_area": {"x": 48, "y": 40, "width": 864, "height": 460}}
}
}
]
}

View File

@@ -0,0 +1,72 @@
{
"canvas": {
"height": 540,
"viewBox": "0 0 960 540",
"width": 960
},
"content": {
"chips": [
"地貌",
"监测",
"风险"
],
"eyebrow": "ICELAND VOLCANO",
"subtitle": "地貌证据、活动迹象与连续观测。",
"title": "冰岛火山研究"
},
"quality_constraints": {
"max_title_lines": 2,
"min_font_size": 18,
"safe_area": {
"height": 460,
"width": 864,
"x": 48,
"y": 40
}
},
"safe_area": {
"height": 460,
"width": 864,
"x": 48,
"y": 40
},
"semantic_elements": [
{
"bbox": {
"height": 142,
"width": 628,
"x": 84,
"y": 142
},
"element_id": "title",
"kind": "text",
"role": "title",
"source_ref": "canvas_spec.content.title"
}
],
"template_id": "cover-hero",
"theme": {
"colors": {
"accent": "#F97316",
"background": "#071827",
"muted": "#93B4C8",
"panel": "#0E2A3F",
"primary": "#5EEAD4",
"text": "#EAF6FF"
},
"spacing": {
"card": 28,
"gap": 20,
"page": 64
},
"theme_id": "blueprint-technical",
"typography": {
"body": 20,
"label": 18,
"subtitle": 23,
"title": 54
}
},
"theme_id": "blueprint-technical",
"version": "svglide-canvas-spec/v1"
}

View File

@@ -0,0 +1,15 @@
<svg xmlns="http://www.w3.org/2000/svg" width="960" height="540" viewBox="0 0 960 540">
<rect data-node-id="background" x="0" y="0" width="960" height="540" fill="#071827"/>
<circle data-node-id="accent-orbit" cx="820" cy="132" r="96" fill="#F97316" opacity="0.28"/>
<circle data-node-id="accent-dot" cx="812" cy="150" r="72" fill="#5EEAD4" opacity="0.22"/>
<rect data-node-id="panel" x="56" y="64" width="704" height="356" fill="#0E2A3F" opacity="0.82"/>
<text data-node-id="eyebrow" data-box-x="84" data-box-y="98" data-box-width="420" data-box-height="32" x="84" y="116" fill="#5EEAD4" font-size="18" font-weight="700" font-family="Inter">ICELAND VOLCANO</text>
<text data-node-id="title" data-box-x="84" data-box-y="142" data-box-width="628" data-box-height="142" x="84" y="200" fill="#EAF6FF" font-size="58" font-weight="800" font-family="Inter">冰岛火山研究</text>
<text data-node-id="subtitle" data-box-x="88" data-box-y="302" data-box-width="610" data-box-height="74" x="88" y="326" fill="#93B4C8" font-size="24" font-weight="500" font-family="Inter">地貌证据、活动迹象与连续观测。</text>
<rect data-node-id="chip-1-bg" x="84" y="444" width="92" height="40" fill="#5EEAD4" opacity="0.16"/>
<text data-node-id="chip-1" data-box-x="99" data-box-y="450" data-box-width="62" data-box-height="30" x="99" y="471" fill="#EAF6FF" font-size="17" font-weight="600" font-family="Inter">地貌</text>
<rect data-node-id="chip-2-bg" x="190" y="444" width="92" height="40" fill="#5EEAD4" opacity="0.16"/>
<text data-node-id="chip-2" data-box-x="205" data-box-y="450" data-box-width="62" data-box-height="30" x="205" y="471" fill="#EAF6FF" font-size="17" font-weight="600" font-family="Inter">监测</text>
<rect data-node-id="chip-3-bg" x="296" y="444" width="92" height="40" fill="#5EEAD4" opacity="0.16"/>
<text data-node-id="chip-3" data-box-x="311" data-box-y="450" data-box-width="62" data-box-height="30" x="311" y="471" fill="#EAF6FF" font-size="17" font-weight="600" font-family="Inter">风险</text>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -0,0 +1,89 @@
{
"drift": {
"max_px": 0,
"status": "not_measured_in_p0"
},
"nodes": [
{
"height": 540,
"id": "background",
"kind": "rect",
"width": 960,
"x": 0,
"y": 0
},
{
"height": 192,
"id": "accent-orbit",
"kind": "circle",
"width": 192,
"x": 724,
"y": 36
},
{
"height": 356,
"id": "panel",
"kind": "rect",
"width": 704,
"x": 56,
"y": 64
},
{
"height": 32,
"id": "eyebrow",
"kind": "text",
"text": "ICELAND VOLCANO",
"width": 420,
"x": 84,
"y": 98
},
{
"height": 142,
"id": "title",
"kind": "text",
"text": "冰岛火山研究",
"width": 628,
"x": 84,
"y": 142
},
{
"height": 74,
"id": "subtitle",
"kind": "text",
"text": "地貌证据、活动迹象与连续观测。",
"width": 610,
"x": 88,
"y": 302
},
{
"height": 40,
"id": "chip-1",
"kind": "text",
"text": "地貌",
"width": 92,
"x": 84,
"y": 444
},
{
"height": 40,
"id": "chip-2",
"kind": "text",
"text": "监测",
"width": 92,
"x": 190,
"y": 444
},
{
"height": 40,
"id": "chip-3",
"kind": "text",
"text": "风险",
"width": 92,
"x": 296,
"y": 444
}
],
"page": 1,
"source": "template-layout-map",
"version": "svglide-node-layout-map/v1"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View File

@@ -0,0 +1,104 @@
{
"canvas_spec_path": "02-plan/slide_plan.json#/slides/0/canvas_spec",
"canvas_spec_sha256": "c217ffcfb547080e01de408d01933ed81f2ed1f9dc06de845e8a0dad7a19af02",
"canvas_template_svg": "04-svg/artboard/page-001.canvas-template.svg",
"canvas_template_svg_sha256": "a21538ac26cd5ce67d306ec05f45e7c6ee98b4ae5a954c6bb92414994df2abc2",
"compiler": {
"compiler_input": "CanvasSpecTemplateSVG",
"fail_fast": [
"clipPath",
"defs",
"filter",
"foreignObject",
"image",
"linearGradient",
"pattern",
"radialGradient",
"use"
],
"native_mapped": [
"rect",
"circle",
"circle",
"rect",
"text->foreignObject",
"text->foreignObject",
"text->foreignObject",
"rect",
"text->foreignObject",
"rect",
"text->foreignObject",
"rect",
"text->foreignObject"
],
"satori_svg_usage": "preview_only",
"semantic_source": "CanvasSpec"
},
"compiler_input": "04-svg/artboard/page-001.canvas-template.svg",
"compiler_input_sha256": "a21538ac26cd5ce67d306ec05f45e7c6ee98b4ae5a954c6bb92414994df2abc2",
"created_at": "2026-06-21T13:59:15+08:00",
"font_hashes": [
{
"path": "/System/Library/Fonts/Supplemental/Arial Unicode.ttf",
"sha256": "876af2cd4854644e7f3e7feb2f688997fdb3343c6df6693611209c9dfb47ccec"
}
],
"node_layout_map": "04-svg/artboard/page-001.node-layout-map.json",
"node_layout_map_sha256": "895b6d98ab57c2e5633b92513c4b06047e26411c4bf75bf8f248be5358752c86",
"node_version": "v20.18.1",
"page": 1,
"png": "04-svg/artboard/page-001.png",
"png_sha256": "83d25aa4149a7ea9131f459dd3446bdf5aacfebea5d3d83e57638a5ad97f7e11",
"render_metadata": "04-svg/artboard/page-001.render-metadata.json",
"render_metadata_sha256": "050b8900bfe64f3e7e08007a4f695096976000215233e0a5bfe9743ee4f7acf4",
"renderer": {
"actual_satori_package": true,
"adapter": "skills/lark-slides/scripts/artboard_renderer/dist/render.mjs",
"engine": "satori-node",
"name": "satori-resvg-p0"
},
"resvg_version": "2.6.2",
"satori_preview": {
"element_counts": {
"g": 8,
"mask": 10,
"path": 1,
"rect": 15,
"svg": 1,
"text": 30
},
"fail_fast": [
"clipPath",
"defs",
"filter",
"foreignObject",
"image",
"linearGradient",
"pattern",
"radialGradient",
"use"
],
"issues": [],
"status": "passed",
"strict": false
},
"satori_svg": "04-svg/artboard/raw/page-001.satori.svg",
"satori_svg_sha256": "700e2b829a7a00354739512182916d4566877165748e9e1fa8b3aeeafcb21540",
"satori_version": "0.26.0",
"semantic_map": "04-svg/artboard/page-001.semantic-map.json",
"semantic_map_sha256": "0ce8309c016ff337d9dd72c322e928f2541a66b2acaa097c321979265ca06df7",
"stage": "generate_svg",
"status": "passed",
"svglide_svg": "04-svg/page-001.svg",
"svglide_svg_sha256": "0bec84addb5a7b9319b71a573e0bd9de7890dcbe6985b61e3456e54950850fb3",
"template_id": "cover-hero",
"template_registry": "skills/lark-slides/references/svglide-template-registry.json",
"template_registry_sha256": "78c6639ee435ff8b8818f64f7b536967a7486281eacab5abce8e20626e20f6b3",
"theme_files": [
"skills/lark-slides/scripts/artboard_renderer/themes/blueprint-technical.json"
],
"theme_id": "blueprint-technical",
"theme_registry": "skills/lark-slides/scripts/artboard_renderer/themes/registry.json",
"theme_registry_sha256": "7ae629f17d38922702489ad3a23da26d465c3e6f5c284b409d4ab10a53ef6ed0",
"version": "svglide-artboard-receipt/v1"
}

View File

@@ -0,0 +1,7 @@
{
"node_version": "v20.18.1",
"satori_version": "0.26.0",
"resvg_version": "2.6.2",
"font_path": "/System/Library/Fonts/Supplemental/Arial Unicode.ttf",
"png_bytes": 26106
}

View File

@@ -0,0 +1,132 @@
{
"content_keys": [
"chips",
"eyebrow",
"subtitle",
"title"
],
"elements": [
{
"bbox": {
"height": 540.0,
"width": 960.0,
"x": 0.0,
"y": 0.0
},
"element_id": "background",
"kind": "rect",
"role": "background",
"source_ref": null,
"text": null
},
{
"bbox": {
"height": 192.0,
"width": 192.0,
"x": 724.0,
"y": 36.0
},
"element_id": "accent-orbit",
"kind": "circle",
"role": "decorative",
"source_ref": null,
"text": null
},
{
"bbox": {
"height": 356.0,
"width": 704.0,
"x": 56.0,
"y": 64.0
},
"element_id": "panel",
"kind": "rect",
"role": "container",
"source_ref": null,
"text": null
},
{
"bbox": {
"height": 32.0,
"width": 420.0,
"x": 84.0,
"y": 98.0
},
"element_id": "eyebrow",
"kind": "text",
"role": "eyebrow",
"source_ref": "canvas_spec.content.eyebrow",
"text": "ICELAND VOLCANO"
},
{
"bbox": {
"height": 142.0,
"width": 628.0,
"x": 84.0,
"y": 142.0
},
"element_id": "title",
"kind": "text",
"role": "title",
"source_ref": "canvas_spec.content.title",
"text": "冰岛火山研究"
},
{
"bbox": {
"height": 74.0,
"width": 610.0,
"x": 88.0,
"y": 302.0
},
"element_id": "subtitle",
"kind": "text",
"role": "subtitle",
"source_ref": "canvas_spec.content.subtitle",
"text": "地貌证据、活动迹象与连续观测。"
},
{
"bbox": {
"height": 40.0,
"width": 92.0,
"x": 84.0,
"y": 444.0
},
"element_id": "chip-1",
"kind": "text",
"role": "badge",
"source_ref": "canvas_spec.content.chips[]",
"text": "地貌"
},
{
"bbox": {
"height": 40.0,
"width": 92.0,
"x": 190.0,
"y": 444.0
},
"element_id": "chip-2",
"kind": "text",
"role": "badge",
"source_ref": "canvas_spec.content.chips[]",
"text": "监测"
},
{
"bbox": {
"height": 40.0,
"width": 92.0,
"x": 296.0,
"y": 444.0
},
"element_id": "chip-3",
"kind": "text",
"role": "badge",
"source_ref": "canvas_spec.content.chips[]",
"text": "风险"
}
],
"page": 1,
"semantic_source": "CanvasSpec",
"template_id": "cover-hero",
"theme_id": "blueprint-technical",
"version": "svglide-semantic-map/v1"
}

View File

@@ -0,0 +1,78 @@
{
"canvas": {
"height": 540,
"viewBox": "0 0 960 540",
"width": 960
},
"content": {
"conclusion": "多源观测共同降低单一证据偏差。",
"left_points": [
"熔岩地貌",
"热异常",
"气体羽流"
],
"left_title": "地表信号",
"right_points": [
"地震活动",
"形变趋势",
"岩浆补给"
],
"right_title": "地下信号",
"title": "观测证据对比"
},
"quality_constraints": {
"max_title_lines": 2,
"min_font_size": 18,
"safe_area": {
"height": 460,
"width": 864,
"x": 48,
"y": 40
}
},
"safe_area": {
"height": 460,
"width": 864,
"x": 48,
"y": 40
},
"semantic_elements": [
{
"bbox": {
"height": 64,
"width": 760,
"x": 64,
"y": 52
},
"element_id": "title",
"kind": "text",
"role": "title",
"source_ref": "canvas_spec.content.title"
}
],
"template_id": "comparison-cards",
"theme": {
"colors": {
"accent": "#B45309",
"background": "#F7F3E8",
"muted": "#6B7280",
"panel": "#FFFDF6",
"primary": "#1E3A8A",
"text": "#1F2937"
},
"spacing": {
"card": 28,
"gap": 20,
"page": 64
},
"theme_id": "paper-research",
"typography": {
"body": 20,
"label": 18,
"subtitle": 22,
"title": 52
}
},
"theme_id": "paper-research",
"version": "svglide-canvas-spec/v1"
}

View File

@@ -0,0 +1,22 @@
<svg xmlns="http://www.w3.org/2000/svg" width="960" height="540" viewBox="0 0 960 540">
<rect data-node-id="background" x="0" y="0" width="960" height="540" fill="#F7F3E8"/>
<text data-node-id="title" data-box-x="64" data-box-y="52" data-box-width="760" data-box-height="64" x="64" y="96" fill="#1F2937" font-size="40" font-weight="800" font-family="Inter">观测证据对比</text>
<rect data-node-id="left-card" x="64" y="140" width="390" height="250" fill="#FFFDF6" opacity="0.82"/>
<rect data-node-id="right-card" x="506" y="140" width="390" height="250" fill="#FFFDF6" opacity="0.82"/>
<text data-node-id="left-title" data-box-x="92" data-box-y="168" data-box-width="320" data-box-height="34" x="92" y="194" fill="#1E3A8A" font-size="24" font-weight="800" font-family="Inter">地表信号</text>
<text data-node-id="right-title" data-box-x="534" data-box-y="168" data-box-width="320" data-box-height="34" x="534" y="194" fill="#B45309" font-size="24" font-weight="800" font-family="Inter">地下信号</text>
<circle data-node-id="left-dot-1" cx="98" cy="241" r="5" fill="#1E3A8A"/>
<text data-node-id="left-point-1" data-box-x="116" data-box-y="222" data-box-width="296" data-box-height="36" x="116" y="248" fill="#6B7280" font-size="20" font-weight="500" font-family="Inter">熔岩地貌</text>
<circle data-node-id="left-dot-2" cx="98" cy="289" r="5" fill="#1E3A8A"/>
<text data-node-id="left-point-2" data-box-x="116" data-box-y="270" data-box-width="296" data-box-height="36" x="116" y="296" fill="#6B7280" font-size="20" font-weight="500" font-family="Inter">热异常</text>
<circle data-node-id="left-dot-3" cx="98" cy="337" r="5" fill="#1E3A8A"/>
<text data-node-id="left-point-3" data-box-x="116" data-box-y="318" data-box-width="296" data-box-height="36" x="116" y="344" fill="#6B7280" font-size="20" font-weight="500" font-family="Inter">气体羽流</text>
<circle data-node-id="right-dot-1" cx="540" cy="241" r="5" fill="#B45309"/>
<text data-node-id="right-point-1" data-box-x="558" data-box-y="222" data-box-width="296" data-box-height="36" x="558" y="248" fill="#6B7280" font-size="20" font-weight="500" font-family="Inter">地震活动</text>
<circle data-node-id="right-dot-2" cx="540" cy="289" r="5" fill="#B45309"/>
<text data-node-id="right-point-2" data-box-x="558" data-box-y="270" data-box-width="296" data-box-height="36" x="558" y="296" fill="#6B7280" font-size="20" font-weight="500" font-family="Inter">形变趋势</text>
<circle data-node-id="right-dot-3" cx="540" cy="337" r="5" fill="#B45309"/>
<text data-node-id="right-point-3" data-box-x="558" data-box-y="318" data-box-width="296" data-box-height="36" x="558" y="344" fill="#6B7280" font-size="20" font-weight="500" font-family="Inter">岩浆补给</text>
<rect data-node-id="conclusion-bg" x="64" y="414" width="832" height="66" fill="#1E3A8A" opacity="0.16"/>
<text data-node-id="conclusion" data-box-x="86" data-box-y="426" data-box-width="788" data-box-height="42" x="86" y="454" fill="#1F2937" font-size="22" font-weight="700" font-family="Inter">多源观测共同降低单一证据偏差。</text>
</svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@@ -0,0 +1,125 @@
{
"drift": {
"max_px": 0,
"status": "not_measured_in_p0"
},
"nodes": [
{
"height": 540,
"id": "background",
"kind": "rect",
"width": 960,
"x": 0,
"y": 0
},
{
"height": 64,
"id": "title",
"kind": "text",
"text": "观测证据对比",
"width": 760,
"x": 64,
"y": 52
},
{
"height": 250,
"id": "left-card",
"kind": "rect",
"width": 390,
"x": 64,
"y": 140
},
{
"height": 250,
"id": "right-card",
"kind": "rect",
"width": 390,
"x": 506,
"y": 140
},
{
"height": 34,
"id": "left-title",
"kind": "text",
"text": "地表信号",
"width": 320,
"x": 92,
"y": 168
},
{
"height": 36,
"id": "left-point-1",
"kind": "text",
"text": "熔岩地貌",
"width": 296,
"x": 116,
"y": 222
},
{
"height": 36,
"id": "left-point-2",
"kind": "text",
"text": "热异常",
"width": 296,
"x": 116,
"y": 270
},
{
"height": 36,
"id": "left-point-3",
"kind": "text",
"text": "气体羽流",
"width": 296,
"x": 116,
"y": 318
},
{
"height": 34,
"id": "right-title",
"kind": "text",
"text": "地下信号",
"width": 320,
"x": 534,
"y": 168
},
{
"height": 36,
"id": "right-point-1",
"kind": "text",
"text": "地震活动",
"width": 296,
"x": 558,
"y": 222
},
{
"height": 36,
"id": "right-point-2",
"kind": "text",
"text": "形变趋势",
"width": 296,
"x": 558,
"y": 270
},
{
"height": 36,
"id": "right-point-3",
"kind": "text",
"text": "岩浆补给",
"width": 296,
"x": 558,
"y": 318
},
{
"height": 42,
"id": "conclusion",
"kind": "text",
"text": "多源观测共同降低单一证据偏差。",
"width": 788,
"x": 86,
"y": 426
}
],
"page": 2,
"source": "template-layout-map",
"version": "svglide-node-layout-map/v1"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View File

@@ -0,0 +1,111 @@
{
"canvas_spec_path": "02-plan/slide_plan.json#/slides/1/canvas_spec",
"canvas_spec_sha256": "71c31d9e39a828f37a38c95912be4ec79343a4101072afede90384d5454d2270",
"canvas_template_svg": "04-svg/artboard/page-002.canvas-template.svg",
"canvas_template_svg_sha256": "acb7c3b4563b96ff052ca745c2ee43de958c2025cee815cc49b92611b3fbd82b",
"compiler": {
"compiler_input": "CanvasSpecTemplateSVG",
"fail_fast": [
"clipPath",
"defs",
"filter",
"foreignObject",
"image",
"linearGradient",
"pattern",
"radialGradient",
"use"
],
"native_mapped": [
"rect",
"text->foreignObject",
"rect",
"rect",
"text->foreignObject",
"text->foreignObject",
"circle",
"text->foreignObject",
"circle",
"text->foreignObject",
"circle",
"text->foreignObject",
"circle",
"text->foreignObject",
"circle",
"text->foreignObject",
"circle",
"text->foreignObject",
"rect",
"text->foreignObject"
],
"satori_svg_usage": "preview_only",
"semantic_source": "CanvasSpec"
},
"compiler_input": "04-svg/artboard/page-002.canvas-template.svg",
"compiler_input_sha256": "acb7c3b4563b96ff052ca745c2ee43de958c2025cee815cc49b92611b3fbd82b",
"created_at": "2026-06-21T13:59:15+08:00",
"font_hashes": [
{
"path": "/System/Library/Fonts/Supplemental/Arial Unicode.ttf",
"sha256": "876af2cd4854644e7f3e7feb2f688997fdb3343c6df6693611209c9dfb47ccec"
}
],
"node_layout_map": "04-svg/artboard/page-002.node-layout-map.json",
"node_layout_map_sha256": "1ad67ee48e5a22981c1432d858587ff346ac6069b4376a52990f16bce3e16c5e",
"node_version": "v20.18.1",
"page": 2,
"png": "04-svg/artboard/page-002.png",
"png_sha256": "ba705a55dc18aaef73144e01197fd75dba6fe37d27eb36897df95683f13161a7",
"render_metadata": "04-svg/artboard/page-002.render-metadata.json",
"render_metadata_sha256": "cc88b7d0489199a4aee08a29316b247fdd6cccb1df4fdec5f4e7be6dcbbb5a34",
"renderer": {
"actual_satori_package": true,
"adapter": "skills/lark-slides/scripts/artboard_renderer/dist/render.mjs",
"engine": "satori-node",
"name": "satori-resvg-p0"
},
"resvg_version": "2.6.2",
"satori_preview": {
"element_counts": {
"g": 1,
"mask": 26,
"path": 6,
"rect": 30,
"svg": 1,
"text": 52
},
"fail_fast": [
"clipPath",
"defs",
"filter",
"foreignObject",
"image",
"linearGradient",
"pattern",
"radialGradient",
"use"
],
"issues": [],
"status": "passed",
"strict": false
},
"satori_svg": "04-svg/artboard/raw/page-002.satori.svg",
"satori_svg_sha256": "5979d2dd93d09936d216d871929aacb5d06eecab43d34a2e0d8070991d1ed88a",
"satori_version": "0.26.0",
"semantic_map": "04-svg/artboard/page-002.semantic-map.json",
"semantic_map_sha256": "61fb537e82510e1fcada54f9f043b4746c1aeb10553391e4778cd6b76aeca520",
"stage": "generate_svg",
"status": "passed",
"svglide_svg": "04-svg/page-002.svg",
"svglide_svg_sha256": "3c539aa631b9240c89e3d36411a2f74f0007eba48f02aaa9bda1fff98b87481d",
"template_id": "comparison-cards",
"template_registry": "skills/lark-slides/references/svglide-template-registry.json",
"template_registry_sha256": "78c6639ee435ff8b8818f64f7b536967a7486281eacab5abce8e20626e20f6b3",
"theme_files": [
"skills/lark-slides/scripts/artboard_renderer/themes/paper-research.json"
],
"theme_id": "paper-research",
"theme_registry": "skills/lark-slides/scripts/artboard_renderer/themes/registry.json",
"theme_registry_sha256": "85be63912ba33fd278b2bff4471ac7a1e6761d67292dc1a1a374b4fd7bd5edfd",
"version": "svglide-artboard-receipt/v1"
}

View File

@@ -0,0 +1,7 @@
{
"node_version": "v20.18.1",
"satori_version": "0.26.0",
"resvg_version": "2.6.2",
"font_path": "/System/Library/Fonts/Supplemental/Arial Unicode.ttf",
"png_bytes": 30539
}

View File

@@ -0,0 +1,186 @@
{
"content_keys": [
"conclusion",
"left_points",
"left_title",
"right_points",
"right_title",
"title"
],
"elements": [
{
"bbox": {
"height": 540.0,
"width": 960.0,
"x": 0.0,
"y": 0.0
},
"element_id": "background",
"kind": "rect",
"role": "background",
"source_ref": null,
"text": null
},
{
"bbox": {
"height": 64.0,
"width": 760.0,
"x": 64.0,
"y": 52.0
},
"element_id": "title",
"kind": "text",
"role": "title",
"source_ref": "canvas_spec.content.title",
"text": "观测证据对比"
},
{
"bbox": {
"height": 250.0,
"width": 390.0,
"x": 64.0,
"y": 140.0
},
"element_id": "left-card",
"kind": "rect",
"role": "container",
"source_ref": null,
"text": null
},
{
"bbox": {
"height": 250.0,
"width": 390.0,
"x": 506.0,
"y": 140.0
},
"element_id": "right-card",
"kind": "rect",
"role": "container",
"source_ref": null,
"text": null
},
{
"bbox": {
"height": 34.0,
"width": 320.0,
"x": 92.0,
"y": 168.0
},
"element_id": "left-title",
"kind": "text",
"role": "left-title",
"source_ref": "canvas_spec.content.left_title",
"text": "地表信号"
},
{
"bbox": {
"height": 36.0,
"width": 296.0,
"x": 116.0,
"y": 222.0
},
"element_id": "left-point-1",
"kind": "text",
"role": "body",
"source_ref": "canvas_spec.content.left_points[]",
"text": "熔岩地貌"
},
{
"bbox": {
"height": 36.0,
"width": 296.0,
"x": 116.0,
"y": 270.0
},
"element_id": "left-point-2",
"kind": "text",
"role": "body",
"source_ref": "canvas_spec.content.left_points[]",
"text": "热异常"
},
{
"bbox": {
"height": 36.0,
"width": 296.0,
"x": 116.0,
"y": 318.0
},
"element_id": "left-point-3",
"kind": "text",
"role": "body",
"source_ref": "canvas_spec.content.left_points[]",
"text": "气体羽流"
},
{
"bbox": {
"height": 34.0,
"width": 320.0,
"x": 534.0,
"y": 168.0
},
"element_id": "right-title",
"kind": "text",
"role": "right-title",
"source_ref": "canvas_spec.content.right_title",
"text": "地下信号"
},
{
"bbox": {
"height": 36.0,
"width": 296.0,
"x": 558.0,
"y": 222.0
},
"element_id": "right-point-1",
"kind": "text",
"role": "body",
"source_ref": "canvas_spec.content.right_points[]",
"text": "地震活动"
},
{
"bbox": {
"height": 36.0,
"width": 296.0,
"x": 558.0,
"y": 270.0
},
"element_id": "right-point-2",
"kind": "text",
"role": "body",
"source_ref": "canvas_spec.content.right_points[]",
"text": "形变趋势"
},
{
"bbox": {
"height": 36.0,
"width": 296.0,
"x": 558.0,
"y": 318.0
},
"element_id": "right-point-3",
"kind": "text",
"role": "body",
"source_ref": "canvas_spec.content.right_points[]",
"text": "岩浆补给"
},
{
"bbox": {
"height": 42.0,
"width": 788.0,
"x": 86.0,
"y": 426.0
},
"element_id": "conclusion",
"kind": "text",
"role": "conclusion",
"source_ref": "canvas_spec.content.conclusion",
"text": "多源观测共同降低单一证据偏差。"
}
],
"page": 2,
"semantic_source": "CanvasSpec",
"template_id": "comparison-cards",
"theme_id": "paper-research",
"version": "svglide-semantic-map/v1"
}

View File

@@ -0,0 +1,72 @@
{
"canvas": {
"height": 540,
"viewBox": "0 0 960 540",
"width": 960
},
"content": {
"eyebrow": "NEXT",
"subtitle": "把研究结果转成可执行的观测闭环。",
"takeaways": [
"连续监测",
"现场复核",
"风险沟通"
],
"title": "下一步观测"
},
"quality_constraints": {
"max_title_lines": 2,
"min_font_size": 18,
"safe_area": {
"height": 460,
"width": 864,
"x": 48,
"y": 40
}
},
"safe_area": {
"height": 460,
"width": 864,
"x": 48,
"y": 40
},
"semantic_elements": [
{
"bbox": {
"height": 110,
"width": 700,
"x": 72,
"y": 110
},
"element_id": "title",
"kind": "text",
"role": "title",
"source_ref": "canvas_spec.content.title"
}
],
"template_id": "summary-final",
"theme": {
"colors": {
"accent": "#22D3EE",
"background": "#27130F",
"muted": "#F4C9A8",
"panel": "#3A211B",
"primary": "#F97316",
"text": "#FFF7ED"
},
"spacing": {
"card": 28,
"gap": 20,
"page": 64
},
"theme_id": "warm-editorial",
"typography": {
"body": 20,
"label": 18,
"subtitle": 23,
"title": 54
}
},
"theme_id": "warm-editorial",
"version": "svglide-canvas-spec/v1"
}

View File

@@ -0,0 +1,19 @@
<svg xmlns="http://www.w3.org/2000/svg" width="960" height="540" viewBox="0 0 960 540">
<rect data-node-id="background" x="0" y="0" width="960" height="540" fill="#27130F"/>
<circle data-node-id="accent" cx="786" cy="136" r="82" fill="#22D3EE" opacity="0.22"/>
<rect data-node-id="metric-bar-1" x="712" y="322" width="18" height="30" fill="#F97316" opacity="0.72"/>
<rect data-node-id="metric-bar-2" x="742" y="304" width="18" height="48" fill="#F97316" opacity="0.86"/>
<rect data-node-id="metric-bar-3" x="772" y="286" width="18" height="66" fill="#22D3EE" opacity="0.92"/>
<text data-node-id="eyebrow" data-box-x="72" data-box-y="64" data-box-width="420" data-box-height="32" x="72" y="84" fill="#F97316" font-size="18" font-weight="800" font-family="Inter">NEXT</text>
<text data-node-id="title" data-box-x="72" data-box-y="110" data-box-width="700" data-box-height="110" x="72" y="164" fill="#FFF7ED" font-size="50" font-weight="850" font-family="Inter">下一步观测</text>
<text data-node-id="subtitle" data-box-x="72" data-box-y="244" data-box-width="640" data-box-height="66" x="72" y="274" fill="#F4C9A8" font-size="23" font-weight="500" font-family="Inter">把研究结果转成可执行的观测闭环。</text>
<rect data-node-id="takeaway-card-1" x="72" y="344" width="250" height="126" fill="#3A211B"/>
<text data-node-id="takeaway-index-1" data-box-x="94" data-box-y="366" data-box-width="64" data-box-height="30" x="94" y="386" fill="#F97316" font-size="18" font-weight="800" font-family="Inter">01</text>
<text data-node-id="takeaway-1" data-box-x="94" data-box-y="404" data-box-width="202" data-box-height="54" x="94" y="428" fill="#FFF7ED" font-size="21" font-weight="700" font-family="Inter">连续监测</text>
<rect data-node-id="takeaway-card-2" x="340" y="344" width="250" height="126" fill="#3A211B"/>
<text data-node-id="takeaway-index-2" data-box-x="362" data-box-y="366" data-box-width="64" data-box-height="30" x="362" y="386" fill="#F97316" font-size="18" font-weight="800" font-family="Inter">02</text>
<text data-node-id="takeaway-2" data-box-x="362" data-box-y="404" data-box-width="202" data-box-height="54" x="362" y="428" fill="#FFF7ED" font-size="21" font-weight="700" font-family="Inter">现场复核</text>
<rect data-node-id="takeaway-card-3" x="608" y="344" width="250" height="126" fill="#3A211B"/>
<text data-node-id="takeaway-index-3" data-box-x="630" data-box-y="366" data-box-width="64" data-box-height="30" x="630" y="386" fill="#F97316" font-size="18" font-weight="800" font-family="Inter">03</text>
<text data-node-id="takeaway-3" data-box-x="630" data-box-y="404" data-box-width="202" data-box-height="54" x="630" y="428" fill="#FFF7ED" font-size="21" font-weight="700" font-family="Inter">风险沟通</text>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@@ -0,0 +1,148 @@
{
"drift": {
"max_px": 0,
"status": "not_measured_in_p0"
},
"nodes": [
{
"height": 540,
"id": "background",
"kind": "rect",
"width": 960,
"x": 0,
"y": 0
},
{
"height": 30,
"id": "metric-bar-1",
"kind": "rect",
"width": 18,
"x": 712,
"y": 322
},
{
"height": 48,
"id": "metric-bar-2",
"kind": "rect",
"width": 18,
"x": 742,
"y": 304
},
{
"height": 66,
"id": "metric-bar-3",
"kind": "rect",
"width": 18,
"x": 772,
"y": 286
},
{
"height": 32,
"id": "eyebrow",
"kind": "text",
"text": "NEXT",
"width": 420,
"x": 72,
"y": 64
},
{
"height": 110,
"id": "title",
"kind": "text",
"text": "下一步观测",
"width": 700,
"x": 72,
"y": 110
},
{
"height": 66,
"id": "subtitle",
"kind": "text",
"text": "把研究结果转成可执行的观测闭环。",
"width": 640,
"x": 72,
"y": 244
},
{
"height": 126,
"id": "takeaway-card-1",
"kind": "rect",
"width": 250,
"x": 72,
"y": 344
},
{
"height": 30,
"id": "takeaway-index-1",
"kind": "text",
"text": "01",
"width": 64,
"x": 94,
"y": 366
},
{
"height": 54,
"id": "takeaway-1",
"kind": "text",
"text": "连续监测",
"width": 202,
"x": 94,
"y": 404
},
{
"height": 126,
"id": "takeaway-card-2",
"kind": "rect",
"width": 250,
"x": 340,
"y": 344
},
{
"height": 30,
"id": "takeaway-index-2",
"kind": "text",
"text": "02",
"width": 64,
"x": 362,
"y": 366
},
{
"height": 54,
"id": "takeaway-2",
"kind": "text",
"text": "现场复核",
"width": 202,
"x": 362,
"y": 404
},
{
"height": 126,
"id": "takeaway-card-3",
"kind": "rect",
"width": 250,
"x": 608,
"y": 344
},
{
"height": 30,
"id": "takeaway-index-3",
"kind": "text",
"text": "03",
"width": 64,
"x": 630,
"y": 366
},
{
"height": 54,
"id": "takeaway-3",
"kind": "text",
"text": "风险沟通",
"width": 202,
"x": 630,
"y": 404
}
],
"page": 3,
"source": "template-layout-map",
"version": "svglide-node-layout-map/v1"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@@ -0,0 +1,108 @@
{
"canvas_spec_path": "02-plan/slide_plan.json#/slides/2/canvas_spec",
"canvas_spec_sha256": "1409994cbf70a22f5fffff357295569ef0adc9caae55ebf6bb52920b2c385615",
"canvas_template_svg": "04-svg/artboard/page-003.canvas-template.svg",
"canvas_template_svg_sha256": "81c00e0a1494c858ea821a001833825b93bf44fe328905a4fe7f822b69b6ac2e",
"compiler": {
"compiler_input": "CanvasSpecTemplateSVG",
"fail_fast": [
"clipPath",
"defs",
"filter",
"foreignObject",
"image",
"linearGradient",
"pattern",
"radialGradient",
"use"
],
"native_mapped": [
"rect",
"circle",
"rect",
"rect",
"rect",
"text->foreignObject",
"text->foreignObject",
"text->foreignObject",
"rect",
"text->foreignObject",
"text->foreignObject",
"rect",
"text->foreignObject",
"text->foreignObject",
"rect",
"text->foreignObject",
"text->foreignObject"
],
"satori_svg_usage": "preview_only",
"semantic_source": "CanvasSpec"
},
"compiler_input": "04-svg/artboard/page-003.canvas-template.svg",
"compiler_input_sha256": "81c00e0a1494c858ea821a001833825b93bf44fe328905a4fe7f822b69b6ac2e",
"created_at": "2026-06-21T13:59:15+08:00",
"font_hashes": [
{
"path": "/System/Library/Fonts/Supplemental/Arial Unicode.ttf",
"sha256": "876af2cd4854644e7f3e7feb2f688997fdb3343c6df6693611209c9dfb47ccec"
}
],
"node_layout_map": "04-svg/artboard/page-003.node-layout-map.json",
"node_layout_map_sha256": "5b0c426195a980724c7e77f0d09cc5a8f32bafb2f85ce7d5ac3a6b641cc72886",
"node_version": "v20.18.1",
"page": 3,
"png": "04-svg/artboard/page-003.png",
"png_sha256": "a1d172b3953148d4a428d7359f48f9f3b97d74a89ef07257583861271653bc6a",
"render_metadata": "04-svg/artboard/page-003.render-metadata.json",
"render_metadata_sha256": "2c3d41831d1d62dd42c2a404501a71d0c361c9b827c583bb8acfaf586fe14f0b",
"renderer": {
"actual_satori_package": true,
"adapter": "skills/lark-slides/scripts/artboard_renderer/dist/render.mjs",
"engine": "satori-node",
"name": "satori-resvg-p0"
},
"resvg_version": "2.6.2",
"satori_preview": {
"element_counts": {
"g": 4,
"mask": 19,
"path": 1,
"rect": 26,
"svg": 1,
"text": 37
},
"fail_fast": [
"clipPath",
"defs",
"filter",
"foreignObject",
"image",
"linearGradient",
"pattern",
"radialGradient",
"use"
],
"issues": [],
"status": "passed",
"strict": false
},
"satori_svg": "04-svg/artboard/raw/page-003.satori.svg",
"satori_svg_sha256": "07396b9b453965f332cee318eb9b8e55dbd01fd844aa69f66e5e532aec1f7ab6",
"satori_version": "0.26.0",
"semantic_map": "04-svg/artboard/page-003.semantic-map.json",
"semantic_map_sha256": "8bacd61d84cae3f0f1ca36bcf10bc65d61b847b74a02eb224c90f3039b1195ec",
"stage": "generate_svg",
"status": "passed",
"svglide_svg": "04-svg/page-003.svg",
"svglide_svg_sha256": "9318eccee9a629b10c181b3416fe30f9de32883e36b93169121c30037dc50fd6",
"template_id": "summary-final",
"template_registry": "skills/lark-slides/references/svglide-template-registry.json",
"template_registry_sha256": "78c6639ee435ff8b8818f64f7b536967a7486281eacab5abce8e20626e20f6b3",
"theme_files": [
"skills/lark-slides/scripts/artboard_renderer/themes/warm-editorial.json"
],
"theme_id": "warm-editorial",
"theme_registry": "skills/lark-slides/scripts/artboard_renderer/themes/registry.json",
"theme_registry_sha256": "283def187d5c69623fd43618332b3f4f212b0dad313f2e90f13f614569d797fb",
"version": "svglide-artboard-receipt/v1"
}

View File

@@ -0,0 +1,7 @@
{
"node_version": "v20.18.1",
"satori_version": "0.26.0",
"resvg_version": "2.6.2",
"font_path": "/System/Library/Fonts/Supplemental/Arial Unicode.ttf",
"png_bytes": 25871
}

View File

@@ -0,0 +1,223 @@
{
"content_keys": [
"eyebrow",
"subtitle",
"takeaways",
"title"
],
"elements": [
{
"bbox": {
"height": 540.0,
"width": 960.0,
"x": 0.0,
"y": 0.0
},
"element_id": "background",
"kind": "rect",
"role": "background",
"source_ref": null,
"text": null
},
{
"bbox": {
"height": 30.0,
"width": 18.0,
"x": 712.0,
"y": 322.0
},
"element_id": "metric-bar-1",
"kind": "rect",
"role": "decorative",
"source_ref": null,
"text": null
},
{
"bbox": {
"height": 48.0,
"width": 18.0,
"x": 742.0,
"y": 304.0
},
"element_id": "metric-bar-2",
"kind": "rect",
"role": "decorative",
"source_ref": null,
"text": null
},
{
"bbox": {
"height": 66.0,
"width": 18.0,
"x": 772.0,
"y": 286.0
},
"element_id": "metric-bar-3",
"kind": "rect",
"role": "decorative",
"source_ref": null,
"text": null
},
{
"bbox": {
"height": 32.0,
"width": 420.0,
"x": 72.0,
"y": 64.0
},
"element_id": "eyebrow",
"kind": "text",
"role": "eyebrow",
"source_ref": "canvas_spec.content.eyebrow",
"text": "NEXT"
},
{
"bbox": {
"height": 110.0,
"width": 700.0,
"x": 72.0,
"y": 110.0
},
"element_id": "title",
"kind": "text",
"role": "title",
"source_ref": "canvas_spec.content.title",
"text": "下一步观测"
},
{
"bbox": {
"height": 66.0,
"width": 640.0,
"x": 72.0,
"y": 244.0
},
"element_id": "subtitle",
"kind": "text",
"role": "subtitle",
"source_ref": "canvas_spec.content.subtitle",
"text": "把研究结果转成可执行的观测闭环。"
},
{
"bbox": {
"height": 126.0,
"width": 250.0,
"x": 72.0,
"y": 344.0
},
"element_id": "takeaway-card-1",
"kind": "rect",
"role": "container",
"source_ref": "canvas_spec.content.takeaways[]",
"text": null
},
{
"bbox": {
"height": 30.0,
"width": 64.0,
"x": 94.0,
"y": 366.0
},
"element_id": "takeaway-index-1",
"kind": "text",
"role": "takeaway-index-1",
"source_ref": "canvas_spec.content.takeaways[]",
"text": "01"
},
{
"bbox": {
"height": 54.0,
"width": 202.0,
"x": 94.0,
"y": 404.0
},
"element_id": "takeaway-1",
"kind": "text",
"role": "takeaway-1",
"source_ref": "canvas_spec.content.takeaways[]",
"text": "连续监测"
},
{
"bbox": {
"height": 126.0,
"width": 250.0,
"x": 340.0,
"y": 344.0
},
"element_id": "takeaway-card-2",
"kind": "rect",
"role": "container",
"source_ref": "canvas_spec.content.takeaways[]",
"text": null
},
{
"bbox": {
"height": 30.0,
"width": 64.0,
"x": 362.0,
"y": 366.0
},
"element_id": "takeaway-index-2",
"kind": "text",
"role": "takeaway-index-2",
"source_ref": "canvas_spec.content.takeaways[]",
"text": "02"
},
{
"bbox": {
"height": 54.0,
"width": 202.0,
"x": 362.0,
"y": 404.0
},
"element_id": "takeaway-2",
"kind": "text",
"role": "takeaway-2",
"source_ref": "canvas_spec.content.takeaways[]",
"text": "现场复核"
},
{
"bbox": {
"height": 126.0,
"width": 250.0,
"x": 608.0,
"y": 344.0
},
"element_id": "takeaway-card-3",
"kind": "rect",
"role": "container",
"source_ref": "canvas_spec.content.takeaways[]",
"text": null
},
{
"bbox": {
"height": 30.0,
"width": 64.0,
"x": 630.0,
"y": 366.0
},
"element_id": "takeaway-index-3",
"kind": "text",
"role": "takeaway-index-3",
"source_ref": "canvas_spec.content.takeaways[]",
"text": "03"
},
{
"bbox": {
"height": 54.0,
"width": 202.0,
"x": 630.0,
"y": 404.0
},
"element_id": "takeaway-3",
"kind": "text",
"role": "takeaway-3",
"source_ref": "canvas_spec.content.takeaways[]",
"text": "风险沟通"
}
],
"page": 3,
"semantic_source": "CanvasSpec",
"template_id": "summary-final",
"theme_id": "warm-editorial",
"version": "svglide-semantic-map/v1"
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.7 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 12 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.3 KiB

View File

@@ -0,0 +1,14 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:slide="https://slides.bytedance.com/ns" slide:role="slide" slide:contract-version="svglide-authoring-contract/v1" width="960" height="540" viewBox="0 0 960 540">
<rect x="0" y="0" width="960" height="540" fill="#071827" slide:role="shape" />
<circle cx="820" cy="132" r="96" fill="#F97316" opacity="0.28" slide:role="shape" />
<circle cx="812" cy="150" r="72" fill="#5EEAD4" opacity="0.22" slide:role="shape" />
<rect x="56" y="64" width="704" height="356" fill="#0E2A3F" opacity="0.82" slide:role="shape" />
<foreignObject slide:role="shape" slide:shape-type="text" x="84" y="98" width="420" height="32" data-node-id="eyebrow"><div xmlns="http://www.w3.org/1999/xhtml" style="font-size:18px;font-weight:700;font-family:Inter;color:#5EEAD4;line-height:1.16;white-space:normal">ICELAND VOLCANO</div></foreignObject>
<foreignObject slide:role="shape" slide:shape-type="text" x="84" y="142" width="628" height="142" data-node-id="title"><div xmlns="http://www.w3.org/1999/xhtml" style="font-size:58px;font-weight:800;font-family:Inter;color:#EAF6FF;line-height:1.16;white-space:normal">冰岛火山研究</div></foreignObject>
<foreignObject slide:role="shape" slide:shape-type="text" x="88" y="302" width="610" height="74" data-node-id="subtitle"><div xmlns="http://www.w3.org/1999/xhtml" style="font-size:24px;font-weight:500;font-family:Inter;color:#93B4C8;line-height:1.16;white-space:normal">地貌证据、活动迹象与连续观测。</div></foreignObject>
<rect x="84" y="444" width="92" height="40" fill="#5EEAD4" opacity="0.16" slide:role="shape" />
<rect x="190" y="444" width="92" height="40" fill="#5EEAD4" opacity="0.16" slide:role="shape" />
<foreignObject slide:role="shape" slide:shape-type="text" x="190" y="444" width="92" height="40" data-node-id="chip-2"><div xmlns="http://www.w3.org/1999/xhtml" style="font-size:17px;font-weight:600;font-family:Inter;color:#EAF6FF;line-height:1.16;white-space:normal">监测</div></foreignObject>
<rect x="296" y="444" width="92" height="40" fill="#5EEAD4" opacity="0.16" slide:role="shape" />
<foreignObject slide:role="shape" slide:shape-type="text" x="296" y="444" width="92" height="40" data-node-id="chip-3"><div xmlns="http://www.w3.org/1999/xhtml" style="font-size:17px;font-weight:600;font-family:Inter;color:#EAF6FF;line-height:1.16;white-space:normal">风险</div></foreignObject>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@@ -0,0 +1,22 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:slide="https://slides.bytedance.com/ns" slide:role="slide" slide:contract-version="svglide-authoring-contract/v1" width="960" height="540" viewBox="0 0 960 540">
<rect x="0" y="0" width="960" height="540" fill="#F7F3E8" slide:role="shape" />
<foreignObject slide:role="shape" slide:shape-type="text" x="64" y="52" width="760" height="64" data-node-id="title"><div xmlns="http://www.w3.org/1999/xhtml" style="font-size:40px;font-weight:800;font-family:Inter;color:#1F2937;line-height:1.16;white-space:normal">观测证据对比</div></foreignObject>
<rect x="64" y="140" width="390" height="250" fill="#FFFDF6" opacity="0.82" slide:role="shape" />
<rect x="506" y="140" width="390" height="250" fill="#FFFDF6" opacity="0.82" slide:role="shape" />
<foreignObject slide:role="shape" slide:shape-type="text" x="92" y="168" width="320" height="34" data-node-id="left-title"><div xmlns="http://www.w3.org/1999/xhtml" style="font-size:24px;font-weight:800;font-family:Inter;color:#1E3A8A;line-height:1.16;white-space:normal">地表信号</div></foreignObject>
<foreignObject slide:role="shape" slide:shape-type="text" x="534" y="168" width="320" height="34" data-node-id="right-title"><div xmlns="http://www.w3.org/1999/xhtml" style="font-size:24px;font-weight:800;font-family:Inter;color:#B45309;line-height:1.16;white-space:normal">地下信号</div></foreignObject>
<circle cx="98" cy="241" r="5" fill="#1E3A8A" slide:role="shape" />
<foreignObject slide:role="shape" slide:shape-type="text" x="116" y="222" width="296" height="36" data-node-id="left-point-1"><div xmlns="http://www.w3.org/1999/xhtml" style="font-size:20px;font-weight:500;font-family:Inter;color:#6B7280;line-height:1.16;white-space:normal">熔岩地貌</div></foreignObject>
<circle cx="98" cy="289" r="5" fill="#1E3A8A" slide:role="shape" />
<foreignObject slide:role="shape" slide:shape-type="text" x="116" y="270" width="296" height="36" data-node-id="left-point-2"><div xmlns="http://www.w3.org/1999/xhtml" style="font-size:20px;font-weight:500;font-family:Inter;color:#6B7280;line-height:1.16;white-space:normal">热异常</div></foreignObject>
<circle cx="98" cy="337" r="5" fill="#1E3A8A" slide:role="shape" />
<foreignObject slide:role="shape" slide:shape-type="text" x="116" y="318" width="296" height="36" data-node-id="left-point-3"><div xmlns="http://www.w3.org/1999/xhtml" style="font-size:20px;font-weight:500;font-family:Inter;color:#6B7280;line-height:1.16;white-space:normal">气体羽流</div></foreignObject>
<circle cx="540" cy="241" r="5" fill="#B45309" slide:role="shape" />
<foreignObject slide:role="shape" slide:shape-type="text" x="558" y="222" width="296" height="36" data-node-id="right-point-1"><div xmlns="http://www.w3.org/1999/xhtml" style="font-size:20px;font-weight:500;font-family:Inter;color:#6B7280;line-height:1.16;white-space:normal">地震活动</div></foreignObject>
<circle cx="540" cy="289" r="5" fill="#B45309" slide:role="shape" />
<foreignObject slide:role="shape" slide:shape-type="text" x="558" y="270" width="296" height="36" data-node-id="right-point-2"><div xmlns="http://www.w3.org/1999/xhtml" style="font-size:20px;font-weight:500;font-family:Inter;color:#6B7280;line-height:1.16;white-space:normal">形变趋势</div></foreignObject>
<circle cx="540" cy="337" r="5" fill="#B45309" slide:role="shape" />
<foreignObject slide:role="shape" slide:shape-type="text" x="558" y="318" width="296" height="36" data-node-id="right-point-3"><div xmlns="http://www.w3.org/1999/xhtml" style="font-size:20px;font-weight:500;font-family:Inter;color:#6B7280;line-height:1.16;white-space:normal">岩浆补给</div></foreignObject>
<rect x="64" y="414" width="832" height="66" fill="#1E3A8A" opacity="0.16" slide:role="shape" />
<foreignObject slide:role="shape" slide:shape-type="text" x="86" y="426" width="788" height="42" data-node-id="conclusion"><div xmlns="http://www.w3.org/1999/xhtml" style="font-size:22px;font-weight:700;font-family:Inter;color:#1F2937;line-height:1.16;white-space:normal">多源观测共同降低单一证据偏差。</div></foreignObject>
</svg>

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

@@ -0,0 +1,19 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:slide="https://slides.bytedance.com/ns" slide:role="slide" slide:contract-version="svglide-authoring-contract/v1" width="960" height="540" viewBox="0 0 960 540">
<rect x="0" y="0" width="960" height="540" fill="#27130F" slide:role="shape" />
<circle cx="786" cy="136" r="82" fill="#22D3EE" opacity="0.22" slide:role="shape" />
<rect x="712" y="322" width="18" height="30" fill="#F97316" opacity="0.72" slide:role="shape" />
<rect x="742" y="304" width="18" height="48" fill="#F97316" opacity="0.86" slide:role="shape" />
<rect x="772" y="286" width="18" height="66" fill="#22D3EE" opacity="0.92" slide:role="shape" />
<foreignObject slide:role="shape" slide:shape-type="text" x="72" y="64" width="420" height="32" data-node-id="eyebrow"><div xmlns="http://www.w3.org/1999/xhtml" style="font-size:18px;font-weight:800;font-family:Inter;color:#F97316;line-height:1.16;white-space:normal">NEXT</div></foreignObject>
<foreignObject slide:role="shape" slide:shape-type="text" x="72" y="110" width="700" height="110" data-node-id="title"><div xmlns="http://www.w3.org/1999/xhtml" style="font-size:50px;font-weight:850;font-family:Inter;color:#FFF7ED;line-height:1.16;white-space:normal">下一步观测</div></foreignObject>
<foreignObject slide:role="shape" slide:shape-type="text" x="72" y="244" width="640" height="66" data-node-id="subtitle"><div xmlns="http://www.w3.org/1999/xhtml" style="font-size:23px;font-weight:500;font-family:Inter;color:#F4C9A8;line-height:1.16;white-space:normal">把研究结果转成可执行的观测闭环。</div></foreignObject>
<rect x="72" y="344" width="250" height="126" fill="#3A211B" slide:role="shape" />
<foreignObject slide:role="shape" slide:shape-type="text" x="94" y="366" width="64" height="30" data-node-id="takeaway-index-1"><div xmlns="http://www.w3.org/1999/xhtml" style="font-size:18px;font-weight:800;font-family:Inter;color:#F97316;line-height:1.16;white-space:normal">01</div></foreignObject>
<foreignObject slide:role="shape" slide:shape-type="text" x="94" y="404" width="202" height="54" data-node-id="takeaway-1"><div xmlns="http://www.w3.org/1999/xhtml" style="font-size:21px;font-weight:700;font-family:Inter;color:#FFF7ED;line-height:1.16;white-space:normal">连续监测</div></foreignObject>
<rect x="340" y="344" width="250" height="126" fill="#3A211B" slide:role="shape" />
<foreignObject slide:role="shape" slide:shape-type="text" x="362" y="366" width="64" height="30" data-node-id="takeaway-index-2"><div xmlns="http://www.w3.org/1999/xhtml" style="font-size:18px;font-weight:800;font-family:Inter;color:#F97316;line-height:1.16;white-space:normal">02</div></foreignObject>
<foreignObject slide:role="shape" slide:shape-type="text" x="362" y="404" width="202" height="54" data-node-id="takeaway-2"><div xmlns="http://www.w3.org/1999/xhtml" style="font-size:21px;font-weight:700;font-family:Inter;color:#FFF7ED;line-height:1.16;white-space:normal">现场复核</div></foreignObject>
<rect x="608" y="344" width="250" height="126" fill="#3A211B" slide:role="shape" />
<foreignObject slide:role="shape" slide:shape-type="text" x="630" y="366" width="64" height="30" data-node-id="takeaway-index-3"><div xmlns="http://www.w3.org/1999/xhtml" style="font-size:18px;font-weight:800;font-family:Inter;color:#F97316;line-height:1.16;white-space:normal">03</div></foreignObject>
<foreignObject slide:role="shape" slide:shape-type="text" x="630" y="404" width="202" height="54" data-node-id="takeaway-3"><div xmlns="http://www.w3.org/1999/xhtml" style="font-size:21px;font-weight:700;font-family:Inter;color:#FFF7ED;line-height:1.16;white-space:normal">风险沟通</div></foreignObject>
</svg>

After

Width:  |  Height:  |  Size: 3.6 KiB

Some files were not shown because too many files have changed in this diff Show More