diff --git a/shortcuts/slides/slides_create_svg_test.go b/shortcuts/slides/slides_create_svg_test.go index 70c0310e2..8e20d11b0 100644 --- a/shortcuts/slides/slides_create_svg_test.go +++ b/shortcuts/slides/slides_create_svg_test.go @@ -148,7 +148,7 @@ func TestSlidesCreateSVGExecuteCreatesSlidesInFileOrder(t *testing.T) { func TestSlidesCreateSVGChartMarkerPassesThroughSlideContent(t *testing.T) { dir := t.TempDir() withSlidesTestWorkingDir(t, dir) - svg := `` + testSVGlideChartMarker(testSVGlideChartMetadata(``)) + `` + svg := `` + testSVGlideChartMarker(testSVGlideChartMetadata(testSVGlideChartSpecJSON())) + `` if err := os.WriteFile("chart.svg", []byte(svg), 0o644); err != nil { t.Fatalf("write chart.svg: %v", err) } @@ -196,8 +196,8 @@ func TestSlidesCreateSVGChartMarkerPassesThroughSlideContent(t *testing.T) { `slide:contract-version="svglide-authoring-contract/v1"`, ``, `data-svglide-chart="svglide-chart-inline/v1"`, - `data-format="sxsd-chart-v1"`, - `data-encoding="base64url"`, + `data-format="svglide-chart-spec-v1"`, + `data-encoding="base64url-json"`, `data-payload-hash="sha256:`, } { if !strings.Contains(content, want) { diff --git a/shortcuts/slides/svg_helpers.go b/shortcuts/slides/svg_helpers.go index aab8838a7..cd4cac170 100644 --- a/shortcuts/slides/svg_helpers.go +++ b/shortcuts/slides/svg_helpers.go @@ -9,9 +9,8 @@ import ( "encoding/base64" "encoding/hex" "encoding/json" - "encoding/xml" "fmt" - "io" + "math" "path/filepath" "regexp" "strings" @@ -26,10 +25,27 @@ const ( svglideSlideNS = "https://slides.bytedance.com/ns" svglideContractVersion = "svglide-authoring-contract/v1" svglideChartMarkerVersion = "svglide-chart-inline/v1" - svglideChartFormat = "sxsd-chart-v1" - svglideChartEncoding = "base64url" + svglideChartFormat = "svglide-chart-spec-v1" + svglideChartSpecVersion = "svglide-chart-spec/v1" + svglideChartEncoding = "base64url-json" ) +type svglideChartSpecPayload struct { + Version *string `json:"version"` + ChartType *string `json:"chartType"` + Data *svglideChartSpecData `json:"data"` +} + +type svglideChartSpecData struct { + Categories []string `json:"categories"` + Series []svglideChartSpecSeries `json:"series"` +} + +type svglideChartSpecSeries struct { + Name *string `json:"name"` + Values []json.RawMessage `json:"values"` +} + type RewrittenSVGPage struct { Content string Tokens []string @@ -532,8 +548,8 @@ func validateSVGlideChartMarker(path, attrs, inner string) (string, error) { if !strings.EqualFold(hash, "sha256:"+hex.EncodeToString(sum[:])) { return "", output.ErrValidation("--file %s: chart marker metadata data-payload-hash does not match decoded payload", path) } - if err := validateSVGlideChartPayloadXML(decoded); err != nil { - return "", output.ErrValidation("--file %s: chart marker metadata decoded payload must be a single root: %v", path, err) + if err := validateSVGlideChartSpecPayload(decoded); err != nil { + return "", output.ErrValidation("--file %s: chart marker metadata decoded payload must be valid %s JSON: %v", path, svglideChartFormat, err) } return chartRef, nil } @@ -610,44 +626,76 @@ func decodeSVGlideChartPayload(payload string) ([]byte, error) { return base64.RawURLEncoding.DecodeString(payload) } -func validateSVGlideChartPayloadXML(payload []byte) error { - decoder := xml.NewDecoder(bytes.NewReader(payload)) - rootSeen := false - depth := 0 - for { - token, err := decoder.Token() - if err == io.EOF { - break +func validateSVGlideChartSpecPayload(payload []byte) error { + var spec svglideChartSpecPayload + if err := json.Unmarshal(payload, &spec); err != nil { + return err + } + if spec.Version == nil || strings.TrimSpace(*spec.Version) == "" { + return fmt.Errorf("missing version") + } + if strings.TrimSpace(*spec.Version) != svglideChartSpecVersion { + return fmt.Errorf("version must be %q", svglideChartSpecVersion) + } + if spec.ChartType == nil || strings.TrimSpace(*spec.ChartType) == "" { + return fmt.Errorf("missing chartType") + } + switch strings.TrimSpace(*spec.ChartType) { + case "bar", "line": + default: + return fmt.Errorf("chartType must be one of bar,line") + } + if spec.Data == nil { + return fmt.Errorf("missing data") + } + if len(spec.Data.Categories) == 0 { + return fmt.Errorf("data.categories must be a non-empty array") + } + for i, category := range spec.Data.Categories { + if strings.TrimSpace(category) == "" { + return fmt.Errorf("data.categories[%d] must be a non-empty string", i) } - if err != nil { - return err + } + if len(spec.Data.Series) == 0 { + return fmt.Errorf("data.series must be a non-empty array") + } + for i, series := range spec.Data.Series { + if series.Name == nil || strings.TrimSpace(*series.Name) == "" { + return fmt.Errorf("data.series[%d].name must be a non-empty string", i) } - switch t := token.(type) { - case xml.StartElement: - if !rootSeen { - if t.Name.Local != "chart" { - return fmt.Errorf("got <%s>", t.Name.Local) - } - rootSeen = true - } else if depth == 0 { - return fmt.Errorf("multiple root elements") - } - depth++ - case xml.EndElement: - if depth > 0 { - depth-- - } - case xml.CharData: - if depth == 0 && strings.TrimSpace(string(t)) != "" { - return fmt.Errorf("non-whitespace text outside root") + if len(series.Values) != len(spec.Data.Categories) { + return fmt.Errorf("data.series[%d].values length must match data.categories length", i) + } + for j, value := range series.Values { + if err := validateSVGlideChartNumber(value); err != nil { + return fmt.Errorf("data.series[%d].values[%d] must be a finite number: %v", i, j, err) } } } - if !rootSeen { - return fmt.Errorf("missing root") + return nil +} + +func validateSVGlideChartNumber(raw json.RawMessage) error { + decoder := json.NewDecoder(bytes.NewReader(raw)) + decoder.UseNumber() + var value any + if err := decoder.Decode(&value); err != nil { + return err } - if depth != 0 { - return fmt.Errorf("unclosed root") + var extra any + if err := decoder.Decode(&extra); err == nil { + return fmt.Errorf("multiple JSON values") + } + number, ok := value.(json.Number) + if !ok { + return fmt.Errorf("got %T", value) + } + n, err := number.Float64() + if err != nil { + return err + } + if math.IsNaN(n) || math.IsInf(n, 0) { + return fmt.Errorf("non-finite number") } return nil } diff --git a/shortcuts/slides/svg_helpers_test.go b/shortcuts/slides/svg_helpers_test.go index 86986fc5e..95c0f4aeb 100644 --- a/shortcuts/slides/svg_helpers_test.go +++ b/shortcuts/slides/svg_helpers_test.go @@ -177,7 +177,7 @@ func TestValidateSVGlideSVGRecursiveChildren(t *testing.T) { }, { name: "root chart marker with inline payload", - svg: `` + testSVGlideChartMarker(testSVGlideChartMetadata(``)) + ``, + svg: `` + testSVGlideChartMarker(testSVGlideChartMetadata(testSVGlideChartSpecJSON())) + ``, }, { name: "style and nested defs are ignored", @@ -297,43 +297,63 @@ func TestValidateSVGlideSVGRecursiveChildren(t *testing.T) { }, { name: "nested chart marker is rejected", - svg: `` + testSVGlideChartMarker(testSVGlideChartMetadata(``)) + ``, + svg: `` + testSVGlideChartMarker(testSVGlideChartMetadata(testSVGlideChartSpecJSON())) + ``, wantErr: ` must be a direct child of root `, }, { name: "chart marker requires ref", - svg: `` + testSVGlideChartMetadata(``) + ``, + svg: `` + testSVGlideChartMetadata(testSVGlideChartSpecJSON()) + ``, wantErr: `missing required attribute "slide:chart-ref"`, }, { name: "chart marker rejects bad bbox", - svg: `` + testSVGlideChartMetadata(``) + ``, + svg: `` + testSVGlideChartMetadata(testSVGlideChartSpecJSON()) + ``, wantErr: `attribute "x" must be a number or px length`, }, { name: "chart marker requires single metadata", - svg: `` + testSVGlideChartMarker(testSVGlideChartMetadata(``)+testSVGlideChartMetadata(``)) + ``, + svg: `` + testSVGlideChartMarker(testSVGlideChartMetadata(testSVGlideChartSpecJSON())+testSVGlideChartMetadata(testSVGlideChartSpecJSON())) + ``, wantErr: `must contain exactly one metadata child`, }, { name: "chart marker rejects duplicate chart refs", - svg: `` + testSVGlideChartMarker(testSVGlideChartMetadata(``)) + testSVGlideChartMarker(testSVGlideChartMetadata(``)) + ``, + svg: `` + testSVGlideChartMarker(testSVGlideChartMetadata(testSVGlideChartSpecJSON())) + testSVGlideChartMarker(testSVGlideChartMetadata(testSVGlideLineChartSpecJSON())) + ``, wantErr: `duplicate slide:chart-ref "chart-1"`, }, { name: "chart marker rejects bad base64url", - svg: `` + testSVGlideChartMarker(`bad+payload`) + ``, + svg: `` + testSVGlideChartMarker(`bad+payload`) + ``, wantErr: `payload must be base64url`, }, + { + name: "chart marker rejects old sxsd chart format", + svg: `` + testSVGlideChartMarker(``+base64.RawURLEncoding.EncodeToString([]byte(``))+``) + ``, + wantErr: `data-format="svglide-chart-spec-v1"`, + }, { name: "chart marker rejects hash mismatch", - svg: `` + testSVGlideChartMarker(testSVGlideChartMetadataWithHash(``, "sha256:"+strings.Repeat("0", 64))) + ``, + svg: `` + testSVGlideChartMarker(testSVGlideChartMetadataWithHash(testSVGlideChartSpecJSON(), "sha256:"+strings.Repeat("0", 64))) + ``, wantErr: `data-payload-hash does not match`, }, { - name: "chart marker decoded payload must be chart root", - svg: `` + testSVGlideChartMarker(testSVGlideChartMetadata(``)) + ``, - wantErr: `decoded payload must be a single root`, + name: "chart marker decoded payload must be json", + svg: `` + testSVGlideChartMarker(testSVGlideChartMetadata(``)) + ``, + wantErr: `decoded payload must be valid svglide-chart-spec-v1 JSON`, + }, + { + name: "chart marker rejects unsupported chart type", + svg: `` + testSVGlideChartMarker(testSVGlideChartMetadata(`{"version":"svglide-chart-spec/v1","chartType":"pie","data":{"categories":["Q1"],"series":[{"name":"Revenue","values":[12]}]}}`)) + ``, + wantErr: `chartType must be one of bar,line`, + }, + { + name: "chart marker rejects values length mismatch", + svg: `` + testSVGlideChartMarker(testSVGlideChartMetadata(`{"version":"svglide-chart-spec/v1","chartType":"bar","data":{"categories":["Q1","Q2"],"series":[{"name":"Revenue","values":[12]}]}}`)) + ``, + wantErr: `values length must match data.categories length`, + }, + { + name: "chart marker rejects nonnumeric values", + svg: `` + testSVGlideChartMarker(testSVGlideChartMetadata(`{"version":"svglide-chart-spec/v1","chartType":"line","data":{"categories":["Q1"],"series":[{"name":"Revenue","values":["12"]}]}}`)) + ``, + wantErr: `must be a finite number`, }, } @@ -361,15 +381,23 @@ func testSVGlideChartMarker(metadata string) string { return `` + metadata + `` } -func testSVGlideChartMetadata(chartXML string) string { - sum := sha256.Sum256([]byte(chartXML)) - return testSVGlideChartMetadataWithHash(chartXML, fmt.Sprintf("sha256:%x", sum)) +func testSVGlideChartSpecJSON() string { + return `{"version":"svglide-chart-spec/v1","chartType":"bar","data":{"categories":["Q1","Q2"],"series":[{"name":"Revenue","values":[12.5,18]}]}}` } -func testSVGlideChartMetadataWithHash(chartXML, hash string) string { - payload := base64.RawURLEncoding.EncodeToString([]byte(chartXML)) +func testSVGlideLineChartSpecJSON() string { + return `{"version":"svglide-chart-spec/v1","chartType":"line","data":{"categories":["Q1","Q2"],"series":[{"name":"Revenue","values":[12.5,18]}]}}` +} + +func testSVGlideChartMetadata(chartJSON string) string { + sum := sha256.Sum256([]byte(chartJSON)) + return testSVGlideChartMetadataWithHash(chartJSON, fmt.Sprintf("sha256:%x", sum)) +} + +func testSVGlideChartMetadataWithHash(chartJSON, hash string) string { + payload := base64.RawURLEncoding.EncodeToString([]byte(chartJSON)) return fmt.Sprintf( - `%s`, + `%s`, hash, payload, ) diff --git a/skills/lark-slides/SKILL.md b/skills/lark-slides/SKILL.md index ce391f29e..d4ec763cc 100644 --- a/skills/lark-slides/SKILL.md +++ b/skills/lark-slides/SKILL.md @@ -20,7 +20,7 @@ metadata: | 编辑单个标题、文本块、图片或局部元素 | 优先块级替换/插入,不改页序 | `slides +replace-slide`、`lark-slides-replace-slide.md` | | 读取或分析已有 PPT | 解析 slides/wiki token,回读全文或单页 XML,保存 `xml_presentation_id`、`slide_id`、`revision_id` | `xml_presentations.get`、`xml_presentation.slide.get` | | 上传或使用图片 | Preview 阶段优先多用真实图片增强视觉冲击;可先用公开可访问 http(s)/data 图片或本地 `@./path`,来源/授权只 warning 不阻断;正式交付再替换为授权清晰的 file token / 本地资产 | `slides +media-upload`,或 `+create --slides` / `+create-svg` 的 `@./path` 占位符 | -| 在 slide 中绘制柱/条/折线/面积/雷达/饼等有数据序列的图表 | XML 路径使用原生 ``;SVG 路径如需原生 chart snapshot,使用 `slide:role="chart"` marker | `xml-schema-quick-ref.md`、`svg-protocol.md` | +| 在 slide 中绘制柱/条/折线等 MVP 支持的数据图表 | XML 路径使用原生 ``;SVG 路径如需原生 chart,使用 `slide:role="chart"` spec marker | `xml-schema-quick-ref.md`、`svg-protocol.md` | | 在 slide 中绘制流程图、时序图、架构图、散点图、漏斗图或装饰图案 | XML 路径必须先用 Read 工具读取参考文档,再生成 `` 元素;`slides +create-svg` 明确不支持 whiteboard marker | [`lark-slides-whiteboard.md`](references/lark-slides-whiteboard.md)、`svg-protocol.md` | | 使用语义图标 | 先检索 IconPark,再写 `` | `iconpark_tool.py search → resolve`、`iconpark.md` | | 用户提到模板、主题、版式 | 先检索模板,再摘要,必要时裁切骨架 | `template_tool.py search → summarize → extract` | @@ -32,7 +32,7 @@ metadata: **CRITICAL — 走 `slides +create-svg` 时,输入必须是 SVGlide SVG:root `` 声明 `xmlns:slide` 且 `slide:role="slide"`;可渲染 SVG 元素必须用 `slide:role="shape"` 或 `slide:role="image"` 表达;`g` / 嵌套 `svg` 可作为容器,但容器内实际渲染元素仍必须各自声明 role。CLI 只读取文件、上传/替换图片占位符、注入 transport metadata 和调用现有 `/slide` 路由,不会把普通 SVG 自动补齐成协议 SVG。** -**CRITICAL — `slides +create-svg` 只允许 chart direct snapshot marker,不允许 whiteboard marker。chart marker 必须是 root `` 的直系 ``,且只包含一个 `data-svglide-chart="svglide-chart-inline/v1"` metadata;CLI 只校验 marker 外壳、base64url/hash 和 decoded 单根 ``,不解析 chart 业务合法性,也不会为 chart 调任何额外 API。`slide:role="whiteboard"` 和旧的 `data-svglide-whiteboard` marker 必须改走 XML/whiteboard 路径。** +**CRITICAL — `slides +create-svg` 只允许 chart spec marker,不允许 whiteboard marker。chart marker 必须是 root `` 的直系 ``,且只包含一个 `data-svglide-chart="svglide-chart-inline/v1"` metadata;metadata 必须使用 `data-format="svglide-chart-spec-v1"`、`data-encoding="base64url-json"`,payload 是 canonical JSON chart spec,不是 SXSD `` XML,也不是 chart snapshot/staticData。CLI 校验 marker 外壳、base64url/hash、JSON 基础结构、bar/line chartType、categories/series/values 长度和数值合法性,不会为 chart 调任何额外 API。`slide:role="whiteboard"` 和旧的 `data-svglide-whiteboard` marker 必须改走 XML/whiteboard 路径。** **CRITICAL — SVGlide deck 页数默认值:当用户要求生成 SVG/SVGlide 幻灯片但未说明页数,或使用“一份 slide / 一份 PPT / 做个 slide / 生成一个 slide”这类模糊表达时,默认生成 `10` 页,不要仅因页数缺失而停下来追问。只有用户明确说“一页 / 单页 / onepage / one slide / 只要封面”等单页意图时,才生成 `1` 页;用户给出明确页数时始终服从用户要求。默认 10 页时必须在 `slide_plan.json` 写入 `page_count` 或 `target_slide_count=10`,并包含明确 closing slide。** diff --git a/skills/lark-slides/references/lark-slides-create-svg.md b/skills/lark-slides/references/lark-slides-create-svg.md index 52aefe257..8a18e52f9 100644 --- a/skills/lark-slides/references/lark-slides-create-svg.md +++ b/skills/lark-slides/references/lark-slides-create-svg.md @@ -15,7 +15,7 @@ lark-cli slides +create-svg \ - AI 已经能生成符合 [svg-protocol.md](svg-protocol.md) 的 SVGlide SVG。 - 希望按文件逐页创建,避免把大段 XML/SVG 塞进 shell 参数。 - 需要 SVG 内本地图片占位符自动上传并替换为 file token。 -- 需要把已经生成好的原生 `` snapshot 作为 root direct chart marker 透传给服务端。 +- 需要把原生 chart 的 canonical JSON spec 作为 root chart spec marker 透传给服务端。 不适用: @@ -59,7 +59,7 @@ body: 不会新增 `/svg_slide` 路由,也不会把 `file_meta_map` 当成 CLI 到服务端的契约。 -chart direct snapshot 也不新增 API。CLI 不会解析 chart 业务合法性,不会上传 chart 资源,也不会调用任何 chart 创建接口;它只把通过轻校验的 marker 留在同一个 `slide.content` SVG 中。 +chart spec marker 也不新增 API。CLI 不会上传 chart 资源,也不会调用任何 chart 创建接口;它只把通过 marker 外壳、hash 和 JSON spec 基础校验的 marker 留在同一个 `slide.content` SVG 中。 ## 图片处理 @@ -85,9 +85,9 @@ CLI 会: } ``` -## Chart Direct Snapshot +## Chart Spec Marker -`slides +create-svg` 支持一种最小 chart marker,用于透传已经生成好的 SXSD chart XML snapshot: +`slides +create-svg` 支持一种最小 chart marker,用于透传 canonical JSON chart spec。payload 不是 SXSD `` XML,也不是 chart snapshot/staticData;服务端会在 SVGlide parser 内部把 spec 转成 chart 创建所需数据: ```xml BASE64URL_PAYLOAD ``` +Decoded canonical JSON shape: + +```json +{"version":"svglide-chart-spec/v1","chartType":"bar","data":{"categories":["Q1","Q2"],"series":[{"name":"Revenue","values":[12.5,18]}]}} +``` + CLI 校验范围只包括: - marker 必须是 root `` 直系 ``。 - `slide:chart-ref` 和 `x/y/width/height` bbox 必填,bbox 只接受数字或 `px`。 - marker 内必须且只能有一个 ``。 -- metadata 必须使用 `data-svglide-chart="svglide-chart-inline/v1"`、`data-format="sxsd-chart-v1"`、`data-encoding="base64url"`。 -- payload 必须是无 padding base64url,`data-payload-hash` 必须匹配 decoded bytes 的 sha256。 -- decoded payload 必须是单根 `` XML。 +- metadata 必须使用 `data-svglide-chart="svglide-chart-inline/v1"`、`data-format="svglide-chart-spec-v1"`、`data-encoding="base64url-json"`。 +- payload 必须是无 padding base64url,`data-payload-hash` 必须匹配 decoded canonical JSON bytes 的 sha256;不要对 base64 文本计算 hash。 +- decoded payload 必须是 JSON object,且包含 `version="svglide-chart-spec/v1"`、`chartType`、`data.categories`、`data.series[].name` 和 `data.series[].values`。 +- MVP 只支持 `chartType="bar"` / `"line"`;`categories` 和每个 `values` 数组长度必须一致;`values` 只能是有限 JSON number。 -CLI 不检查 `` 内部的系列、样式、数据范围等业务合法性;这些仍由服务端 chart/snapshot 消费方负责。`slide:role="whiteboard"` 和旧的 `data-svglide-whiteboard` marker 明确不属于 `+create-svg` 协议面。 +旧 `sxsd-chart-v1` / `base64url` 的 SXSD `` XML payload 不属于 SVGlide chart marker 协议面,会被 CLI 拒绝。`slide:role="whiteboard"` 和旧的 `data-svglide-whiteboard` marker 明确不属于 `+create-svg` 协议面。 ## 生成质量规则 @@ -617,7 +624,7 @@ Preview 阶段优先使用这些来源来快速获得丰富视觉;正式交付 内容页可以用三种方式提高密度,不要把高密度等同于堆文字: - `text-dense`: 多解释、多证据、多注释,适合背景分析和概念讲解。 -- `chart-dense`: SVG shape 手绘矩阵、流程、时间线、微柱状、雷达、散点、标尺;如果必须保留原生 chart snapshot,使用 root direct chart marker;不要把外部图表截图当成唯一方案。 +- `chart-dense`: SVG shape 手绘矩阵、流程、时间线、微柱状、雷达、散点、标尺;如果需要原生 bar/line chart,使用 root chart spec marker;不要把外部图表截图当成唯一方案。 - `visual-dense`: 高级视觉图案或图片上叠加标注层、数据 callout、局部标签、对比线和图例。 视觉区要补足可读细节,避免只有装饰符号: diff --git a/skills/lark-slides/references/svg-protocol.md b/skills/lark-slides/references/svg-protocol.md index 4d2e6ba62..524006cd6 100644 --- a/skills/lark-slides/references/svg-protocol.md +++ b/skills/lark-slides/references/svg-protocol.md @@ -31,7 +31,7 @@ - `slide:role="shape"` 目前只支持 `rect`、`ellipse`、`circle`、`line`、`path`、`foreignObject`。 - 文本使用文本型 shape:`...`。 - 图片使用 ``;本地占位符写成 `href="@./image.png"`。 -- 原生 chart snapshot 可使用 root 直系 `` marker;marker 内只能有一个 chart metadata,metadata payload 是 base64url 编码后的单根 `` XML。 +- 原生 chart 可使用 root 直系 `` spec marker;marker 内只能有一个 chart metadata,metadata payload 是 base64url 编码后的 canonical JSON chart spec,不是 SXSD `` XML,也不是 chart snapshot/staticData。 - `` 和 `
", "sha256:" + "0" * 64))} + {chart_marker(chart_metadata(chart_spec(), "sha256:" + "0" * 64))} abc @@ -207,20 +215,47 @@ class SvgPreflightTest(unittest.TestCase): self.assertIn("unsupported_whiteboard_role", codes) self.assertIn("legacy_whiteboard_marker", codes) - def test_lint_svg_rejects_chart_marker_payload_hash_and_root(self) -> None: + def test_lint_svg_rejects_chart_marker_payload_hash_and_spec(self) -> None: svg = f""" - {chart_marker(chart_metadata("", "sha256:" + "0" * 64))} - {chart_marker(chart_metadata("
"))} + {chart_marker(chart_metadata(chart_spec(), "sha256:" + "0" * 64))} + {chart_marker(chart_metadata('{"version":"svglide-chart-spec/v1","chartType":"pie","data":{"categories":["Q1"],"series":[{"name":"Revenue","values":[12]}]}}'))} """ result = svg_preflight.lint_svg(with_contract(svg)) codes = [issue["code"] for issue in result["issues"]] self.assertIn("chart_marker_payload_hash_mismatch", codes) - self.assertIn("chart_marker_payload_root", codes) + self.assertIn("chart_marker_payload_spec", codes) + + def test_lint_svg_rejects_old_sxsd_chart_marker_format(self) -> None: + svg = f""" + + {chart_marker(chart_metadata("", data_format="sxsd-chart-v1", data_encoding="base64url"))} + + """ + result = svg_preflight.lint_svg(with_contract(svg)) + codes = [issue["code"] for issue in result["issues"]] + self.assertIn("chart_marker_metadata_format", codes) + self.assertIn("chart_marker_metadata_encoding", codes) + + def test_lint_svg_rejects_chart_marker_values_mismatch(self) -> None: + svg = f""" + + {chart_marker(chart_metadata('{"version":"svglide-chart-spec/v1","chartType":"bar","data":{"categories":["Q1","Q2"],"series":[{"name":"Revenue","values":[12]}]}}'))} + + """ + result = svg_preflight.lint_svg(with_contract(svg)) + codes = [issue["code"] for issue in result["issues"]] + self.assertIn("chart_marker_payload_spec", codes) def test_lint_svg_rejects_duplicate_chart_refs(self) -> None: svg = f""" @@ -228,8 +263,8 @@ class SvgPreflightTest(unittest.TestCase): xmlns:slide="https://slides.bytedance.com/ns" slide:role="slide" width="960" height="540" viewBox="0 0 960 540"> - {chart_marker(chart_metadata(""))} - {chart_marker(chart_metadata(""))} + {chart_marker(chart_metadata(chart_spec()))} + {chart_marker(chart_metadata(chart_spec("line")))} """ result = svg_preflight.lint_svg(with_contract(svg))